diff --git a/README.md b/README.md index 17c1d3f..5be518c 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,8 @@ no identifying information other than the fact that you are using SSHSecure. It will create backups of any file(s) it replaces and automatically rolls back `sshd` configuration changes if it does not pass the syntax check (`sshd -t`) to avoid accidentally locking you out. - + + ## FAQ diff --git a/TODO b/TODO index 45c53f9..bd76d47 100644 --- a/TODO +++ b/TODO @@ -1,27 +1,24 @@ --sshkeys (see ref//parse_poc_.go for POC) ---hostkeys (https://security.stackexchange.com/questions/211106/what-is-the-difference-between-host-and-client-ssh-key-generation)? --moduli dhparams generation (dh.c? moduli.c?) ---ssh-keygen.c, ~L3565 - - General/common --- Locking? --- Constants for common file dests --- Func to write to dest, backing up dest if exists first --- Test ssh config (sshd -t) and rollback if fail +-- HALF-DONE: Locking? +-- DONE: Constants for common file dests +-- DONE: Func to write to dest, backing up dest if exists first +-- DONE: Test ssh config (sshd -t) and rollback if fail +-- When completely done, go.mod - Key generation -- DONE: Generate priv/pubkeys --- Build key structure +-- DONE: Build key structure -- write out base64 with headers to files - SSH Moduli --- Do DH param gen in goroutine so we can do other things while it spawns and runs --- Check if haveged is running. If not and installed, start it. --- Generate moduli --- Render to /etc/ssh/moduli format ---- custom moduli marshaler/unmarshaler? (e.g. https://stackoverflow.com/a/50211222) +-- NOT NEEDED?: Do DH param gen in goroutine so we can do other things while it spawns and runs +--- Check if haveged is running. If not and installed, start it. +--- Generate moduli +-- DONE: Render to /etc/ssh/moduli format +--- DONE: custom moduli marshaler/unmarshaler? (e.g. https://stackoverflow.com/a/50211222) -- Write to dest - Config -- Need to merge in changes --- Track options in struct? \ No newline at end of file +-- Track options in struct? +--- add subtype structs diff --git a/config/const.go b/config/const.go new file mode 100644 index 0000000..2bf528c --- /dev/null +++ b/config/const.go @@ -0,0 +1,268 @@ +/* + SSHSecure - a program to harden OpenSSH from defaults + Copyright (C) 2020 Brent Saner + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package config + +const ( + // To check if this release of SSHSecure is up-to-date with the default values. + // upstreamSshdURL string = "https://anongit.mindrot.org/openssh.git/plain/sshd_config" + upstreamSshdURL string = "https://raw.githubusercontent.com/openssh/openssh-portable/master/sshd_config" + upstreamSshdCksum string = "952C844D7B36C54B03E2ADFB24860405" + + "1A702620A0ADC0738A8C30DC83D42A75" + + "27F5B3C184E779B1430168950F7695A1" + + "AA249F7CC719DEC1631ACFDDC2E8B653" + // upstreamSshURL string = "https://anongit.mindrot.org/openssh.git/plain/ssh_config" + upstreamSshURL string = "https://raw.githubusercontent.com/openssh/openssh-portable/master/ssh_config" + upstreamSshCksum string = "FF2D600465CC5D9CFBB57346491CCAF2" + + "C917E2F0C7B4D4EF6B851940948B55BD" + + "88205AC8153210ECA6C6BEA38E800F33" + + "7562C0190AE760220417A5DC2E00A5E1" +) + +// These are items that this program modifies. +var ( + // sshdModify are values we modify. + sshdModify = [...]string{ + "HostKey", + "PermitRootLogin", + "StrictModes", + "PubkeyAuthentication", + "PasswordAuthentication", + "PermitEmptyPasswords", + "ChallengeResponseAuthentication", + "KexAlgorithms", + "Protocol", + "Ciphers", + "MACs", + } + + // sshModify are values we modify. + sshModify = [...]string{""} +) + +// These are collections of long lists of valid values. +var ( + // sshdMulti are values that can be specified multiple times (multiple lines). + sshdMulti = [...]string{ + "", + } + + // authMethods are authentication methods that openssh supports. + authMethods = []string{ + "any", + "keyboard-interactive", + "keyboard-interactive:bsdauth", + "keyboard-interactive:pam", + "gssapi-with-mic", + "hostbased", + "none", + "password", + "publickey", + } + + // ciphers are cipher algorithms that openssh supports. + ciphers = []string{ + "3des-cbc", + "aes128-cbc", + "aes192-cbc", + "aes256-cbc", + "aes128-ctr", + "aes192-ctr", + "aes256-ctr", + "aes128-gcm@openssh.com", + "aes256-gcm@openssh.com", + "chacha20-poly1305@openssh.com", + } + + // forwardAllows are shared values used by forwarding access control. + forwardAllows = []string{"yes", "all", "no", "local", "remote"} + + // hostkeyTypes are algorithms/types used for host keys. + // The following should generate the same list. + // ssh -Q HostKeyAlgorithms | sed -re 's/^/"/g' -e 's/$/",/g' + // ssh -Q HostbasedAcceptedKeyTypes | sed -re 's/^/"/g' -e 's/$/",/g' + hostkeyTypes = []string{ + "ssh-ed25519", + "ssh-ed25519-cert-v01@openssh.com", + "sk-ssh-ed25519@openssh.com", + "sk-ssh-ed25519-cert-v01@openssh.com", + "ssh-rsa", + "rsa-sha2-256", + "rsa-sha2-512", + "ssh-dss", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "sk-ecdsa-sha2-nistp256@openssh.com", + "ssh-rsa-cert-v01@openssh.com", + "rsa-sha2-256-cert-v01@openssh.com", + "rsa-sha2-512-cert-v01@openssh.com", + "ssh-dss-cert-v01@openssh.com", + "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384-cert-v01@openssh.com", + "ecdsa-sha2-nistp521-cert-v01@openssh.com", + "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", + } + + // ipQoS is a list of valid QoS profiles. + ipQoS = []string{ + // This also supports a "numeric value" per sshd_config(5), + // but I have no idea what those values are, their range, etc. + // So strings only. Makes for more readable configs anyways. + // TODO: is this specified in the source anywhere? + "af11", + "af12", + "af13", + "af21", + "af22", + "af23", + "af31", + "af32", + "af33", + "af41", + "af42", + "af43", + "cs0", + "cs1", + "cs2", + "cs3", + "cs4", + "cs5", + "cs6", + "cs7", + "ef", + "le", + "lowdelay", + "throughput", + "reliability", + "none", + } + + // kexAlgos is a lost of valid kex ("KEy eXchange") algorithms. + // ssh -Q kex | sed -re 's/^/"/g' -e 's/$/",/g' + kexAlgos = []string{ + "diffie-hellman-group1-sha1", + "diffie-hellman-group14-sha1", + "diffie-hellman-group14-sha256", + "diffie-hellman-group16-sha512", + "diffie-hellman-group18-sha512", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group-exchange-sha256", + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + "curve25519-sha256", + "curve25519-sha256@libssh.org", + "sntrup4591761x25519-sha512@tinyssh.org", + } + + // sigAlgos is a list of valid algorithms for CA signatures. + sigAlgos = []string{ + // These are the defaults. TODO: are any others valid? + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "ssh-ed25519", + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-rsa", + } + + // logLevels is a list of valid LogLevel levels. + logLevels = []string{ + "QUIET", + "FATAL", + "ERROR", + "INFO", // default + "VERBOSE", + "DEBUG", // same as DEBUG1 + "DEBUG1", // same as DEBUG + "DEBUG2", + "DEBUG3", + } + + // macAlgos is a list of valid MAC (Message Authentication Code) values. "-etm" algos are recommended by upstream. + // ssh -Q mac | sed -re 's/^/"/g' -e 's/$/",/g' + macAlgos = []string{ + "hmac-sha1", + "hmac-sha1-96", + "hmac-sha2-256", + "hmac-sha2-512", + "hmac-md5", + "hmac-md5-96", + "umac-64@openssh.com", + "umac-128@openssh.com", + "hmac-sha1-etm@openssh.com", + "hmac-sha1-96-etm@openssh.com", + "hmac-sha2-256-etm@openssh.com", + "hmac-sha2-512-etm@openssh.com", + "hmac-md5-etm@openssh.com", + "hmac-md5-96-etm@openssh.com", + "umac-64-etm@openssh.com", + "umac-128-etm@openssh.com", + } +) + +// This is a collection related to Match blocks. +var ( + // sshdMatchCriteria is a list of valid criteria that can be used in a Match block. + // Valid keys for sshdMatchCriteria are tracked via field names in SshdMatchRule. + // Multiple criteria can be specified by e.g. "Match User foo, Host bar.tld" + sshdMatchCriteria = []string{ + "All", + "User", + "Group", + "Host", + "LocalAddress", + "LocalPort", + "RDomain", + "Address", + } +) + +// The following are validator maps. +var ( + // These directives can also begin with "+", "-", or "^", so they need to be stripped off. + // TODO: How to do this non-destructively? + sshdStripPre = []string{ + "HostbasedAcceptedKeyTypes", + "KexAlgorithms", + } + // validSshdSingleVals are values that can accept a single string value from a static list. + validSshdSingleVals = map[string][]string{ + "AddressFamily": {"any", "inet", "inet6"}, + "AllowStreamLocalForwarding": forwardAllows, + "AllowTcpForwarding": forwardAllows, + "Compression": {"yes", "delayed", "no"}, // "delayed" is legacy, same as "yes". + "FingerprintHash": {"sha256", "md5"}, + "GatewayPorts": {"yes", "no", "clientspecified"}, + "IgnoreRHosts": {"yes", "shosts-only", "no"}, + "LogLevel": logLevels, + } + + // validSshdMultiVals are values that can accept multiple values from a static list. + validSshdMultiVals = map[string][]string{ + "AuthenticationMethods": authMethods, + "CASignatureAlgorithms": sigAlgos, + "Ciphers": ciphers, + "HostbasedAcceptedKeyTypes": hostkeyTypes, // NOTE: Can also begin with "+", "-", or "^" + "HostKeyAlgorithms": hostkeyTypes, + "KexAlgorithms": kexAlgos, // NOTE: Can also begin with "+", "-", or "^" + "MACs": macAlgos, + } +) diff --git a/config/func.go b/config/func.go new file mode 100644 index 0000000..19dbdcf --- /dev/null +++ b/config/func.go @@ -0,0 +1,81 @@ +/* + SSHSecure - a program to harden OpenSSH from defaults + Copyright (C) 2020 Brent Saner + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package config + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + + "github.com/pkg/errors" + "r00t2.io/sysutils/exec_extra" + "r00t2.io/sysutils/paths" +) + +func TestConfig(config *[]byte) (bool, error) { + var sysPaths []string + var binPath string + var tmpConf *os.File + var err error + var stdout, stderr bytes.Buffer + // sshd *requires* to be invoked with an absolute path. + sysPaths, err = paths.GetPathEnv() + if err != nil { + return false, err + } + for _, p := range sysPaths { + fpath := path.Join(p, "sshd") + if exists, err := paths.RealPathExists(&fpath); err != nil { + return false, err + } else if !exists { + continue + } + binPath = fpath + break + } + tmpConf, err = ioutil.TempFile("/tmp", ".test.sshconf.") + if err != nil { + return false, err + } + defer os.Remove(tmpConf.Name()) + if err = tmpConf.Chmod(0600); err != nil { + return false, err + } + if _, err = tmpConf.Write(*config); err != nil { + return false, err + } + cmd := exec.Command(binPath, + "-T", fmt.Sprintf("-f %v", + tmpConf.Name())) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + exitStatus, err := exec_extra.ExecCmdReturn(cmd) + if err != nil { + return false, err + } + if exitStatus != 0 { + // TODO: also handle non-empty stderr? + e := fmt.Sprintf("returned status/exit code %d", exitStatus) + return false, errors.New(e) + } + return true, nil +} diff --git a/config/struct.go b/config/struct.go new file mode 100644 index 0000000..b90c006 --- /dev/null +++ b/config/struct.go @@ -0,0 +1,150 @@ +/* + SSHSecure - a program to harden OpenSSH from defaults + Copyright (C) 2020 Brent Saner + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package config + +/* + NOTATION KEY: + .: Exists in default upstream config (but usually they're commented out) + +: These values are/may be modified by this program. + *: These values are not in the upstream config but are allowed via the man page (sshd_config(5) and ssh_config(5)). +*/ + +// More or less a subset of SshdConf. These are valid keywords for Match blocks in sshd_config. +type SshdMatchRule struct { + AcceptEnv []string // * + AllowAgentForwarding sshBool // . + AllowGroups []string // * + AllowStreamLocalForwarding string // * + AllowTcpForwarding string // . + AllowUsers []string // * + AuthenticationMethods []string // +* + AuthorizedKeysCommand string // . + AuthorizedKeysCommandUser string // . + AuthorizedKeysFile string // . + AuthorizedPrincipalsCommand string // * + AuthorizedPrincipalsCommandUser string // * + AuthorizedPrincipalsFile string // . + Banner string // . + ChrootDirectory string // . + ClientAliveCountMax int // . + ClientAliveInterval int // . + DenyGroups []string // * + DenyUsers []string // * + ForceCommand string // * + GatewayPorts string // . + GSSAPIAuthentication sshBool // . + HostbasedAcceptedKeyTypes []string // * + HostbasedAuthentication sshBool // . + HostbasedUsesNameFromPacketOnly sshBool // * + IgnoreRhosts string // . + // Do we handle includes? Or just let sshd -T handle it? + Include string // * + // Accepts one or two. If two, first is interactive and second is non-interactive. + IPQoS [2]string // * + KbdInteractiveAuthentication sshBool // * + KerberosAuthentication sshBool // . + LogLevel string // . + MaxAuthTries int // . + MaxSessions int // . + PasswordAuthentication sshBool // .+ + PermitEmptyPasswords sshBool // + + PermitListen string // * + PermitOpen string // * + PermitRootLogin string // .+ + PermitTTY sshBool // . + PermitTunnel string // . + PermitUserRC sshBool // * + PubkeyAcceptedKeyTypes []string // * + PubkeyAuthentication sshBool // .+ + RekeyLimit string // . + RevokedKeys string // * + RDomain string // * + SetEnv map[string]string // * + // max is 4095, it goes in the config as an octal. + StreamLocalBindMask uint16 // * + StreamLocalBindUnlink sshBool // * + TrustedUserCAKeys string // * + X11DisplayOffset int // . + X11Forwarding sshBool // . +} + +// SshdConf represents an /etc/ssh/sshd_config file's directives/values. +// Values in SshdMatchRule are not reproduced here. +type SshdConf struct { + SshdMatchRule + AddressFamily string // . + CASignatureAlgorithms []string // * + ChallengeResponseAuthentication sshBool // .+ + Ciphers []string // +* + Compression string // . + DisableForwarding sshBool // * + ExposeAuthInfo sshBool // * + FingerprintHash string // * + GSSAPICleanupCredentials sshBool // . + GSSAPIStrictAcceptorCheck sshBool // * + HostCertificate string // * + HostKeyAgent string // * + HostKeyAlgorithms []string // +* + HostKey []string // . + IgnoreUserKnownHosts sshBool // . + KerberosGetAFSToken sshBool // . + KerberosOrLocalPasswd sshBool // . + KerberosTicketCleanup sshBool // . + KexAlgorithms string // +* + ListenAddress ListenAddr // . + LoginGraceTime string // . + MACs []string // +* + Match map[string]string // . + MaxStartups string // . + PermitUserEnvironment sshBool // . + PidFile string // . + Port uint16 // . + PrintLastLog sshBool // .+ + PrintMotd sshBool // . + Protocol int // +* + PubkeyAuthOptions string // * + SecurityKeyProvider string // * + StrictModes sshBool // .+ + Subsystem string // . + SyslogFacility string // . + TCPKeepAlive sshBool // . + UseDNS sshBool // . + UsePAM sshBool // . + VersionAddendum string // . + X11UseLocalhost sshBool // . + XAuthLocation string // * +} + +// SshConf represents an /etc/ssh/ssh_config (or ~/.ssh/config) file +type SshConf struct { + // These are in the default upstream sshd_config so we don't touch them. (Most, if not all, are commented out.) + // We just have them here to parse them. + Host map[string]string +} + +type ListenAddr struct { + Addr string // hostname|address, hostname:port, IPv4_address:port, or [hostname|address]:port in conf string. + Port uint16 + RDomain string +} + +type MatchSshd struct { + Criteria map[string]string + Rules []SshdMatchRule +} diff --git a/config/type.go b/config/type.go new file mode 100644 index 0000000..a54dc27 --- /dev/null +++ b/config/type.go @@ -0,0 +1,10 @@ +package config + +type sshBool bool + +func (b sshBool) Str() string { + if b { + return "yes" + } + return "no" +} diff --git a/config/validator.go b/config/validator.go new file mode 100644 index 0000000..16052b3 --- /dev/null +++ b/config/validator.go @@ -0,0 +1,34 @@ +package config + +import ( + "fmt" + + "github.com/oleiade/reflections" + "github.com/pkg/errors" +) + +var err error + +func (c *SshdConf) Validate() (bool, error) { + for k, v := range validSshdSingleVals { + realV, err := reflections.GetField(c, k) + if err != nil { + return false, err + } + valid := false + for _, i := range v { + if i == realV { + valid = true + break + } + } + if !valid { + e := fmt.Sprintf( + "field %v value %v is not allowed", + k, realV) + return false, errors.New(e) + } + } + + return true, nil +} diff --git a/go.mod b/go.mod deleted file mode 100644 index 2549b8b..0000000 --- a/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module r00t2.io/sshsecure - -go 1.15 - -require ( - github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a - github.com/go-restruct/restruct v1.2.0-alpha - golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index d0fa314..0000000 --- a/go.sum +++ /dev/null @@ -1,24 +0,0 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU= -github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0= -github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= -github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/moduli/const.go b/moduli/const.go index bf5cec0..dc299f2 100644 --- a/moduli/const.go +++ b/moduli/const.go @@ -27,7 +27,8 @@ import ( // Misc const ( // Where to find an up-to-date copy of the upstream moduli and its SHA3-512 (NIST) checksum. - pregenURL string = "https://anongit.mindrot.org/openssh.git/tree/moduli" + // pregenURL string = "https://anongit.mindrot.org/openssh.git/plain/moduli" + pregenURL string = "https://raw.githubusercontent.com/openssh/openssh-portable/master/moduli" // This is the best way I could think of to verify integrity, since the file itself doesn't have a signature or anything like that. pregenCksum string = "106EDB19A936608D065D2E8E81F7BDE7" + "434AF80EF81102E9440B99ACB98FBEF8" + @@ -37,7 +38,8 @@ const ( parseTag string = "sshmoduli" // The recommended minimum moduli to have available. recMinMod int = 400 - // The + // The minimum bits for filtering. It's generally bits - 1 + minBits uint8 = 4095 ) // The header line on the /etc/ssh/moduli file. @@ -55,7 +57,8 @@ const ( timeFormat string = "20060102150405" // %Y%m%d%H%M%S ) -// For validation +// For validation. Currently unused. +/* var ( validTypes = []uint8{ 0, // Unknown, not tested @@ -69,3 +72,4 @@ var ( 0x04, // Probabilistic Miller-Rabin primality tests. } ) +*/ diff --git a/moduli/func.go b/moduli/func.go index e946058..f083a2e 100644 --- a/moduli/func.go +++ b/moduli/func.go @@ -28,33 +28,38 @@ import ( "golang.org/x/crypto/sha3" ) -func getPregen() ([]byte, error) { +// getPregen gets the pregenerated moduli from upstream mirror. +func getPregen() (Moduli, error) { + m := Moduli{} // get the pregenerated moduli resp, err := http.Get(pregenURL) if err != nil { - return nil, err + return m, err } if resp.StatusCode != http.StatusOK { - return nil, errors.New(fmt.Sprintf("returned status code %v: %v", resp.StatusCode, resp.Status)) + return m, errors.New(fmt.Sprintf("returned status code %v: %v", resp.StatusCode, resp.Status)) } defer resp.Body.Close() b := make([]byte, resp.ContentLength) if _, err = resp.Body.Read(b); err != nil { - return nil, err + return m, err } // and compare the SHA3-512 (NIST) checksum. s := sha3.New512() if _, err = s.Write(b); err != nil { // TODO: return nil instead of b? - return b, err + return m, err } goodCksum, err := hex.DecodeString(pregenCksum) if err != nil { - return nil, err + return m, err } // We just compare the bytestrings. if bytes.Compare(s.Sum(nil), goodCksum) != 0 { - return nil, errors.New("checksums do not match") + return m, errors.New("checksums do not match") } - return b, nil + if err := Unmarshal(b, m); err != nil { + return m, err + } + return m, nil } diff --git a/moduli/main.go b/moduli/main.go deleted file mode 100644 index e44b7ba..0000000 --- a/moduli/main.go +++ /dev/null @@ -1,23 +0,0 @@ -/* - SSHSecure - a program to harden OpenSSH from defaults - Copyright (C) 2020 Brent Saner - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package moduli - -func main() { - -} diff --git a/moduli/parser.go b/moduli/parser.go index d503bee..72fdf39 100644 --- a/moduli/parser.go +++ b/moduli/parser.go @@ -50,7 +50,7 @@ func (m *Moduli) Marshal() ([]byte, error) { } // marshalEntry is used to parse a specific DH entry into the moduli. -func (m *ModuliEntry) marshalEntry() ([]byte, error) { +func (m *Entry) marshalEntry() ([]byte, error) { mod := hex.EncodeToString(m.Modulus.Bytes()) s := fmt.Sprintf( "%v %v %v %v %v %v %v\n", @@ -68,10 +68,10 @@ func (m *ModuliEntry) marshalEntry() ([]byte, error) { // Unmarshal writes the Moduli format into m from the /etc/ssh/moduli format in data. func Unmarshal(data []byte, m Moduli) error { var lines []string - var entries []ModuliEntry + var entries []Entry lines = strings.Split(string(data), "\n") for _, line := range lines { - e := ModuliEntry{} + e := Entry{} if reSkipLine.MatchString(line) { continue } @@ -85,7 +85,8 @@ func Unmarshal(data []byte, m Moduli) error { return nil } -func unmarshalEntry(line []string, m ModuliEntry) error { +func unmarshalEntry(line []string, m Entry) error { + var err error if len(line) != 7 { return errors.New("field count mismatch") } @@ -117,3 +118,19 @@ func unmarshalEntry(line []string, m ModuliEntry) error { m.Modulus.SetBytes(modb) return nil } + +func (m *Moduli) Harden() error { + var entries []Entry + for _, e := range m.Params { + if e.Size >= minBits { + entries = append(entries, e) + } + } + m.Params = entries + if len(m.Params) < recMinMod { + return errors.New("does not meet recommended minimum moduli") + } + return nil +} + +// TODO: find way of testing/sieving primes diff --git a/moduli/struct.go b/moduli/struct.go index ccff1f2..b4459cb 100644 --- a/moduli/struct.go +++ b/moduli/struct.go @@ -26,11 +26,11 @@ import ( // Moduli contains all data needed for generated /etc/ssh/moduli. of ModuliEntry entries. type Moduli struct { Header string - Params []ModuliEntry + Params []Entry } // Moduli is a struct reflecting the format of a single /etc/ssh/moduli entry. See moduli(5) for details. -type ModuliEntry struct { +type Entry struct { Time time.Time // YYYYMMDDHHSS /* // man 5 moduli: diff --git a/sharedconsts/const.go b/sharedconsts/const.go index 854ab83..cbcdaae 100644 --- a/sharedconsts/const.go +++ b/sharedconsts/const.go @@ -1,3 +1,21 @@ +/* + SSHSecure - a program to harden OpenSSH from defaults + Copyright (C) 2020 Brent Saner + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + package sharedconsts import ( @@ -11,3 +29,22 @@ const ( ) var IDCmnt = string(fmt.Sprintf("Autogenerated by %v (%v)", projName, projUrl)) + +// Common file/directory paths. +const ( + // System-wide files. + LockFile string = "/tmp/.sshsecure.lck" + SysSshConfDir string = "/etc/ssh" + // The following are joined with SysSshConfDir. + SshdConfFile string = "sshd_config" + SshConfFile string = "ssh_config" + HostEd25519File string = "ssh_host_ed25519_key" + HostRsaFile string = "ssh_host_rsa_key" + ModuliFile string = "moduli" + // Invoking user's files. + UserSshConfDir string = "~/.ssh" + // The following are joined with userSshConfDir. + UserSshConfFile string = "config" + UserEd25519File string = "id_ed25519" + UserRsaFile string = "id_rsa" +) diff --git a/sshkeys/const.go b/sshkeys/const.go index 77df225..b3c0c3e 100644 --- a/sshkeys/const.go +++ b/sshkeys/const.go @@ -43,7 +43,7 @@ const ( CipherAes256Ctr string = "aes256-ctr" ) -var allowed_ciphers = [...]string{CipherNull, CipherAes256Ctr} +var allowedCiphers = [...]string{CipherNull, CipherAes256Ctr} // Key types. const ( @@ -51,7 +51,7 @@ const ( KeyRsa string = "ssh-rsa" ) -var allowed_keytypes = [...]string{KeyEd25519, KeyRsa} +var allowedKeytypes = [...]string{KeyEd25519, KeyRsa} // KDF names. I believe only bcrypt is supported upstream currently. const ( @@ -59,7 +59,7 @@ const ( KdfBcrypt string = "bcrypt" ) -var allowed_kdfnames = [...]string{KdfNull, KdfBcrypt} +var allowedKdfnames = [...]string{KdfNull, KdfBcrypt} // Key lengths. const ( diff --git a/sshkeys/crypt.go b/sshkeys/crypt.go index 5ba9af6..429c96e 100644 --- a/sshkeys/crypt.go +++ b/sshkeys/crypt.go @@ -1,12 +1,30 @@ +/* + SSHSecure - a program to harden OpenSSH from defaults + Copyright (C) 2020 Brent Saner + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + package sshkeys import ( - `crypto/aes` - `crypto/cipher` - `errors` + "crypto/aes" + "crypto/cipher" + "errors" // `golang.org/x/crypto/ssh/internal/bcrypt_pbkdf` Golang doesn't let you import "internal" libs. Fine. Lame language. - `github.com/dchest/bcrypt_pbkdf` + "github.com/dchest/bcrypt_pbkdf" ) func (k *EncryptedSSHKeyV1) setCrypt() error { @@ -22,6 +40,7 @@ func (k *EncryptedSSHKeyV1) setCrypt() error { } func (k *EncryptedSSHKeyV1) setCipher() error { + var err error switch k.CipherName { case CipherAes256Ctr: if k.Crypt.Cipher, err = aes.NewCipher(k.Crypt.PrivateKey); err != nil { @@ -37,14 +56,15 @@ func (k *EncryptedSSHKeyV1) setCipher() error { } func (k *EncryptedSSHKeyV1) setKDF() error { + var err error // Upstream only currently supports bcrypt_pbkdf ("bcrypt"). // This should always eval to true, but is here for future planning in case other KDF are implemented. switch k.KDFName { case KdfBcrypt: if k.Crypt.CryptKey, err = bcrypt_pbkdf.Key(k.Passphrase, - k.KDFOpts.Salt, - int(k.KDFOpts.Rounds), - kdfKeyLen + len(k.KDFOpts.Salt), + k.KDFOpts.Salt, + int(k.KDFOpts.Rounds), + kdfKeyLen+len(k.KDFOpts.Salt), ); err != nil { return err } else { @@ -54,4 +74,5 @@ func (k *EncryptedSSHKeyV1) setKDF() error { default: return errors.New("could not find KDF") } -} \ No newline at end of file + return nil +} diff --git a/sshkeys/func.go b/sshkeys/func.go index 305fdfe..0e9f136 100644 --- a/sshkeys/func.go +++ b/sshkeys/func.go @@ -22,7 +22,6 @@ import ( "bytes" "crypto/rand" "errors" - "fmt" "r00t2.io/sshsecure/sharedconsts" ) @@ -34,19 +33,19 @@ func (k *EncryptedSSHKeyV1) validate() error { var validCipher bool var validKDF bool var validKT bool - for _, v := range allowed_ciphers { + for _, v := range allowedCiphers { if v == k.CipherName { validCipher = true break } } - for _, v := range allowed_kdfnames { + for _, v := range allowedKdfnames { if v == k.KDFName { validKDF = true break } } - for _, v := range allowed_keytypes { + for _, v := range allowedKeytypes { if v == k.DefKeyType { validKT = true } @@ -107,7 +106,7 @@ func (k *EncryptedSSHKeyV1) Generate(force bool) error { return errors.New("unknown key type; could not generate private/public keypair") } k.Keys = append(k.Keys, pk) - // We also need an encrypter/decrypter since this is an encrypted key. + // We also need an encryptor/decryptor since this is an encrypted key. if err := k.setCrypt(); err != nil { return err } @@ -120,7 +119,7 @@ func (k *EncryptedSSHKeyV1) Generate(force bool) error { func (k *SSHKeyV1) validate() error { var validKT bool - for _, v := range allowed_keytypes { + for _, v := range allowedKeytypes { if v == k.DefKeyType { validKT = true } @@ -143,7 +142,7 @@ func (k *SSHKeyV1) Generate(force bool) error { // Currently, OpenSSH has an option for multiple private keys. However, it is hardcoded to 1. // If multiple key support is added in the future, will need to re-tool how I do this, perhaps, in the future. TODO. pk := SSHPrivKey{ - Comment: fmt.Sprintf("Autogenerated via SSHSecure (%v)", projUrl), + Comment: sharedconsts.IDCmnt, } pk.Checksum = make([]byte, 4) if _, err := rand.Read(pk.Checksum); err != nil { diff --git a/sshkeys/main.go b/sshkeys/main.go deleted file mode 100644 index 74f1174..0000000 --- a/sshkeys/main.go +++ /dev/null @@ -1,23 +0,0 @@ -/* - SSHSecure - a program to harden OpenSSH from defaults - Copyright (C) 2020 Brent Saner - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -package sshkeys - -func main() { - -} diff --git a/utils/files.go b/utils/files.go new file mode 100644 index 0000000..79d522d --- /dev/null +++ b/utils/files.go @@ -0,0 +1,172 @@ +/* + SSHSecure - a program to harden OpenSSH from defaults + Copyright (C) 2020 Brent Saner + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package utils + +import ( + "encoding/gob" + "errors" + "fmt" + "os" + gopath "path" + "path/filepath" + "time" + + "r00t2.io/sysutils/paths" + + "r00t2.io/sshsecure/sharedconsts" +) + +func backupWrite(path string, data []byte, system bool, force bool) error { + var err error + var uid int + var gid int + var running bool + var runningInfo runInfo + var curInfo runInfo + uid, gid, err = getPerms(system) + if err != nil { + return err + } + if err = paths.RealPath(&path); err != nil { + return err + } + curInfo = getInfo() + parent := filepath.Dir(path) + parentExists, err := paths.RealPathExists(&parent) + infoFile := gopath.Join( + parent, + fmt.Sprintf(".%v.gob", + filepath.Base(path), + )) + if err = paths.MakeDirIfNotExist(&parent); err != nil { + return err + } + exists, err := paths.RealPathExists(&path) + if err != nil { + return err + } else if !parentExists { + // It exists *now*, but it's a new dir. We set perms thusly. + os.Chmod(parent, 0700) + } + if gobExists, err := paths.RealPathExists(&infoFile); err != nil { + return err + } else if gobExists && !force { + return nil // Don't go any further; the file was already generated. + } + running, runningInfo, err = isRunning() + if err != nil { + return err + } + if exists && running { + e := fmt.Sprintf( + "refusing to write to %v; another instance of program is currently running with PID %v started at %v", + path, runningInfo.Pid, runningInfo.TimeStarted) + return errors.New(e) + } else if exists && !running { + newName := fmt.Sprintf("%v.bak.%d", path, curInfo.TimeStarted.Unix()) + if err = os.Rename(path, newName); err != nil { + return err + } + } + // FINALLY. The business end. + w, err := os.Create(path) + if err != nil { + return err + } + gobFile, err := os.Create(infoFile) + if err != nil { + return err + } + g := gob.NewEncoder(gobFile) + defer w.Close() + defer gobFile.Close() + if err = w.Chmod(0600); err != nil { + return err + } + if err = w.Chown(uid, gid); err != nil { + return err + } + if _, err = w.Write(data); err != nil { + return err + } + if err = gobFile.Chmod(0600); err != nil { + return err + } + if err = gobFile.Chown(uid, gid); err != nil { + return err + } + if err = g.Encode(curInfo); err != nil { + return err + } + return nil +} + +func getInfo() runInfo { + curInfo := runInfo{ + TimeStarted: getTime(), + Pid: getPid(), + } + return curInfo +} + +func getPid() int { + return os.Getpid() +} + +func getPerms(system bool) (int, int, error) { + var uid int + var gid int + if system { + if os.Geteuid() != 0 { + return 0, 0, errors.New("cannot create system file without root permissions") + } + uid = 0 + gid = 0 + } else { + uid = os.Geteuid() + gid = os.Getegid() + } + return uid, gid, nil +} + +func getTime() time.Time { + return time.Now() +} + +func isRunning() (bool, runInfo, error) { + curInfo := getInfo() + var fileInfo runInfo + lckFile := sharedconsts.LockFile + exists, _ := paths.RealPathExists(&lckFile) + if !exists { + // This should never happen. If it does, I goofed the locking (or it's not invoked from a shell). + return false, fileInfo, errors.New("*grabs shotgun* computer's haunted") + } + reader, err := os.Open(lckFile) + if err != nil { + return false, fileInfo, err + } + defer reader.Close() + gobDecoder := gob.NewDecoder(reader) + gobDecoder.Decode(&fileInfo) + if fileInfo != curInfo { + return true, fileInfo, nil + } + return false, fileInfo, nil +} diff --git a/utils/struct.go b/utils/struct.go new file mode 100644 index 0000000..2cedefa --- /dev/null +++ b/utils/struct.go @@ -0,0 +1,10 @@ +package utils + +import ( + "time" +) + +type runInfo struct { + TimeStarted time.Time + Pid int +}