diff --git a/TODO b/TODO new file mode 100644 index 0000000..df7bafd --- /dev/null +++ b/TODO @@ -0,0 +1,3 @@ +-sshkeys (need to figure out generation process) +-moduli dhparams generation (dh.c? moduli.c?) +--ssh-keygen.c, ~L3565 \ No newline at end of file diff --git a/const.go b/const.go deleted file mode 100644 index 05f8dda..0000000 --- a/const.go +++ /dev/null @@ -1,14 +0,0 @@ -package sshsecure - -import ( - "git.square-r00t.net/sshsecure/sshkeys" -) - -const ( - RoundsDefUser uint = 100 - RoundsDefHost uint = 100 - RSABitSize uint = 4096 - DefKeyType string = sshkeys.KEY_ED25519 - DefCipher string = sshkeys.CIPHER_AES256_CTR - DefKDF string = sshkeys.KDF_BCRYPT -) diff --git a/go.mod b/go.mod index ef12c7c..2adc21d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module git.square-r00t.net/sshsecure go 1.15 + +require ( + github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a + golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d026318 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +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= +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= diff --git a/sshkeys/const.go b/sshkeys/const.go index 04117fc..13c15cb 100644 --- a/sshkeys/const.go +++ b/sshkeys/const.go @@ -5,26 +5,53 @@ const ( KeyV1Magic string = "openssh-key-v1" ) -// Cipher names. I believe only AES256-CTR is supported upstream currently. +// "Meta". Used for comment strings, etc. + +const projUrl = "https://git.square-r00t.net/SSHSecure" + +// Defaults. const ( - CIPHER_NULL string = "none" - CIPHER_AES256_CTR string = "aes256-ctr" + defCipher string = CipherAes256Ctr + defKeyType string = KeyEd25519 + defKDF string = KdfBcrypt + defRounds uint32 = 100 + defRSABitSize uint32 = 4096 + defSaltLen int = 16 ) -var allowed_ciphers = [...]string{CIPHER_NULL, CIPHER_AES256_CTR} +// Cipher names. I believe only AES256-CTR is supported upstream currently. +const ( + CipherNull string = "none" + CipherAes256Ctr string = "aes256-ctr" +) + +var allowed_ciphers = [...]string{CipherNull, CipherAes256Ctr} // Key types. const ( - KEY_ED25519 string = "ssh-ed25519" - KEY_RSA string = "ssh-rsa" + KeyEd25519 string = "ssh-ed25519" + KeyRsa string = "ssh-rsa" ) -var allowed_keytypes = [...]string{KEY_ED25519, KEY_RSA} +var allowed_keytypes = [...]string{KeyEd25519, KeyRsa} // KDF names. I believe only bcrypt is supported upstream currently. const ( - KDF_NULL string = "none" - KDF_BCRYPT string = "bcrypt" + KdfNull string = "none" + KdfBcrypt string = "bcrypt" ) -var allowed_kdfnames = [...]string{KDF_NULL, KDF_BCRYPT} +var allowed_kdfnames = [...]string{KdfNull, KdfBcrypt} + +// Key lengths. +const ( + // ED25519 in OpenSSH uses a static key size of 64 bytes. + ed25519Len uint32 = 64 +) + +// Key/Block sizes. +const ( + keyEd25519 uint32 = 32 + blockEd25519 uint32 = 16 + blockNull uint32 = 8 +) diff --git a/sshkeys/func.go b/sshkeys/func.go index 8630dc2..cfb1ec2 100644 --- a/sshkeys/func.go +++ b/sshkeys/func.go @@ -1,51 +1,104 @@ package sshkeys import ( + "crypto/rand" "errors" + "fmt" - "r00t2.io/goutils/checks" + // `golang.org/x/crypto/ssh/internal/bcrypt_pbkdf` Golang doesn't let you import "internal" libs. Fine. Lame language. + "github.com/dchest/bcrypt_pbkdf" ) -func genPrivKey(cipherAlgo string, kdf string, salt []byte, rounds uint32, passphrase string) ([]byte, error) { - return nil, nil -} - -func genPubKey(privKey *[]byte) ([]byte, error) { - if *privKey == nil { - return nil, errors.New("must generate private key before public key") - } - return nil, nil -} - -func (k *EncryptedSSHKeyV1) GeneratePrivate(force bool) error { - if k.Passphrase == "" { +func (k *EncryptedSSHKeyV1) validate() error { + if k.Passphrase == nil { return errors.New("cannot use encrypted key with empty passphrase") } + var validCipher bool + var validKDF bool + var validKT bool + for _, v := range allowed_ciphers { + if v == k.CipherName { + validCipher = true + break + } + } + for _, v := range allowed_kdfnames { + if v == k.KDFName { + validKDF = true + break + } + } + for _, v := range allowed_keytypes { + if v == k.DefKeyType { + validKT = true + } + } + if !validCipher || !validKDF || !validKT { + return errors.New("invalid CipherName, KDFName, or DefKeyType specified") + } + return nil +} + +func (k *EncryptedSSHKeyV1) Generate(force bool) error { + if k.DefKeyType == "" { + k.DefKeyType = defKeyType + } + if k.KDFName == "" { + k.KDFName = defKDF + } + if k.CipherName == "" { + k.CipherName = defCipher + } + if err := k.validate(); err != nil { + return err + } if k.PrivateKeys != nil && !force { return nil // Already generated. } + if k.KDFOpts.Salt == nil { + k.KDFOpts.Salt = make([]byte, defSaltLen) + if _, err := rand.Read(k.KDFOpts.Salt); err != nil { + return err + } + } + if k.KDFOpts.Rounds == 0 { + k.KDFOpts.Rounds = defRounds + } + if k.DefKeyType == KeyRsa && k.BitSize == 0 { + k.BitSize = defRSABitSize + } else if k.DefKeyType == KeyEd25519 { + k.BitSize = ed25519Len + k.KeySize = keyEd25519 + k.BlockSize = blockEd25519 + } + // 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), + } + pk.Checksum = make([]byte, 4) + if _, err := rand.Read(pk.Checksum); err != nil { + return err + } + // 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. + if k.KDFName == KdfBcrypt { + if pk.Key, err = bcrypt_pbkdf.Key(k.Passphrase, k.KDFOpts.Salt, int(k.KDFOpts.Rounds), int(k.KeySize)); err != nil { + return err + } + } return nil } -func (k *EncryptedSSHKeyV1) GeneratePublic(force bool) error { - if err := k.GeneratePrivate(force); err != nil { - return err - } - +func (k *SSHKeyV1) validate() error { return nil } func (k *SSHKeyV1) GeneratePrivate(force bool) error { + k.validate() if k.PrivateKeys != nil && !force { return nil // Already generated. } return nil } - -func (k *SSHKeyV1) GeneratePublic(force bool) error { - if err := k.GeneratePrivate(force); err != nil { - return err - } - return nil -} diff --git a/sshkeys/interface.go b/sshkeys/interface.go index a9560de..204400f 100644 --- a/sshkeys/interface.go +++ b/sshkeys/interface.go @@ -1,6 +1,6 @@ package sshkeys type OpenSSHKeypair interface { - GeneratePrivate(keyType uint8) error - GeneratePublic(keyType uint8) error + GeneratePrivate(force bool) error + GeneratePublic(force bool) error } diff --git a/sshkeys/ref/encrypted/private b/sshkeys/ref/encrypted/private index 61d8307..c633443 100644 --- a/sshkeys/ref/encrypted/private +++ b/sshkeys/ref/encrypted/private @@ -54,12 +54,24 @@ ANNOTATED HEX: 4.0.0.1 00000020 (32) 4.0.0.1.0 bfa2031aa5463113e40e16896af503c5299ead76b09cb63846f41cc4de1740f6 (bytes) 4.0.1 000000a0 (160) - 4.0.1 (AES256-CTR encrypted block) (bytes) - c49777cd0d1a7d37db77a1814991278f8ce99d57 - 2e2c666b93b99867425c60da4652fddb85550985 - 32b51beeee2959f9db5cf5a0905052720c5de25f - 2c4dd87ebcc7bb5ea3d7bcbeacc6b732e4c39295 - d9991a97ef3f0838f8a9bfd43edb340318964908 - 8f6cfb78946fb914e358ac6abc64691072f5f278 - 8534d9d42d7f406bc5090b30df23cb7dd8c5cb93 - 8e41facd6e38e8845b8160bff840598118d447c2 \ No newline at end of file + 4.0.1.0 - 4.0.1.5 (AES256-CBC encrypted block) (bytes) + c49777cd0d1a7d37db77a1814991278f + 8ce99d572e2c666b93b99867425c60da + 4652fddb8555098532b51beeee2959f9 + db5cf5a0905052720c5de25f2c4dd87e + bcc7bb5ea3d7bcbeacc6b732e4c39295 + d9991a97ef3f0838f8a9bfd43edb3403 + 189649088f6cfb78946fb914e358ac6a + bc64691072f5f2788534d9d42d7f406b + c5090b30df23cb7dd8c5cb938e41facd + 6e38e8845b8160bff840598118d447c2 + +DECRYPTED 4.0.1: +(...) +4.0.1 000000a0 (160) + 4.0.1.0 + 4.0.1.1 + 4.0.1.2 + 4.0.2.3 + 4.0.2.4 + 4.0.2.5 \ No newline at end of file diff --git a/sshkeys/ref/sources b/sshkeys/ref/sources index d6e8643..235a35c 100644 --- a/sshkeys/ref/sources +++ b/sshkeys/ref/sources @@ -5,3 +5,27 @@ https://stackoverflow.com/a/56300901/733214 https://stackoverflow.com/a/59283692/733214 https://coolaj86.com/articles/the-openssh-private-key-format/ https://coolaj86.com/articles/the-ssh-public-key-format/ +https://crypto.stackexchange.com/a/40910 +https://flak.tedunangst.com/post/new-openssh-key-format-and-bcrypt-pbkdf +("(Technical note: PBKDF2, aka PKCS #5, supports pluggable hash functions, though in practice everybody uses HMAC-SHA1. The bcrypt pbkdf essentially is PBKDF2, but with bcrypt plugged into it instead.)" +http://www.tedunangst.com/flak/post/bcrypt-pbkdf + +## UPSTREAM +https://github.com/openssh/openssh-portable/blob/master/sshkey.c + funcs: + sshkey_generate (~L1714) + sshkey_private_to_blob2 (~L3833) + sshkey_private_to_fileblob (~L4413) +https://github.com/openssh/openssh-portable/blob/master/cipher.c + funcs: + cipher_ivlen +https://github.com/openssh/openssh-portable/blob/master/ed25519.c + funcs: + crypto_sign_ed25519_keypair (~L26) +https://github.com/openssh/openssh-portable/blob/master/authfile.c + funcs: + sshkey_save_private (~L68) + sshkey_save_private_blob (~L56) +https://github.com/openssh/openssh-portable/blob/master/ssh-keygen.c + funcs: + main (~L3145; ~L3673 onwards for key generation) \ No newline at end of file diff --git a/sshkeys/struct.go b/sshkeys/struct.go index 2ccac0f..3d0752f 100644 --- a/sshkeys/struct.go +++ b/sshkeys/struct.go @@ -4,9 +4,8 @@ package sshkeys type EncryptedSSHKeyV1 struct { SSHKeyV1 CipherName string - KDFName string KDFOpts SSHKDFOpts - Passphrase string + Passphrase []byte } // SSHKDFOpts contains a set of KDF options. @@ -20,19 +19,26 @@ type SSHKDFOpts struct { // Patch your shit. type SSHKeyV1 struct { Magic string + BitSize uint32 + DefKeyType string + KDFName string + KeySize uint32 + BlockSize uint32 PublicKeys []SSHPubKey PrivateKeys []SSHPrivKey } // SSHPubKey contains the Public key of an SSH Keypair. type SSHPubKey struct { - KeyType string PrivateKey *SSHPrivKey + KeyType string + Key []byte } // SSHPrivKey contains the Private key of an SSH Keypair. type SSHPrivKey struct { PublicKey *SSHPubKey - Checksum uint32 + Key []byte + Checksum []byte Comment string }