From 86266685f5fa3b620f1b2466b96807b6756a6a3b Mon Sep 17 00:00:00 2001 From: brent s Date: Fri, 18 Sep 2020 04:04:39 -0400 Subject: [PATCH] checking in for the night. key generation should be done, need to finish packing/formatting. also need to start on moduli generation. --- TODO | 3 +- moduli/const.go | 1 + moduli/func.go | 1 + moduli/main.go | 5 ++ moduli/ref/sources | 6 ++ moduli/struct.go | 1 + sshkeys/const.go | 11 ++- sshkeys/ed25519.go | 24 ++++++ sshkeys/func.go | 104 ++++++++++++++++++++---- sshkeys/interface.go | 6 -- sshkeys/ref/encrypted/parse_poc_rsa.go | 106 ++++++++++++++++++++----- sshkeys/ref/sources | 19 ++++- sshkeys/rsa.go | 23 ++++++ sshkeys/struct.go | 46 +++++++---- 14 files changed, 297 insertions(+), 59 deletions(-) create mode 100644 moduli/const.go create mode 100644 moduli/func.go create mode 100644 moduli/main.go create mode 100644 moduli/ref/sources create mode 100644 moduli/struct.go create mode 100644 sshkeys/ed25519.go delete mode 100644 sshkeys/interface.go create mode 100644 sshkeys/rsa.go diff --git a/TODO b/TODO index df7bafd..be412e8 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,4 @@ --sshkeys (need to figure out generation process) +-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 \ No newline at end of file diff --git a/moduli/const.go b/moduli/const.go new file mode 100644 index 0000000..336688e --- /dev/null +++ b/moduli/const.go @@ -0,0 +1 @@ +package moduli diff --git a/moduli/func.go b/moduli/func.go new file mode 100644 index 0000000..336688e --- /dev/null +++ b/moduli/func.go @@ -0,0 +1 @@ +package moduli diff --git a/moduli/main.go b/moduli/main.go new file mode 100644 index 0000000..5fd09e6 --- /dev/null +++ b/moduli/main.go @@ -0,0 +1,5 @@ +package moduli + +func main() { + +} diff --git a/moduli/ref/sources b/moduli/ref/sources new file mode 100644 index 0000000..0012195 --- /dev/null +++ b/moduli/ref/sources @@ -0,0 +1,6 @@ +https://man7.org/linux/man-pages/man5/moduli.5.html +https://security.stackexchange.com/questions/41941/consequences-of-tampered-etc-ssh-moduli +https://access.redhat.com/blogs/766093/posts/2177481 +https://security.stackexchange.com/a/113058 +https://github.com/Luzifer/go-dhparam +https://github.com/mimoo/test_DHparams \ No newline at end of file diff --git a/moduli/struct.go b/moduli/struct.go new file mode 100644 index 0000000..336688e --- /dev/null +++ b/moduli/struct.go @@ -0,0 +1 @@ +package moduli diff --git a/sshkeys/const.go b/sshkeys/const.go index 13c15cb..77476c9 100644 --- a/sshkeys/const.go +++ b/sshkeys/const.go @@ -17,6 +17,10 @@ const ( defRounds uint32 = 100 defRSABitSize uint32 = 4096 defSaltLen int = 16 + // bcrypt_pbkdf maxes out at 32 for private key gen (sk is actually 64; sk+pk) + // But per OpenSSH code, we pass a key len of kdfKeyLen + len(salt) + kdfKeyLen int = 32 + kdfSplit int = 32 ) // Cipher names. I believe only AES256-CTR is supported upstream currently. @@ -51,7 +55,10 @@ const ( // Key/Block sizes. const ( - keyEd25519 uint32 = 32 + keyEd25519 uint32 = 32 + // Is this correct? Based on PROTOCOL.key's "padlen % 255", it seems to be. + blockPad uint32 = 255 blockEd25519 uint32 = 16 - blockNull uint32 = 8 + // Blocksize for RSA depends on key bits, I think. + blockNull uint32 = 8 ) diff --git a/sshkeys/ed25519.go b/sshkeys/ed25519.go new file mode 100644 index 0000000..ab7c939 --- /dev/null +++ b/sshkeys/ed25519.go @@ -0,0 +1,24 @@ +package sshkeys + +import ( + "crypto/ed25519" +) + +func (k *SSHPrivKey) generateEd25519() error { + if k.Key != nil || k.PublicKey.Key != nil { + return nil // Just no-op; already generated. + } + // We cast "pk" (public key) to _ because it's an interface{} that we can't seem to + // assert to []byte. I've tried iterating, a type assertion, initializing,... nada. + // Luckily it's at a fixed half of the secret key. + if _, s, err := ed25519.GenerateKey(nil); err != nil { + return err + } else { + k.Key = s + ks := len(s) - ed25519.PublicKeySize + k.PublicKey.Key = s[ks:] + k.BitSize = ed25519Len + k.PublicKey.KeyType = KeyEd25519 + } + return nil +} diff --git a/sshkeys/func.go b/sshkeys/func.go index cfb1ec2..50bd5a2 100644 --- a/sshkeys/func.go +++ b/sshkeys/func.go @@ -1,6 +1,9 @@ package sshkeys import ( + "bytes" + "crypto/aes" + "crypto/cipher" "crypto/rand" "errors" "fmt" @@ -52,7 +55,7 @@ func (k *EncryptedSSHKeyV1) Generate(force bool) error { if err := k.validate(); err != nil { return err } - if k.PrivateKeys != nil && !force { + if len(k.Keys) > 0 && !force { return nil // Already generated. } if k.KDFOpts.Salt == nil { @@ -64,10 +67,7 @@ func (k *EncryptedSSHKeyV1) Generate(force bool) error { 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 + if k.DefKeyType == KeyEd25519 { k.KeySize = keyEd25519 k.BlockSize = blockEd25519 } @@ -80,25 +80,95 @@ func (k *EncryptedSSHKeyV1) Generate(force bool) error { 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 { + switch k.DefKeyType { + case KeyRsa: + if err := pk.generateRsa(); err != nil { return err } + case KeyEd25519: + if err := pk.generateEd25519(); err != nil { + return err + } + default: + 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. + // 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)); err != nil { + return err + } else { + k.Crypt.PrivateKey = k.Crypt.CryptKey[0:kdfSplit] + k.Crypt.CryptSalt = k.Crypt.CryptKey[kdfSplit:] + } + default: + return errors.New("could not find KDF") + } + switch k.CipherName { + case CipherAes256Ctr: + if k.Crypt.Cipher, err = aes.NewCipher(k.Crypt.PrivateKey); err != nil { + return err + } else { + k.Crypt.Stream = cipher.NewCTR(k.Crypt.Cipher, k.Crypt.CryptSalt) + // Can then be used as k.Crypt.Stream.XORKeyStream(dst []byte, src []byte) + } + default: + return errors.New("could not find Cipher") + } + k.build() return nil } 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. + var validKT bool + for _, v := range allowed_keytypes { + if v == k.DefKeyType { + validKT = true + } + } + if !validKT { + return errors.New("invalid DefKeyType specified") } return nil } + +func (k *SSHKeyV1) Generate(force bool) error { + if len(k.Keys) > 0 && !force { + return nil // Already generated. + } + if k.DefKeyType == KeyEd25519 { + 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 + } + switch k.DefKeyType { + case KeyRsa: + if err := pk.generateRsa(); err != nil { + return err + } + case KeyEd25519: + if err := pk.generateEd25519(); err != nil { + return err + } + default: + return errors.New("unknown key type; could not generate private/public keypair") + } + k.Keys = append(k.Keys, pk) + k.build() + return nil +} + +func (k *SSHKeyV1) build() { + // We actually assemble the key buffer here. Translation to bytes where needed, case switches (ED25519 vs. RSA), etc. +} diff --git a/sshkeys/interface.go b/sshkeys/interface.go deleted file mode 100644 index 204400f..0000000 --- a/sshkeys/interface.go +++ /dev/null @@ -1,6 +0,0 @@ -package sshkeys - -type OpenSSHKeypair interface { - GeneratePrivate(force bool) error - GeneratePublic(force bool) error -} diff --git a/sshkeys/ref/encrypted/parse_poc_rsa.go b/sshkeys/ref/encrypted/parse_poc_rsa.go index dd4eb4c..d9308d8 100644 --- a/sshkeys/ref/encrypted/parse_poc_rsa.go +++ b/sshkeys/ref/encrypted/parse_poc_rsa.go @@ -1,10 +1,13 @@ package main import ( - `crypto/cipher` - `crypto/rsa` - `encoding/hex` - `fmt` + "crypto/aes" + "crypto/cipher" + "crypto/rsa" + "encoding/hex" + "fmt" + + "github.com/dchest/bcrypt_pbkdf" ) // ssh-keygen -f /tmp/tmp2xzvpjhn -q -o -t rsa -b 4096 -C "This is a comment string" -N test -a 100 @@ -379,24 +382,38 @@ bc2a63a20ebb309cc6f3e65db301a058b8dace07e71b38f3f3595433f69b198f func main() { const ( - passphrase string = "test" - rounds int = 100 - keySize int = 4096 - lenPlain int = 160 - publicExponent int = 65537 + passphrase string = "test" + saltLen int = 16 // 3.0.0.0 + rounds int = 100 // 3.0.0.1 + keySize int = 4096 + crtLen int = 256 // 4.0.1.4 + dLen int = 512 // 4.0.1.3 + e int = 65537 // 4.0.0.1 + nLen int = 513 // 4.0.0.2 + pLen int = 257 // 4.0.1.5 + qLen int = 257 // 4.0.1.6 + dataLen int = 1872 // 4.0.1 ) var salt []byte var bcryptKey []byte - var sk []byte - var pk []byte - var pubkey rsa.PublicKey + var crt []byte + var d []byte + var n []byte + var p []byte + var q []byte var key rsa.PrivateKey var decrypted []byte var aesCtx cipher.Block var encData []byte - decrypted = make([]byte, lenPlain) - encData = make([]byte, lenPlain) + crt = make([]byte, crtLen) + d = make([]byte, dLen) + n = make([]byte, nLen) + p = make([]byte, pLen) + q = make([]byte, qLen) + decrypted = make([]byte, dataLen) + encData = make([]byte, dataLen) + salt = make([]byte, saltLen) // Import salt if s, err := hex.DecodeString("07d4b07c0b128348916488008d6e130b"); err != nil { @@ -474,11 +491,64 @@ func main() { encData = b } - // RSA keys - // This is used to validate decrypted keys. - if rk, err := hex.DecodeString(""); err != nil { + // Bcrypt_pbkdf derivation (used for deriving decryption key for AES encrypted private key) + if k, err := bcrypt_pbkdf.Key([]byte(passphrase), salt, rounds, 32+16); err != nil { fmt.Println(err) + return } else { - key = rk + bcryptKey = k } + realBcryptKey := bcryptKey[0:32] + realIV := bcryptKey[32:] + + // N + if b, err := hex.DecodeString( + "00b7cec04601ce2a12f0c924cb9a30eb990066812cb14369193f30b2b9fdd4af" + + "cb300c918f2a77d64410f3617ae7c8ca318c257d3c4df4e2c4108bbbe93a8689" + + "4ba14b3575f2f72150bc381dcbfb742c7a196866fd3184ace96761adda0fc299" + + "2f6c866d7569919fc22d9c4bf0de405a8c76d519aa2a5329dc6825777229a5d0" + + "b753a7825a89b95275f9c025e215343c6c88cd6690a221f8ae9ef675ee464dc7" + + "d118da410507ea5d6b6489dd60afd8a6646492db3e279f1a78240db8abbda6c5" + + "0714c9636650a72081e7fa5d472c1428b07eae5d15b64ea1e2a7508512fe9ab6" + + "55f86a313486d3cca1dd8e90acc5c9fba4d6e767507fbab9f3a7f68c748142af" + + "2a3701d31a8a9b7511958aa77187ba702ed934d385afcee42380e95e0e7e9bc0" + + "f4d23367fc770374167b7f0926fb6fdb6d05aad1cfd191824845b014e18153bf" + + "0d1d3c3b1fadbb25a3f1d151f9b684633d8c1690fcd8cad05aac2aeb23dbf19a" + + "37e480a008910319c116d47bd924b39942543b88a0f6127952b2d8e1290f3029" + + "f542aebe9c0c8e36cf3296865cd6643c8924d566ebf4971809399a1ac096fe1e" + + "dc3b5f871bf5ef0b4d44e0ea27620d205142e0bfcf677b4db025532121a3f074" + + "5aa4d0586331733257855a5cecbe3ac4403d04ff0cc0c58b7c04904b402125c2" + + "bc2a63a20ebb309cc6f3e65db301a058b8dace07e71b38f3f3595433f69b198f" + + "07", + ); err != nil { + fmt.Println(err) + return + } else { + n = b + } + + // Decrypter + if a, err := aes.NewCipher(realBcryptKey); err != nil { + fmt.Println(err) + return + } else { + aesCtx = a + } + + // Actual cipher setup. AES256-CTR + decryptor := cipher.NewCTR(aesCtx, realIV) + decryptor.XORKeyStream(decrypted, encData) + + // Print comparisons + fmt.Printf("Salt: %v\n", hex.EncodeToString(salt)) + fmt.Printf("Bcrypt key: %v\n", hex.EncodeToString(bcryptKey)) + fmt.Printf("CRT: %v\n", hex.EncodeToString(crt)) + fmt.Printf("d: %v\n", hex.EncodeToString(d)) + fmt.Printf("n: %v\n", hex.EncodeToString(n)) + fmt.Printf("p: %v\n", hex.EncodeToString(p)) + fmt.Printf("q: %v\n", hex.EncodeToString(q)) + fmt.Printf("key: %v\n", key) + // var aesCtx cipher.Block + fmt.Printf("encData: %v\n", hex.EncodeToString(encData)) + fmt.Printf("Decrypted?: %v\n", hex.EncodeToString(decrypted)) } diff --git a/sshkeys/ref/sources b/sshkeys/ref/sources index 0539b1a..bef4aac 100644 --- a/sshkeys/ref/sources +++ b/sshkeys/ref/sources @@ -1,10 +1,12 @@ -https://peterlyons.com/problog/2017/12/openssh-ed25519-private-key-file-format/ https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key canonical: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD +https://peterlyons.com/problog/2017/12/openssh-ed25519-private-key-file-format/ 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://coolaj86.com/articles/openssh-vs-openssl-key-formats/ +https://coolaj86.com/articles/ssh-pubilc-key-fingerprints/ 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.)" @@ -12,6 +14,21 @@ http://www.tedunangst.com/flak/post/bcrypt-pbkdf https://xorhash.gitlab.io/xhblog/0010.html https://blog.rebased.pl/2020/02/10/ssh-key-internals.html https://blog.rebased.pl/2020/03/24/basic-key-security.html +https://github.com/pwnedkeys/openssl-additions/blob/master/lib/openssl/ssh_pkey.rb +https://stackoverflow.com/a/25181584/733214 +https://crypto.stackexchange.com/a/68732 + +RSA: +d: 512 +n: 512 +p: 256 +q: 256 +QInv: 256 +d: 4.0.1.3.0 +n: 4.0.0.2.0, 4.0.1.2.1.0 (prefix nullbyte) +p: 4.0.1.5.0 (prefix nullbyte) +q: 4.0.1.6.0 (prefix nullbyte) +QInv: 4.0.1.4.0 ## UPSTREAM https://github.com/openssh/openssh-portable/blob/master/sshkey.c diff --git a/sshkeys/rsa.go b/sshkeys/rsa.go new file mode 100644 index 0000000..ffeb875 --- /dev/null +++ b/sshkeys/rsa.go @@ -0,0 +1,23 @@ +package sshkeys + +import ( + "crypto/rand" + "crypto/rsa" +) + +func (k *SSHPrivKey) generateRsa() error { + if k.BitSize == 0 { + k.BitSize = defRSABitSize + } + if k.Key != nil || k.PublicKey.Key != nil { + return nil // A no-op; key already exists. + } + if sk, err := rsa.GenerateKey(rand.Reader, int(k.BitSize)); err != nil { + return err + } else { + k.Key = sk // See https://golang.org/pkg/crypto/rsa/#PrivateKey + k.PublicKey.KeyType = KeyRsa + k.PublicKey.Key = k.Key.PublicKey + } + return nil +} diff --git a/sshkeys/struct.go b/sshkeys/struct.go index 3d0752f..eabb570 100644 --- a/sshkeys/struct.go +++ b/sshkeys/struct.go @@ -1,13 +1,29 @@ package sshkeys +import ( + "bytes" + "crypto/cipher" +) + // EncryptedSSHKeyV1 represents an encrypted private key. type EncryptedSSHKeyV1 struct { SSHKeyV1 CipherName string + Crypt SSHCrypt KDFOpts SSHKDFOpts Passphrase []byte } +// SSHEncryptionKey contains the PublicKey and PrivateKey bytes (as derived by KDF, different from the actual SSH keypair), +// the Cipher, and the stream. +type SSHCrypt struct { + Stream cipher.Stream + Cipher cipher.Block + CryptSalt []byte + PrivateKey []byte + CryptKey []byte +} + // SSHKDFOpts contains a set of KDF options. type SSHKDFOpts struct { Salt []byte // Also referred to as IV (initialization vector). (https://en.wikipedia.org/wiki/Initialization_vector) @@ -18,27 +34,29 @@ type SSHKDFOpts struct { // We don't bother with the legacy (pre v1) keys. Sorry not sorry. // Patch your shit. type SSHKeyV1 struct { - Magic string - BitSize uint32 - DefKeyType string - KDFName string - KeySize uint32 - BlockSize uint32 - PublicKeys []SSHPubKey - PrivateKeys []SSHPrivKey + Magic string + DefKeyType string + KDFName string + KeySize uint32 + BlockSize uint32 + Keys []SSHPrivKey // 1 by default. + Buffer bytes.Buffer } // SSHPubKey contains the Public key of an SSH Keypair. type SSHPubKey struct { - PrivateKey *SSHPrivKey - KeyType string - Key []byte + KeyType string + Key interface{} } // SSHPrivKey contains the Private key of an SSH Keypair. type SSHPrivKey struct { PublicKey *SSHPubKey - Key []byte - Checksum []byte - Comment string + BitSize uint32 + Key interface{} + // ED25519 keys are actually "sk + pk", where sk is the secret key and pk is the pubkey. + // We store that here. + KeyAlt []byte + Checksum []byte + Comment string }