all the directives are copied in with their types. working on validators now.

This commit is contained in:
brent s. 2020-09-27 03:23:58 -04:00
parent c22786204a
commit 4b912a8dae
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
21 changed files with 859 additions and 131 deletions

View File

@ -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.

<!--
The first time you run it, it will quite possibly take a **very** long time. This is
because it's generating fresh DH parameters, which is a very time-consuming process.
Subsequent runs will not take as long, however, as checks are put in place to determine
@ -62,6 +63,7 @@ if custom DH parameters have been generated or not yet. If it's running on a GNU
system and you have [`haveged`](http://www.issihosts.com/haveged/) installed, that will
significantly speed up the process (SSHSecure will start it automatically if it isn't
running already).
-->

## FAQ


29
TODO
View File

@ -1,27 +1,24 @@
-sshkeys (see ref/<type>/parse_poc_<keytype>.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?
-- Track options in struct?
--- add subtype structs

268
config/const.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/

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,
}
)

81
config/func.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/

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
}

150
config/struct.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/

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
}

10
config/type.go Normal file
View File

@ -0,0 +1,10 @@
package config

type sshBool bool

func (b sshBool) Str() string {
if b {
return "yes"
}
return "no"
}

34
config/validator.go Normal file
View File

@ -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
}

9
go.mod
View File

@ -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
)

24
go.sum
View File

@ -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=

View File

@ -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.
}
)
*/

View File

@ -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
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/

package moduli

func main() {

}

View File

@ -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

View File

@ -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:

View File

@ -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 <https://www.gnu.org/licenses/>.
*/

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"
)

View File

@ -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 (

View File

@ -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 <https://www.gnu.org/licenses/>.
*/

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")
}
}
return nil
}

View File

@ -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 {

View File

@ -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 <https://www.gnu.org/licenses/>.
*/

package sshkeys

func main() {

}

172
utils/files.go Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/

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
}

10
utils/struct.go Normal file
View File

@ -0,0 +1,10 @@
package utils

import (
"time"
)

type runInfo struct {
TimeStarted time.Time
Pid int
}