From f82c1336acc0eaf3c38d17b9ddb11d52b949d941 Mon Sep 17 00:00:00 2001 From: Sudhi Herle Date: Tue, 5 Nov 2019 21:42:25 +0100 Subject: [PATCH] sigtool now supports openssh ed25519 public and private keys. * Added support to read openssh public keys and encrypted private keys * reworked private key handling * made password the default; generating keys without password requires explicit "--no-password" --- README.md | 29 +++- crypt.go | 113 +++++++++++---- go.mod | 1 + go.sum | 2 + sign/sign.go | 60 +++++--- sign/ssh.go | 389 +++++++++++++++++++++++++++++++++++++++++++++++++++ sigtool.go | 63 +++++---- version | 2 +- 8 files changed, 581 insertions(+), 78 deletions(-) create mode 100644 sign/ssh.go diff --git a/README.md b/README.md index 51c4b35..9c1f7f9 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ during the encryption process - this has the benefit of also authenticating the sender (and the receiver can verify the sender if they possess the corresponding public key). +The sign, verify, encrypt, decrypt operations can use OpenSSH Ed25519 keys +*or* the keys generated by sigtool. This means, you can send encrypted +files to any recipient identified by their comment in `~/.ssh/authorized_keys`. ## How do I build it? With Go 1.5 and later: @@ -61,15 +64,15 @@ e.g., to sign `archive.tar.gz` with private key `/tmp/testkey.key`: sigtool sign /tmp/testkey.key archive.tar.gz -If *testkey.key* was encrypted with a user pass phrase: +If *testkey.key* was encrypted without a user pass phrase: - sigtool sign -p /tmp/testkey.key archive.tar.gz + sigtool sign --no-password /tmp/testkey.key archive.tar.gz The signature can also be written directly to a user supplied output file. - sigtool sign -p -o archive.sig /tmp/testkey.key archive.tar.gz + sigtool sign -o archive.sig /tmp/testkey.key archive.tar.gz ### Verify a signature against a file @@ -85,6 +88,10 @@ e.g., to verify the signature of *archive.tar.gz* against sigtool verify /tmp/testkey.pub archive.sig archive.tar.gz + +Note that signing and verifying can also work with OpenSSH ed25519 +keys. + ### Encrypt a file by authenticating the sender If the sender wishes to prove to the recipient that they encrypted a file: @@ -115,6 +122,20 @@ the receiver doesn't need to authenticate the sender: This will create an encrypted file *archive.tar.gz.enc* such that the recipient in possession of *to.key* can decrypt it. +### Encrypt a file to an OpenSSH recipient *without* authenticating the sender +Suppose you want to send an encrypted file where the recipient's +public key is in `~/.ssh/authorized_keys`. Such a recipient is identified +by their OpenSSH key comment (typically `name@domain`): + + sigtool encrypt user@domain -o archive.tar.gz.enc archive.tar.gz + +If you have their public key in file "name-domain.pub", you can do: + + sigtool encrypt name-domain.pub -o archive.tar.gz.enc archive.tar.gz + +This will create an encrypted file *archive.tar.gz.enc* such that the +recipient can decrypt using their private key. + ## Technical Details ### How is the private key protected? @@ -144,7 +165,7 @@ using HKDF to generate a key-encryption-key. The file-encryption key is AEAD encrypted with this key-encryption-key. Thus, each recipient has their own individual encrypted key blob. -The Ed25519 keys generated by `sigtool` are transformed to their +The Ed25519 keys generated by `sigtool` are transformed to their corresponding Curve25519 points in order to generate the shared secret. This elliptic co-ordinate transform follows [FiloSottile's writeup][2]. diff --git a/crypt.go b/crypt.go index 236d2dd..50199ac 100644 --- a/crypt.go +++ b/crypt.go @@ -16,7 +16,9 @@ package main import ( "fmt" "io" + "io/ioutil" "os" + "strings" "github.com/opencoff/go-utils" flag "github.com/opencoff/pflag" @@ -34,12 +36,12 @@ func encrypt(args []string) { var outfile string var keyfile string var envpw string - var pw bool + var nopw bool var sblksize string fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`") fs.StringVarP(&keyfile, "sign", "s", "", "Sign using private key `S`") - fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to decrypt the private key") + fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for passphrase to decrypt the private key") fs.StringVarP(&envpw, "env-password", "", "", "Use passphrase from environment variable `E`") fs.StringVarP(&sblksize, "block-size", "B", "4M", "Use `S` as the encryption block size") @@ -55,19 +57,23 @@ func encrypt(args []string) { var pws, infile string - if len(envpw) > 0 { - pws = os.Getenv(envpw) - } else if pw { - pws, err = utils.Askpass("Enter passphrase for private key", false) - if err != nil { - die("%s", err) - } - } - var sk *sign.PrivateKey if len(keyfile) > 0 { - sk, err = sign.ReadPrivateKey(keyfile, pws) + sk, err = sign.ReadPrivateKey(keyfile, func() ([]byte, error) { + if nopw { + return nil, nil + } + if len(envpw) > 0 { + pws = os.Getenv(envpw) + } else { + pws, err = utils.Askpass("Enter passphrase for private key", false) + if err != nil { + die("%s", err) + } + } + return []byte(pws), nil + }) if err != nil { die("%s", err) } @@ -75,7 +81,7 @@ func encrypt(args []string) { args = fs.Args() if len(args) < 2 { - die("Insufficient args. Try '%s --help'") + die("Insufficient args. Try '%s --help'", os.Args[0]) } var infd io.Reader = os.Stdin @@ -92,6 +98,27 @@ func encrypt(args []string) { } } + // Lets try to read the authorized files + home, err := os.UserHomeDir() + if err != nil { + die("can't find homedir for this user") + } + + authkeys := fmt.Sprintf("%s/.ssh/authorized_keys", home) + authdata, err := ioutil.ReadFile(authkeys) + if err != nil { + if err != os.ErrNotExist { + die("can't open %s: %s", authkeys, err) + } + } + + pka, err := sign.ParseAuthorizedKeys(authdata) + keymap := make(map[string]*sign.PublicKey) + + for _, pk := range pka { + keymap[pk.Comment] = pk + } + if len(outfile) > 0 && outfile != "-" { if inf != nil { ost, err := os.Stat(outfile) @@ -120,11 +147,27 @@ func encrypt(args []string) { die("%s", err) } + errs := 0 for i := 0; i < len(args)-1; i++ { + var err error + var pk *sign.PublicKey + fn := args[i] - pk, err := sign.ReadPublicKey(fn) - if err != nil { - die("%s", err) + if strings.Index(fn, "@") > 0 { + var ok bool + pk, ok = keymap[fn] + if !ok { + warn("can't find user %s in %s", fn, authkeys) + errs += 1 + continue + } + } else { + pk, err = sign.ReadPublicKey(fn) + if err != nil { + warn("%s", err) + errs += 1 + continue + } } err = en.AddRecipient(pk) @@ -133,6 +176,10 @@ func encrypt(args []string) { } } + if errs > 0 { + die("Too many errors!") + } + err = en.Encrypt(infd, outfd) if err != nil { die("%s", err) @@ -149,10 +196,10 @@ func decrypt(args []string) { var envpw string var outfile string var pubkey string - var pw bool + var nopw bool fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`") - fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to decrypt the private key") + fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for passphrase to decrypt the private key") fs.StringVarP(&envpw, "env-password", "", "", "Use passphrase from environment variable `E`") fs.StringVarP(&pubkey, "verify-sender", "v", "", "Verify that the sender matches public key in `F`") @@ -163,25 +210,31 @@ func decrypt(args []string) { args = fs.Args() if len(args) < 1 { - die("Insufficient args. Try '%s --help'") + die("Insufficient args. Try '%s --help'", os.Args[0]) } var infd io.Reader = os.Stdin var outfd io.Writer = os.Stdout var inf *os.File - var pws, infile string - - if len(envpw) > 0 { - pws = os.Getenv(envpw) - } else if pw { - pws, err = utils.Askpass("Enter passphrase for private key", false) - if err != nil { - die("%s", err) - } - } + var infile string keyfile := args[0] - sk, err := sign.ReadPrivateKey(keyfile, pws) + sk, err := sign.ReadPrivateKey(keyfile, func() ([]byte, error) { + var pws string + if nopw { + return nil, nil + } + + if len(envpw) > 0 { + pws = os.Getenv(envpw) + } else { + pws, err = utils.Askpass("Enter passphrase for private key", false) + if err != nil { + die("%s", err) + } + } + return []byte(pws), nil + }) if err != nil { die("%s", err) } diff --git a/go.mod b/go.mod index 39cbc19..2a9c5fb 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/opencoff/sigtool go 1.13 require ( + github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a github.com/gogo/protobuf v1.3.1 github.com/opencoff/go-utils v0.4.0 github.com/opencoff/pflag v0.3.3 diff --git a/go.sum b/go.sum index aff4d00..43f8b2d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +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/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= diff --git a/sign/sign.go b/sign/sign.go index ad70313..6169f73 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -20,6 +20,7 @@ package sign import ( + "bytes" "crypto" "crypto/rand" "crypto/sha256" @@ -55,6 +56,9 @@ type PrivateKey struct { type PublicKey struct { Pk []byte + // Comment string + Comment string + // Curve25519 point corresponding to this Ed25519 key ck []byte @@ -163,7 +167,7 @@ func NewKeypair() (*Keypair, error) { // goes in $bn.key. // If password is non-empty, then the private key is encrypted // before writing to disk. -func (kp *Keypair) Serialize(bn, comment string, pw string) error { +func (kp *Keypair) Serialize(bn, comment string, getpw func() ([]byte, error)) error { sk := &kp.Sec pk := &kp.Pub @@ -176,7 +180,7 @@ func (kp *Keypair) Serialize(bn, comment string, pw string) error { return fmt.Errorf("Can't serialize to %s: %s", pkf, err) } - err = sk.serialize(skf, comment, pw) + err = sk.serialize(skf, comment, getpw) if err != nil { return fmt.Errorf("Can't serialize to %s: %s", pkf, err) } @@ -186,18 +190,25 @@ func (kp *Keypair) Serialize(bn, comment string, pw string) error { // Read the private key in 'fn', optionally decrypting it using // password 'pw' and create new instance of PrivateKey -func ReadPrivateKey(fn string, pw string) (*PrivateKey, error) { +func ReadPrivateKey(fn string, getpw func() ([]byte, error)) (*PrivateKey, error) { yml, err := ioutil.ReadFile(fn) if err != nil { return nil, err } - return MakePrivateKey(yml, pw) + if bytes.Index(yml, []byte("OPENSSH PRIVATE KEY-")) > 0 { + return parseSSHPrivateKey(yml, getpw) + } + + if pw, err := getpw(); err == nil { + return MakePrivateKey(yml, pw) + } + return nil, err } // Make a private key from bytes 'yml' and password 'pw'. The bytes // are assumed to be serialized version of the private key. -func MakePrivateKey(yml []byte, pw string) (*PrivateKey, error) { +func MakePrivateKey(yml []byte, pw []byte) (*PrivateKey, error) { var ssk serializedPrivKey err := yaml.Unmarshal(yml, &ssk) @@ -224,7 +235,7 @@ func MakePrivateKey(yml []byte, pw string) (*PrivateKey, error) { } // We take short passwords and extend them - pwb := sha512.Sum512([]byte(pw)) + pwb := sha512.Sum512(pw) xork, err := scrypt.Key(pwb[:], esk.Salt, int(esk.N), int(esk.r), int(esk.p), len(esk.Esk)) if err != nil { @@ -250,11 +261,14 @@ func MakePrivateKey(yml []byte, pw string) (*PrivateKey, error) { } // Make a private key from 64-bytes of extended Ed25519 key -func PrivateKeyFromBytes(skb []byte) (*PrivateKey, error) { - if len(skb) != 64 { - return nil, fmt.Errorf("private key is malformed (len %d!)", len(skb)) +func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) { + if len(buf) != 64 { + return nil, fmt.Errorf("private key is malformed (len %d!)", len(buf)) } + skb := make([]byte, 64) + copy(skb, buf) + edsk := Ed.PrivateKey(skb) edpk := edsk.Public().(Ed.PublicKey) @@ -283,16 +297,19 @@ func (pk *PublicKey) Hash() []byte { // Serialize the private key to a file // Format: YAML // All []byte are in base64 (RawEncoding) -func (sk *PrivateKey) serialize(fn, comment string, pw string) error { +func (sk *PrivateKey) serialize(fn, comment string, getpw func() ([]byte, error)) error { + + pw, err := getpw() + if err != nil { + return err + } b64 := base64.StdEncoding.EncodeToString esk := &encPrivKey{} ssk := &serializedPrivKey{Comment: comment} - // Even with an empty password, we still encrypt and store. - // expand the password into 64 bytes - pwb := sha512.Sum512([]byte(pw)) + pwb := sha512.Sum512(pw) esk.N = _N esk.r = _r @@ -455,7 +472,12 @@ func ReadPublicKey(fn string) (*PublicKey, error) { return nil, err } - return MakePublicKey(yml) + // first try to parse as a ssh key + pk, err := parseSSHPublicKey(yml) + if err != nil { + pk, err = MakePublicKey(yml) + } + return pk, err } // Parse a serialized public in 'yml' and return the resulting @@ -469,13 +491,17 @@ func MakePublicKey(yml []byte) (*PublicKey, error) { } b64 := base64.StdEncoding.DecodeString - var pk []byte + var pkb []byte - if pk, err = b64(spk.Pk); err != nil { + if pkb, err = b64(spk.Pk); err != nil { return nil, fmt.Errorf("can't decode YAML:Pk: %s", err) } - return PublicKeyFromBytes(pk) + if pk, err := PublicKeyFromBytes(pkb); err == nil { + pk.Comment = spk.Comment + return pk, nil + } + return nil, err } // Make a public key from a byte string diff --git a/sign/ssh.go b/sign/ssh.go new file mode 100644 index 0000000..132094c --- /dev/null +++ b/sign/ssh.go @@ -0,0 +1,389 @@ +// ssh.go - support for reading ssh private and public keys +// +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file is a bastardization of github.com/ScaleFT/sshkeys and +// golang.org/x/crypto/ssh/keys.go +// +// It is licensed under the terms of the original go source code +// OR the Apache 2.0 license (terms of sshkeys). +// +// Changes from that version: +// - don't use password but call a func() to get the password as needed +// - narrowly scope the key support for ONLY ed25519 keys +// - support reading multiple public keys from authorized_keys + +package sign + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "encoding/pem" + "errors" + "fmt" + "strings" + + "github.com/dchest/bcrypt_pbkdf" + "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/ssh" +) + +var ( + ErrIncorrectPassword = errors.New("ssh: Invalid Passphrase") + ErrNoPEMFound = errors.New("no PEM block found") + ErrBadPublicKey = errors.New("ssh: malformed public key") + ErrKeyTooShort = errors.New("ssh: public key too short") + ErrBadTrailers = errors.New("ssh: trailing junk in public key") + ErrBadFormat = errors.New("ssh: invalid openssh private key format") + ErrBadLength = errors.New("ssh: private key unexpected length") + ErrBadPadding = errors.New("ssh: padding not as expected") +) + +const keySizeAES256 = 32 + +// ParseEncryptedRawPrivateKey returns a private key from an +// encrypted ed25519 private key. +func parseSSHPrivateKey(data []byte, getpw func() ([]byte, error)) (*PrivateKey, error) { + block, _ := pem.Decode(data) + if block == nil { + return nil, ErrNoPEMFound + } + + if x509.IsEncryptedPEMBlock(block) { + return nil, fmt.Errorf("ssh: no support for legacy PEM encrypted keys") + } + + switch block.Type { + case "OPENSSH PRIVATE KEY": + return parseOpenSSHPrivateKey(block.Bytes, getpw) + default: + return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type) + } +} + +func parseSSHPublicKey(in []byte) (*PublicKey, error) { + v := bytes.Split(in, []byte(" \t")) + if len(v) != 3 { + return nil, ErrBadPublicKey + } + + return parseEncPubKey(v[1], string(v[2])) +} + +// parse a wire encoded public key +func parseEncPubKey(in []byte, comm string) (*PublicKey, error) { + in, err := base64.StdEncoding.DecodeString(string(in)) + if err != nil { + return nil, err + } + + algo, in, ok := parseString(in) + if !ok { + return nil, ErrKeyTooShort + + } + if string(algo) != ssh.KeyAlgoED25519 { + return nil, nil + } + + var w struct { + KeyBytes []byte + Rest []byte `ssh:"rest"` + } + + if err := ssh.Unmarshal(in, &w); err != nil { + return nil, err + } + + if len(w.Rest) > 0 { + return nil, ErrBadTrailers + } + + pk, err := PublicKeyFromBytes(w.KeyBytes) + if err == nil { + pk.Comment = strings.TrimSpace(comm) + } + return pk, err +} + +func parseString(in []byte) (out, rest []byte, ok bool) { + if len(in) < 4 { + return + } + length := binary.BigEndian.Uint32(in) + in = in[4:] + if uint32(len(in)) < length { + return + } + out = in[:length] + rest = in[length:] + ok = true + return +} + +// parseAuthorizedKey parses a public key in OpenSSH binary format and decodes it. +// removed. +func parseAuthorizedKey(in []byte) (*PublicKey, error) { + in = bytes.TrimSpace(in) + i := bytes.IndexAny(in, " \t") + if i == -1 { + i = len(in) + } + + pk, err := parseEncPubKey(in[:i], string(in[i:])) + if err != nil { + return nil, err + } + + return pk, nil +} + +// ParseAuthorizedKeys parses a public key from an authorized_keys +// file used in OpenSSH according to the sshd(8) manual page. +func ParseAuthorizedKeys(in []byte) ([]*PublicKey, error) { + var pka []*PublicKey + var rest []byte + + for len(in) > 0 { + end := bytes.IndexByte(in, '\n') + if end != -1 { + rest = in[end+1:] + in = in[:end] + } else { + rest = nil + } + + end = bytes.IndexByte(in, '\r') + if end != -1 { + in = in[:end] + } + + in = bytes.TrimSpace(in) + if len(in) == 0 || in[0] == '#' { + in = rest + continue + } + + i := bytes.IndexAny(in, " \t") + if i == -1 { + in = rest + continue + } + + if pk, err := parseAuthorizedKey(in[i:]); err == nil { + if pk != nil { + pka = append(pka, pk) + } + in = rest + continue + } + + // No key type recognised. Maybe there's an options field at + // the beginning. + var b byte + inQuote := false + var candidateOptions []string + optionStart := 0 + for i, b = range in { + isEnd := !inQuote && (b == ' ' || b == '\t') + if (b == ',' && !inQuote) || isEnd { + if i-optionStart > 0 { + candidateOptions = append(candidateOptions, string(in[optionStart:i])) + } + optionStart = i + 1 + } + if isEnd { + break + } + if b == '"' && (i == 0 || (i > 0 && in[i-1] != '\\')) { + inQuote = !inQuote + } + } + for i < len(in) && (in[i] == ' ' || in[i] == '\t') { + i++ + } + if i == len(in) { + // Invalid line: unmatched quote + in = rest + continue + } + + in = in[i:] + i = bytes.IndexAny(in, " \t") + if i == -1 { + in = rest + continue + } + + if pk, err := parseAuthorizedKey(in[i:]); err == nil { + if pk != nil { + pka = append(pka, pk) + } + } + + in = rest + continue + } + + return pka, nil +} + +const opensshv1Magic = "openssh-key-v1" + +type opensshHeader struct { + CipherName string + KdfName string + KdfOpts string + NumKeys uint32 + PubKey string + PrivKeyBlock string +} + +type opensshKey struct { + Check1 uint32 + Check2 uint32 + Keytype string + Rest []byte `ssh:"rest"` +} + +type opensshED25519 struct { + Pub []byte + Priv []byte + Comment string + Pad []byte `ssh:"rest"` +} + +func parseOpenSSHPrivateKey(data []byte, getpw func() ([]byte, error)) (*PrivateKey, error) { + magic := append([]byte(opensshv1Magic), 0) + if !bytes.Equal(magic, data[0:len(magic)]) { + return nil, ErrBadFormat + } + remaining := data[len(magic):] + + w := opensshHeader{} + + if err := ssh.Unmarshal(remaining, &w); err != nil { + return nil, err + } + + if w.NumKeys != 1 { + return nil, fmt.Errorf("ssh: NumKeys must be 1: %d", w.NumKeys) + } + + var privateKeyBytes []byte + var encrypted bool + + switch { + // OpenSSH supports bcrypt KDF w/ AES256-CBC or AES256-CTR mode + case w.KdfName == "bcrypt" && w.CipherName == "aes256-cbc": + pw, err := getpw() + if err != nil { + return nil, err + } + iv, block, err := extractBcryptIvBlock(pw, &w) + if err != nil { + return nil, err + } + + cbc := cipher.NewCBCDecrypter(block, iv) + privateKeyBytes = []byte(w.PrivKeyBlock) + cbc.CryptBlocks(privateKeyBytes, privateKeyBytes) + + encrypted = true + + case w.KdfName == "bcrypt" && w.CipherName == "aes256-ctr": + pw, err := getpw() + if err != nil { + return nil, err + } + iv, block, err := extractBcryptIvBlock(pw, &w) + if err != nil { + return nil, err + } + + stream := cipher.NewCTR(block, iv) + privateKeyBytes = []byte(w.PrivKeyBlock) + stream.XORKeyStream(privateKeyBytes, privateKeyBytes) + + encrypted = true + + case w.KdfName == "none" && w.CipherName == "none": + privateKeyBytes = []byte(w.PrivKeyBlock) + + default: + return nil, fmt.Errorf("ssh: unknown Cipher/KDF: %s:%s", w.CipherName, w.KdfName) + } + + pk1 := opensshKey{} + + if err := ssh.Unmarshal(privateKeyBytes, &pk1); err != nil { + if encrypted { + return nil, ErrIncorrectPassword + } + return nil, err + } + + if pk1.Check1 != pk1.Check2 { + return nil, ErrIncorrectPassword + } + + // we only handle ed25519 and rsa keys currently + switch pk1.Keytype { + case ssh.KeyAlgoED25519: + key := opensshED25519{} + + err := ssh.Unmarshal(pk1.Rest, &key) + if err != nil { + return nil, err + } + + if len(key.Priv) != ed25519.PrivateKeySize { + return nil, ErrBadLength + } + + for i, b := range key.Pad { + if int(b) != i+1 { + return nil, ErrBadPadding + } + } + + pk, err := PrivateKeyFromBytes(key.Priv) + return pk, err + default: + return nil, fmt.Errorf("ssh: unhandled key type: %v", pk1.Keytype) + } +} + +func extractBcryptIvBlock(passphrase []byte, w *opensshHeader) ([]byte, cipher.Block, error) { + cipherKeylen := keySizeAES256 + cipherIvLen := aes.BlockSize + + var opts struct { + Salt []byte + Rounds uint32 + } + + if err := ssh.Unmarshal([]byte(w.KdfOpts), &opts); err != nil { + return nil, nil, err + } + kdfdata, err := bcrypt_pbkdf.Key(passphrase, opts.Salt, int(opts.Rounds), cipherKeylen+cipherIvLen) + if err != nil { + return nil, nil, err + } + + iv := kdfdata[cipherKeylen : cipherIvLen+cipherKeylen] + aeskey := kdfdata[0:cipherKeylen] + block, err := aes.NewCipher(aeskey) + + if err != nil { + return nil, nil, err + } + + return iv, block, nil +} diff --git a/sigtool.go b/sigtool.go index f022b51..ecbcc90 100644 --- a/sigtool.go +++ b/sigtool.go @@ -87,13 +87,13 @@ func main() { // Run the generate command func gen(args []string) { - var pw, help, force bool + var nopw, help, force bool var comment string var envpw string fs := flag.NewFlagSet("generate", flag.ExitOnError) fs.BoolVarP(&help, "help", "h", false, "Show this help and exit") - fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to encrypt the private key") + fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for a password for the private key") fs.StringVarP(&comment, "comment", "c", "", "Use `C` as the text comment for the keys") fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`") fs.BoolVarP(&force, "force", "F", false, "Overwrite the output file if it exists") @@ -124,24 +124,29 @@ Options: die("Public/Private key files (%s.key, %s.pub) exist. Won't overwrite!", bn, bn) } - var pws string var err error - if len(envpw) > 0 { - pws = os.Getenv(envpw) - } else if pw { - pws, err = utils.Askpass("Enter passphrase for private key", true) - if err != nil { - die("%s", err) - } - } - kp, err := sign.NewKeypair() if err != nil { die("%s", err) } - err = kp.Serialize(bn, comment, pws) + err = kp.Serialize(bn, comment, func() ([]byte, error) { + if nopw { + return nil, nil + } + + var pws string + if len(envpw) > 0 { + pws = os.Getenv(envpw) + } else { + pws, err = utils.Askpass("Enter passphrase for private key", true) + if err != nil { + die("%s", err) + } + } + return []byte(pws), nil + }) if err != nil { die("%s", err) } @@ -149,13 +154,13 @@ Options: // Run the 'sign' command. func signify(args []string) { - var pw, help bool + var nopw, help bool var output string var envpw string fs := flag.NewFlagSet("sign", flag.ExitOnError) fs.BoolVarP(&help, "help", "h", false, "Show this help and exit") - fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to decrypt the private key") + fs.BoolVarP(&nopw, "no-password", "", false, "Don't ask for a password for the private key") fs.StringVarP(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`") fs.StringVarP(&output, "output", "o", "", "Write signature to file `F`") @@ -182,23 +187,29 @@ Options: fn := args[1] outf := fmt.Sprintf("%s.sig", fn) - var pws string var err error - if len(envpw) > 0 { - pws = os.Getenv(envpw) - } else if pw { - pws, err = utils.Askpass("Enter passphrase for private key", false) - if err != nil { - die("%s", err) - } - } - if len(output) > 0 { outf = output } - sk, err := sign.ReadPrivateKey(kn, pws) + sk, err := sign.ReadPrivateKey(kn, func() ([]byte, error) { + if nopw { + return nil, nil + } + + var pws string + if len(envpw) > 0 { + pws = os.Getenv(envpw) + } else { + pws, err = utils.Askpass("Enter passphrase for private key", false) + if err != nil { + die("%s", err) + } + } + + return []byte(pws), nil + }) if err != nil { die("%s", err) } diff --git a/version b/version index 9e11b32..1d0ba9e 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.3.1 +0.4.0