diff --git a/README.md b/README.md index 4e1c768..3a4c0f8 100644 --- a/README.md +++ b/README.md @@ -233,8 +233,8 @@ decryption. ### How is the private key protected? The Ed25519 private key is encrypted in AES-GCM-256 mode using a key -derived from the user's pass-phrase. The user pass phrase is expanded via -SHA256; this expanded pass phrase is fed to `scrypt()` to +derived from the user's pass-phrase. The user pass phrase is expanded via +SHA256; this expanded pass phrase is fed to `scrypt()` to generate a key-encryption-key. In pseudo code, this operation looks like below: @@ -263,6 +263,9 @@ etc. * `src/ssh.go` contains code to parse SSH Ed25519 key files * `src/stream.go` contains code that provides an `io.Reader` and `io.WriteCloser` interface for encryption and decryption. +* `tests.sh` simple round trip test using the tool; this is in addition to the tests in + `sign/`. + The generated keys and signatures are proper YAML files and human readable. @@ -275,6 +278,11 @@ Signatures on large files are calculated efficiently by reading them in memory mapped mode (```mmap(2)```) and hashing the file contents using SHA-512. The Ed25519 signature is calculated on the file-hash. +### Tests +The core library in `sign/` has extensive tests to verify signing and encryption. +Additionally, a simple shell script `tests.sh` does a full roundtrip of tests +using `sigtool`. + ## Example of Keys, Signature ### Ed25519 Public Key diff --git a/crypt.go b/crypt.go index 795dea8..51fb680 100644 --- a/crypt.go +++ b/crypt.go @@ -36,14 +36,15 @@ func encrypt(args []string) { var outfile string var keyfile string var envpw string - var nopw bool + var nopw, force bool var blksize uint64 fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`") fs.StringVarP(&keyfile, "sign", "s", "", "Sign using private key `S`") 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(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`") fs.SizeVarP(&blksize, "block-size", "B", 128*1024, "Use `S` as the encryption block size") + fs.BoolVarP(&force, "overwrite", "", false, "Overwrite the output file if it exists") err := fs.Parse(args) if err != nil { @@ -114,26 +115,32 @@ func encrypt(args []string) { } if len(outfile) > 0 && outfile != "-" { + var mode os.FileMode = 0600 // conservative output mode + if inf != nil { - ost, err := os.Stat(outfile) - if err != nil { + var err error + var ist, ost os.FileInfo + + if ost, err = os.Stat(outfile); err != nil { die("can't stat %s: %s", outfile, err) } - ist, err := inf.Stat() - if err != nil { + if ist, err = inf.Stat(); err != nil { die("can't stat %s: %s", infile, err) } if os.SameFile(ist, ost) { die("won't create output file: same as input file!") } + mode = ist.Mode() } - outf := mustOpen(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) - defer outf.Close() - - outfd = outf + sf, err := sign.NewSafeFile(outfile, force, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + die("%s", err) + } + defer sf.Abort() + outfd = sf } en, err := sign.NewEncryptor(sk, blksize) @@ -178,6 +185,7 @@ func encrypt(args []string) { if err != nil { die("%s", err) } + outfd.Close() } type nullWriter struct{} @@ -202,13 +210,14 @@ func decrypt(args []string) { var envpw string var outfile string var pubkey string - var nopw, test bool + var nopw, test, force bool fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`") 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(&envpw, "env-password", "E", "", "Use passphrase from environment variable `E`") fs.StringVarP(&pubkey, "verify-sender", "v", "", "Verify that the sender matches public key in `F`") fs.BoolVarP(&test, "test", "t", false, "Test the encrypted file against the given key without writing to output") + fs.BoolVarP(&force, "overwrite", "", false, "Overwrite the output file if it exists") err := fs.Parse(args) if err != nil { @@ -221,7 +230,7 @@ func decrypt(args []string) { } var infd io.Reader = os.Stdin - var outfd io.Writer = os.Stdout + var outfd io.WriteCloser = os.Stdout var inf *os.File var infile string @@ -268,24 +277,30 @@ func decrypt(args []string) { if test { outfd = &nullWriter{} } else if len(outfile) > 0 && outfile != "-" { + var mode os.FileMode = 0600 // conservative mode + if inf != nil { - ost, err := os.Stat(outfile) - if err != nil { + var ist, ost os.FileInfo + var err error + + if ost, err = os.Stat(outfile); err != nil { die("can't stat %s: %s", outfile, err) } - ist, err := inf.Stat() - if err != nil { + if ist, err = inf.Stat(); err != nil { die("can't stat %s: %s", infile, err) } if os.SameFile(ist, ost) { die("won't create output file: same as input file!") } + mode = ist.Mode() } - outf := mustOpen(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) - defer outf.Close() - - outfd = outf + sf, err := sign.NewSafeFile(outfile, force, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + die("%s", err) + } + defer sf.Abort() + outfd = sf } d, err := sign.NewDecryptor(infd) @@ -306,14 +321,16 @@ func decrypt(args []string) { warn("%s: Missing sender Public Key; can't authenticate sender ..", fn) } - err = d.Decrypt(outfd) - if err != nil { + if err = d.Decrypt(outfd); err != nil { die("%s", err) } + outfd.Close() + if test { warn("Enc file OK") } + } func encryptUsage(fs *flag.FlagSet) { diff --git a/sign/encrypt.go b/sign/encrypt.go index 9831b86..5cb16e3 100644 --- a/sign/encrypt.go +++ b/sign/encrypt.go @@ -605,7 +605,7 @@ func (d *Decryptor) verifySender(key []byte, sk *PrivateKey, senderPK *PublicKey // Wrap data encryption key 'k' with the sender's PK and our ephemeral curve SK // basically, we do a scalarmult: Ephemeral encryption/decryption SK x receiver PK func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) { - rxPK := pk.toCurve25519PK() + rxPK := pk.ToCurve25519PK() dkek, err := curve25519.X25519(e.encSK, rxPK) if err != nil { return nil, fmt.Errorf("wrap: %w", err) @@ -637,7 +637,7 @@ func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) { // Unwrap a wrapped key using the receivers Ed25519 secret key 'sk' and // senders ephemeral PublicKey func (d *Decryptor) unwrapKey(w *pb.WrappedKey, sk *PrivateKey) ([]byte, error) { - ourSK := sk.toCurve25519SK() + ourSK := sk.ToCurve25519SK() dkek, err := curve25519.X25519(ourSK, d.Pk) if err != nil { return nil, fmt.Errorf("unwrap: %w", err) diff --git a/sign/encrypt_test.go b/sign/encrypt_test.go index e3ad9cb..53892a0 100644 --- a/sign/encrypt_test.go +++ b/sign/encrypt_test.go @@ -34,8 +34,10 @@ func (b *Buffer) Close() error { func TestEncryptSimple(t *testing.T) { assert := newAsserter(t) - receiver, err := NewKeypair() - assert(err == nil, "receiver keypair gen failed: %s", err) + sk, err := NewPrivateKey() + assert(err == nil, "SK gen failed: %s", err) + + pk := sk.PublicKey() var blkSize int = 1024 var size int = (blkSize * 10) @@ -49,7 +51,7 @@ func TestEncryptSimple(t *testing.T) { ee, err := NewEncryptor(nil, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) - err = ee.AddRecipient(&receiver.Pub) + err = ee.AddRecipient(pk) assert(err == nil, "can't add recipient: %s", err) rd := bytes.NewBuffer(buf) @@ -63,7 +65,7 @@ func TestEncryptSimple(t *testing.T) { dd, err := NewDecryptor(rd) assert(err == nil, "decryptor create fail: %s", err) - err = dd.SetPrivateKey(&receiver.Sec, nil) + err = dd.SetPrivateKey(sk, nil) assert(err == nil, "decryptor can't add SK: %s", err) wr = Buffer{} @@ -80,8 +82,10 @@ func TestEncryptSimple(t *testing.T) { func TestEncryptSmallSizes(t *testing.T) { assert := newAsserter(t) - receiver, err := NewKeypair() - assert(err == nil, "receiver keypair gen failed: %s", err) + sk, err := NewPrivateKey() + assert(err == nil, "SK gen failed: %s", err) + + pk := sk.PublicKey() var blkSize int = 8 var size int = (blkSize * 4) @@ -99,7 +103,7 @@ func TestEncryptSmallSizes(t *testing.T) { ee, err := NewEncryptor(nil, uint64(blkSize)) assert(err == nil, "encryptor-%d create fail: %s", i, err) - err = ee.AddRecipient(&receiver.Pub) + err = ee.AddRecipient(pk) assert(err == nil, "encryptor-%d: can't add recipient: %s", i, err) rd := bytes.NewBuffer(buf) @@ -113,7 +117,7 @@ func TestEncryptSmallSizes(t *testing.T) { dd, err := NewDecryptor(rd) assert(err == nil, "decryptor-%d create fail: %s", i, err) - err = dd.SetPrivateKey(&receiver.Sec, nil) + err = dd.SetPrivateKey(sk, nil) assert(err == nil, "decryptor-%d can't add SK: %s", i, err) wr = Buffer{} @@ -131,8 +135,10 @@ func TestEncryptSmallSizes(t *testing.T) { func TestEncryptCorrupted(t *testing.T) { assert := newAsserter(t) - receiver, err := NewKeypair() - assert(err == nil, "receiver keypair gen failed: %s", err) + sk, err := NewPrivateKey() + assert(err == nil, "SK gen failed: %s", err) + + pk := sk.PublicKey() var blkSize int = 1024 var size int = (blkSize * 23) + randmod(blkSize) @@ -146,7 +152,7 @@ func TestEncryptCorrupted(t *testing.T) { ee, err := NewEncryptor(nil, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) - err = ee.AddRecipient(&receiver.Pub) + err = ee.AddRecipient(pk) assert(err == nil, "can't add recipient: %s", err) rd := bytes.NewReader(buf) @@ -158,6 +164,7 @@ func TestEncryptCorrupted(t *testing.T) { rb := wr.Bytes() n := len(rb) + // corrupt the input for i := 0; i < n; i++ { j := randint() % n rb[j] = byte(randint() & 0xff) @@ -173,11 +180,11 @@ func TestEncryptCorrupted(t *testing.T) { func TestEncryptSenderVerified(t *testing.T) { assert := newAsserter(t) - sender, err := NewKeypair() - assert(err == nil, "sender keypair gen failed: %s", err) + sender, err := NewPrivateKey() + assert(err == nil, "sender SK gen failed: %s", err) - receiver, err := NewKeypair() - assert(err == nil, "receiver keypair gen failed: %s", err) + receiver, err := NewPrivateKey() + assert(err == nil, "receiver SK gen failed: %s", err) var blkSize int = 1024 var size int = (blkSize * 23) + randmod(blkSize) @@ -188,10 +195,10 @@ func TestEncryptSenderVerified(t *testing.T) { buf[i] = byte(i & 0xff) } - ee, err := NewEncryptor(&sender.Sec, uint64(blkSize)) + ee, err := NewEncryptor(sender, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) - err = ee.AddRecipient(&receiver.Pub) + err = ee.AddRecipient(receiver.PublicKey()) assert(err == nil, "can't add recipient: %s", err) rd := bytes.NewBuffer(buf) @@ -205,14 +212,15 @@ func TestEncryptSenderVerified(t *testing.T) { dd, err := NewDecryptor(rd) assert(err == nil, "decryptor create fail: %s", err) - // first send a wrong sender key - randkey, err := NewKeypair() - assert(err == nil, "receiver rand keypair gen failed: %s", err) + randkey, err := NewPrivateKey() + assert(err == nil, "rand SK gen failed: %s", err) - err = dd.SetPrivateKey(&receiver.Sec, &randkey.Pub) + // first send a wrong sender PK + err = dd.SetPrivateKey(receiver, randkey.PublicKey()) assert(err != nil, "decryptor failed to verify sender") - err = dd.SetPrivateKey(&receiver.Sec, &sender.Pub) + // then the correct sender PK + err = dd.SetPrivateKey(receiver, sender.PublicKey()) assert(err == nil, "decryptor can't add SK: %s", err) wr = Buffer{} @@ -229,8 +237,8 @@ func TestEncryptSenderVerified(t *testing.T) { func TestEncryptMultiReceiver(t *testing.T) { assert := newAsserter(t) - sender, err := NewKeypair() - assert(err == nil, "sender keypair gen failed: %s", err) + sender, err := NewPrivateKey() + assert(err == nil, "sender SK gen failed: %s", err) var blkSize int = 1024 var size int = (blkSize * 23) + randmod(blkSize) @@ -241,17 +249,17 @@ func TestEncryptMultiReceiver(t *testing.T) { buf[i] = byte(i & 0xff) } - ee, err := NewEncryptor(&sender.Sec, uint64(blkSize)) + ee, err := NewEncryptor(sender, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) n := 4 - rx := make([]*Keypair, n) + rx := make([]*PrivateKey, n) for i := 0; i < n; i++ { - r, err := NewKeypair() - assert(err == nil, "can't make receiver key %d: %s", i, err) + r, err := NewPrivateKey() + assert(err == nil, "can't make receiver SK %d: %s", i, err) rx[i] = r - err = ee.AddRecipient(&r.Pub) + err = ee.AddRecipient(r.PublicKey()) assert(err == nil, "can't add recipient %d: %s", i, err) } @@ -268,7 +276,7 @@ func TestEncryptMultiReceiver(t *testing.T) { dd, err := NewDecryptor(rd) assert(err == nil, "decryptor %d create fail: %s", i, err) - err = dd.SetPrivateKey(&rx[i].Sec, &sender.Pub) + err = dd.SetPrivateKey(rx[i], sender.PublicKey()) assert(err == nil, "decryptor can't add SK %d: %s", i, err) wr = Buffer{} @@ -286,7 +294,7 @@ func TestEncryptMultiReceiver(t *testing.T) { func TestStreamIO(t *testing.T) { assert := newAsserter(t) - receiver, err := NewKeypair() + receiver, err := NewPrivateKey() assert(err == nil, "receiver keypair gen failed: %s", err) var blkSize int = 1024 @@ -301,7 +309,7 @@ func TestStreamIO(t *testing.T) { ee, err := NewEncryptor(nil, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) - err = ee.AddRecipient(&receiver.Pub) + err = ee.AddRecipient(receiver.PublicKey()) assert(err == nil, "can't add recipient: %s", err) wr := Buffer{} @@ -334,7 +342,7 @@ func TestStreamIO(t *testing.T) { dd, err := NewDecryptor(rd) assert(err == nil, "decryptor create fail: %s", err) - err = dd.SetPrivateKey(&receiver.Sec, nil) + err = dd.SetPrivateKey(receiver, nil) assert(err == nil, "decryptor can't add SK: %s", err) rio, err := dd.NewStreamReader() @@ -368,8 +376,8 @@ func TestStreamIO(t *testing.T) { func TestSmallSizeStreamIO(t *testing.T) { assert := newAsserter(t) - receiver, err := NewKeypair() - assert(err == nil, "receiver keypair gen failed: %s", err) + receiver, err := NewPrivateKey() + assert(err == nil, "receiver SK gen failed: %s", err) var blkSize int = 8 var size int = blkSize * 10 @@ -387,7 +395,7 @@ func TestSmallSizeStreamIO(t *testing.T) { ee, err := NewEncryptor(nil, uint64(blkSize)) assert(err == nil, "encryptor create fail: %s", err) - err = ee.AddRecipient(&receiver.Pub) + err = ee.AddRecipient(receiver.PublicKey()) assert(err == nil, "can't add recipient: %s", err) wr := Buffer{} @@ -420,7 +428,7 @@ func TestSmallSizeStreamIO(t *testing.T) { dd, err := NewDecryptor(rd) assert(err == nil, "decryptor create fail: %s", err) - err = dd.SetPrivateKey(&receiver.Sec, nil) + err = dd.SetPrivateKey(receiver, nil) assert(err == nil, "decryptor can't add SK: %s", err) rio, err := dd.NewStreamReader() diff --git a/sign/keys.go b/sign/keys.go index 143d19b..3516911 100644 --- a/sign/keys.go +++ b/sign/keys.go @@ -28,7 +28,6 @@ import ( "encoding/binary" "fmt" "hash" - "io" "io/ioutil" "math/big" "os" @@ -64,15 +63,10 @@ type PublicKey struct { hash []byte } -// Ed25519 key pair -type Keypair struct { - Sec PrivateKey - Pub PublicKey -} - // Length of Ed25519 Public Key Hash const PKHashLength = 16 +// constants we use in this module const ( // Scrypt parameters _N int = 1 << 19 @@ -118,55 +112,27 @@ type signature struct { Signature string `yaml:"signature"` } +// given a public key, generate a deterministic short-hash of it. func pkhash(pk []byte) []byte { z := sha256.Sum256(pk) return z[:PKHashLength] } -// Generate a new Ed25519 keypair -func NewKeypair() (*Keypair, error) { - //kp := &Keypair{Sec: PrivateKey{N: 1 << 17, r: 64, p: 1}} - kp := &Keypair{} - sk := &kp.Sec - pk := &kp.Pub - sk.pk = pk - - p, s, err := Ed.GenerateKey(rand.Reader) +// NewPrivateKey generates a new Ed25519 private key +func NewPrivateKey() (*PrivateKey, error) { + pkb, skb, err := Ed.GenerateKey(rand.Reader) if err != nil { - return nil, fmt.Errorf("Can't generate Ed25519 keys: %s", err) + return nil, err } - pk.Pk = []byte(p) - sk.Sk = []byte(s) - pk.hash = pkhash(pk.Pk) - - return kp, nil -} - -// Serialize the keypair to two separate files. The basename of the -// file is 'bn'; the public key goes in $bn.pub and the private key -// 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, getpw func() ([]byte, error)) error { - - sk := &kp.Sec - pk := &kp.Pub - - skf := fmt.Sprintf("%s.key", bn) - pkf := fmt.Sprintf("%s.pub", bn) - - err := pk.serialize(pkf, comment) - if err != nil { - return fmt.Errorf("Can't serialize to %s: %s", pkf, err) + sk := &PrivateKey{ + Sk: []byte(skb), + pk: &PublicKey{ + Pk: []byte(pkb), + hash: pkhash([]byte(pkb)), + }, } - - err = sk.serialize(skf, comment, getpw) - if err != nil { - return fmt.Errorf("Can't serialize to %s: %s", pkf, err) - } - - return nil + return sk, nil } // Read the private key in 'fn', optionally decrypting it using @@ -177,73 +143,30 @@ func ReadPrivateKey(fn string, getpw func() ([]byte, error)) (*PrivateKey, error return nil, err } - if bytes.Index(yml, []byte("OPENSSH PRIVATE KEY-")) > 0 { - return parseSSHPrivateKey(yml, getpw) + var sk PrivateKey + if err = sk.UnmarshalBinary(yml, getpw); err != nil { + return nil, err } - - if pw, err := getpw(); err == nil { - return MakePrivateKey(yml, pw) - } - return nil, err + return &sk, nil } -// Make a private key from bytes 'yml' and password 'pw'. The bytes +// Make a private key from bytes 'yml' using optional caller provided +// getpw() function to read the password if needed. // are assumed to be serialized version of the private key. -func MakePrivateKey(yml []byte, pw []byte) (*PrivateKey, error) { - var ssk serializedPrivKey +func MakePrivateKey(yml []byte, getpw func() ([]byte, error)) (*PrivateKey, error) { + var sk PrivateKey - err := yaml.Unmarshal(yml, &ssk) + err := sk.UnmarshalBinary(yml, getpw) if err != nil { - return nil, fmt.Errorf("make priv key: can't parse YAML: %s", err) + return nil, err } - - if len(ssk.Salt) == 0 || len(ssk.Esk) == 0 { - return nil, fmt.Errorf("sign: not YAML private key") - } - - b64 := base64.StdEncoding.DecodeString - - salt, err := b64(ssk.Salt) - if err != nil { - return nil, fmt.Errorf("make priv key: can't decode salt: %s", err) - } - - esk, err := b64(ssk.Esk) - if err != nil { - return nil, fmt.Errorf("make priv key: can't decode key: %s", err) - } - - // We take short passwords and extend them - pwb := sha512.Sum512(pw) - - // "32" == Length of AES-256 key - key, err := scrypt.Key(pwb[:], salt, ssk.N, ssk.R, ssk.P, 32) - if err != nil { - return nil, fmt.Errorf("make priv key: can't derive key: %s", err) - } - - aes, err := aes.NewCipher(key) - if err != nil { - return nil, fmt.Errorf("make priv key: aes failure: %s", err) - } - - ae, err := cipher.NewGCM(aes) - if err != nil { - return nil, fmt.Errorf("make priv key: aes failure: %s", err) - } - - skb, err := ae.Open(nil, salt[:ae.NonceSize()], esk, nil) - if err != nil { - return nil, fmt.Errorf("make priv key: wrong password") - } - - return PrivateKeyFromBytes(skb) + return &sk, nil } -// Make a private key from 64-bytes of extended Ed25519 key -func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) { +// make a PrivateKey from a byte array containing ed25519 raw SK +func makePrivateKeyFromBytes(sk *PrivateKey, buf []byte) error { if len(buf) != 64 { - return nil, fmt.Errorf("private key is malformed (len %d!)", len(buf)) + return fmt.Errorf("private key is malformed (len %d!)", len(buf)) } skb := make([]byte, 64) @@ -256,21 +179,27 @@ func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) { Pk: []byte(edpk), hash: pkhash([]byte(edpk)), } - sk := &PrivateKey{ - Sk: skb, - pk: pk, - } - - return sk, nil + sk.Sk = skb + sk.pk = pk + return nil } +/* +// Make a private key from 64-bytes of extended Ed25519 key +func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) { + var sk PrivateKey + + return makePrivateKeyFromBytes(&sk, buf) +} +*/ + // Given a secret key, return the corresponding Public Key func (sk *PrivateKey) PublicKey() *PublicKey { return sk.pk } // Convert an Ed25519 Private Key to Curve25519 Private key -func (sk *PrivateKey) toCurve25519SK() []byte { +func (sk *PrivateKey) ToCurve25519SK() []byte { if sk.ck == nil { var ek [64]byte @@ -284,12 +213,205 @@ func (sk *PrivateKey) toCurve25519SK() []byte { return sk.ck } +// Serialize the private key to file 'fn' using human readable +// 'comment' and encrypt the key with supplied passphrase 'pw'. +func (sk *PrivateKey) Serialize(fn, comment string, ovwrite bool, pw []byte) error { + b, err := sk.MarshalBinary(comment, pw) + if err == nil { + return writeFile(fn, b, ovwrite, 0600) + } + return err +} + +// MarshalBinary marshals the private key with a caller provided +// passphrase 'pw' and human readable 'comment' +func (sk *PrivateKey) MarshalBinary(comment string, pw []byte) ([]byte, error) { + // expand the password into 64 bytes + pass := sha512.Sum512(pw) + salt := make([]byte, 32) + + randRead(salt) + + // "32" == Length of AES-256 key + key, err := scrypt.Key(pass[:], salt, _N, _r, _p, 32) + if err != nil { + return nil, fmt.Errorf("marshal: can't derive scrypt key: %s", err) + } + + aes, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("marshal: %s", err) + } + + ae, err := cipher.NewGCM(aes) + if err != nil { + return nil, fmt.Errorf("marshal: %s", err) + } + + tl := ae.Overhead() + buf := make([]byte, tl+len(sk.Sk)) + esk := ae.Seal(buf[:0], salt[:ae.NonceSize()], sk.Sk, nil) + + enc := base64.StdEncoding.EncodeToString + + ssk := serializedPrivKey{ + Comment: comment, + Esk: enc(esk), + Salt: enc(salt), + Algo: sk_algo, + N: _N, + R: _r, + P: _p, + } + + // We won't protect the Scrypt parameters with the hash above + // because it is not needed. If the parameters are wrong, the + // derived key will be wrong and thus, the hash will not match. + + return yaml.Marshal(&ssk) +} + +// UnmarshalBinary unmarshals the private key and optionally invokes the +// caller provided getpw() function to read the password if needed. If the +// input byte stream 'b' is an OpenSSH ed25519 key, this function transparently +// decodes it. +func (sk *PrivateKey) UnmarshalBinary(b []byte, getpw func() ([]byte, error)) error { + if bytes.Index(b, []byte("OPENSSH PRIVATE KEY-")) > 0 { + xk, err := parseSSHPrivateKey(b, getpw) + if err != nil { + return err + } + *sk = *xk + return nil + } + + var pw []byte + if getpw != nil { + var err error + pw, err = getpw() + if err != nil { + return err + } + } + + // We take short passwords and extend them + pwb := sha512.Sum512(pw) + + var ssk serializedPrivKey + + err := yaml.Unmarshal(b, &ssk) + if err != nil { + return fmt.Errorf("unmarshal priv key: can't parse YAML: %s", err) + } + + if len(ssk.Salt) == 0 || len(ssk.Esk) == 0 { + return fmt.Errorf("unmarshal priv key: not YAML format") + } + + b64 := base64.StdEncoding.DecodeString + + salt, err := b64(ssk.Salt) + if err != nil { + return fmt.Errorf("unmarshal priv key: can't decode salt: %s", err) + } + + esk, err := b64(ssk.Esk) + if err != nil { + return fmt.Errorf("unmarshal priv key: can't decode key: %s", err) + } + + // "32" == Length of AES-256 key + key, err := scrypt.Key(pwb[:], salt, ssk.N, ssk.R, ssk.P, 32) + if err != nil { + return fmt.Errorf("unmarshal priv key: can't derive key: %s", err) + } + + aes, err := aes.NewCipher(key) + if err != nil { + return fmt.Errorf("unmarshal priv key: aes failure: %s", err) + } + + ae, err := cipher.NewGCM(aes) + if err != nil { + return fmt.Errorf("unmarshal priv key: aes failure: %s", err) + } + + skb := make([]byte, 64) + skb, err = ae.Open(skb[:0], salt[:ae.NonceSize()], esk, nil) + if err != nil { + return fmt.Errorf("unmarshal priv key: wrong password") + } + + return makePrivateKeyFromBytes(sk, skb) +} + +// --- Public Key Methods --- + +// Read the public key from 'fn' and create new instance of +// PublicKey +func ReadPublicKey(fn string) (*PublicKey, error) { + var err error + var yml []byte + + if yml, err = ioutil.ReadFile(fn); err != nil { + return nil, err + } + + var pk PublicKey + if err = pk.UnmarshalBinary(yml); err != nil { + return nil, err + } + return &pk, nil +} + +// Parse a serialized public in 'yml' and return the resulting +// public key instance +func MakePublicKey(yml []byte) (*PublicKey, error) { + var pk PublicKey + if err := pk.UnmarshalBinary(yml); err != nil { + return nil, err + } + return &pk, nil +} + +func makePublicKeyFromBytes(pk *PublicKey, b []byte) error { + if len(b) != 32 { + return fmt.Errorf("public key is malformed (len %d!)", len(b)) + } + + pk.Pk = make([]byte, 32) + pk.hash = pkhash(b) + copy(pk.Pk, b) + + return nil +} + +/* +// Make a public key from a byte string +func PublicKeyFromBytes(b []byte) (*PublicKey, error) { + var pk PublicKey + + makePublicKeyFromBytes(&pk, b) +} +*/ + +// Serialize a PublicKey into file 'fn' with a human readable 'comment'. +// If 'ovwrite' is true, overwrite the file if it exists. +func (pk *PublicKey) Serialize(fn, comment string, ovwrite bool) error { + out, err := pk.MarshalBinary(comment) + if err == nil { + return writeFile(fn, out, ovwrite, 0644) + } + + return err +} + // from github.com/FiloSottile/age var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10) // Convert an Ed25519 Public Key to Curve25519 public key // from github.com/FiloSottile/age -func (pk *PublicKey) toCurve25519PK() []byte { +func (pk *PublicKey) ToCurve25519PK() []byte { if pk.ck != nil { return pk.ck } @@ -329,131 +451,8 @@ func (pk *PublicKey) Hash() []byte { return pk.hash } -// Serialize the private key to a file -// AEAD encryption for protecting the private key -// Format: YAML -// All []byte are in base64 (RawEncoding) -func (sk *PrivateKey) serialize(fn, comment string, getpw func() ([]byte, error)) error { - pw, err := getpw() - if err != nil { - return err - } - - // expand the password into 64 bytes - pass := sha512.Sum512(pw) - salt := make([]byte, 32) - - randRead(salt) - - // "32" == Length of AES-256 key - key, err := scrypt.Key(pass[:], salt, _N, _r, _p, 32) - if err != nil { - return fmt.Errorf("marshal: can't derive scrypt key: %s", err) - } - - aes, err := aes.NewCipher(key) - if err != nil { - return fmt.Errorf("marshal: %s", err) - } - - ae, err := cipher.NewGCM(aes) - if err != nil { - return fmt.Errorf("marshal: %s", err) - } - - tl := ae.Overhead() - buf := make([]byte, tl+len(sk.Sk)) - esk := ae.Seal(buf[:0], salt[:ae.NonceSize()], sk.Sk, nil) - - enc := base64.StdEncoding.EncodeToString - - ssk := serializedPrivKey{ - Comment: comment, - Esk: enc(esk), - Salt: enc(salt), - Algo: sk_algo, - N: _N, - R: _r, - P: _p, - } - - // We won't protect the Scrypt parameters with the hash above - // because it is not needed. If the parameters are wrong, the - // derived key will be wrong and thus, the hash will not match. - - out, err := yaml.Marshal(&ssk) - if err != nil { - return fmt.Errorf("can't marahal to YAML: %s", err) - } - - return writeFile(fn, out, 0600) -} - -// --- Public Key Methods --- - -// Read the public key from 'fn' and create new instance of -// PublicKey -func ReadPublicKey(fn string) (*PublicKey, error) { - var err error - var yml []byte - - if yml, err = ioutil.ReadFile(fn); err != nil { - return nil, err - } - - // 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 -// public key instance -func MakePublicKey(yml []byte) (*PublicKey, error) { - var spk serializedPubKey - var err error - - if err = yaml.Unmarshal(yml, &spk); err != nil { - return nil, fmt.Errorf("can't parse YAML: %s", err) - } - - if len(spk.Pk) == 0 { - return nil, fmt.Errorf("sign: not a YAML public key") - } - - b64 := base64.StdEncoding.DecodeString - var pkb []byte - - if pkb, err = b64(spk.Pk); err != nil { - return nil, fmt.Errorf("can't decode YAML:Pk: %s", err) - } - - 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 -func PublicKeyFromBytes(b []byte) (*PublicKey, error) { - if len(b) != 32 { - return nil, fmt.Errorf("public key is malformed (len %d!)", len(b)) - } - - pk := &PublicKey{ - Pk: make([]byte, 32), - hash: pkhash(b), - } - - copy(pk.Pk, b) - return pk, nil -} - -// Serialize Public Keys -func (pk *PublicKey) serialize(fn, comment string) error { +// MarshalBinary marshals a PublicKey into a byte array +func (pk *PublicKey) MarshalBinary(comment string) ([]byte, error) { b64 := base64.StdEncoding.EncodeToString spk := &serializedPubKey{ Comment: comment, @@ -461,57 +460,61 @@ func (pk *PublicKey) serialize(fn, comment string) error { Hash: b64(pk.hash), } - out, err := yaml.Marshal(spk) - if err != nil { - return fmt.Errorf("can't marahal to YAML: %s", err) + return yaml.Marshal(spk) +} + +// UnmarshalBinary constructs a PublicKey from a previously +// marshaled byte stream instance. In addition, it is also +// capable of parsing an OpenSSH ed25519 public key. +func (pk *PublicKey) UnmarshalBinary(yml []byte) error { + + // first try to parse as a ssh key + if xk, err := parseSSHPublicKey(yml); err == nil { + *pk = *xk + return nil } - return writeFile(fn, out, 0644) + // OK Yaml it is. + + var spk serializedPubKey + var err error + + if err = yaml.Unmarshal(yml, &spk); err != nil { + return fmt.Errorf("can't parse YAML: %s", err) + } + + if len(spk.Pk) == 0 { + return fmt.Errorf("sign: not a YAML public key") + } + + b64 := base64.StdEncoding.DecodeString + var pkb []byte + + if pkb, err = b64(spk.Pk); err != nil { + return fmt.Errorf("can't decode YAML:Pk: %s", err) + } + + return makePublicKeyFromBytes(pk, pkb) } // -- Internal Utility Functions -- -// Unlink a file. -func unlink(f string) { - st, err := os.Stat(f) - if err == nil { - if !st.Mode().IsRegular() { - panic(fmt.Sprintf("%s can't be unlinked. Not a regular file?", f)) - } - - os.Remove(f) - return - } -} - // Simple function to reliably write data to a file. // Does MORE than ioutil.WriteFile() - in that it doesn't trash the // existing file with an incomplete write. -func writeFile(fn string, b []byte, mode uint32) error { - tmp := fmt.Sprintf("%s.tmp", fn) - unlink(tmp) - - fd, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(mode)) +func writeFile(fn string, b []byte, ovwrite bool, mode uint32) error { + sf, err := NewSafeFile(fn, ovwrite, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(mode)) if err != nil { - return fmt.Errorf("Can't create file %s: %s", tmp, err) + return err } + defer sf.Abort() // always cleanup on error - _, err = fd.Write(b) - if err != nil { - fd.Close() - // XXX Do we delete the tmp file? - return fmt.Errorf("Can't write %v bytes to %s: %s", len(b), tmp, err) - } - - fd.Close() // we ignore close(2) errors; unrecoverable anyway. - - os.Rename(tmp, fn) - return nil + sf.Write(b) + return sf.Close() } // Generate file checksum out of hash function h func fileCksum(fn string, h hash.Hash) ([]byte, error) { - fd, err := os.Open(fn) if err != nil { return nil, fmt.Errorf("can't open %s: %s", fn, err) @@ -528,7 +531,7 @@ func fileCksum(fn string, h hash.Hash) ([]byte, error) { binary.BigEndian.PutUint64(b[:], uint64(sz)) h.Write(b[:]) - return h.Sum(nil), nil + return h.Sum(nil)[:], nil } func clamp(k []byte) []byte { @@ -538,13 +541,5 @@ func clamp(k []byte) []byte { return k } -func randRead(b []byte) []byte { - _, err := io.ReadFull(rand.Reader, b) - if err != nil { - panic(fmt.Sprintf("can't read %d bytes of random data: %s", len(b), err)) - } - return b -} - // EOF // vim: noexpandtab:ts=8:sw=8:tw=92: diff --git a/sign/rand.go b/sign/rand.go new file mode 100644 index 0000000..e571783 --- /dev/null +++ b/sign/rand.go @@ -0,0 +1,40 @@ +// rand.go - utility functions to generate random quantities +// +// (c) 2018 Sudhi Herle +// +// Licensing Terms: GPLv2 +// +// If you need a commercial license for this work, please contact +// the author. +// +// This software does not come with any express or implied +// warranty; it is provided "as is". No claim is made to its +// suitability for any purpose. + +package sign + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + "io" +) + +func randu32() uint32 { + var b [4]byte + + _, err := io.ReadFull(rand.Reader, b[:]) + if err != nil { + panic(fmt.Sprintf("can't read 4 rand bytes: %s", err)) + } + + return binary.LittleEndian.Uint32(b[:]) +} + +func randRead(b []byte) []byte { + _, err := io.ReadFull(rand.Reader, b) + if err != nil { + panic(fmt.Sprintf("can't read %d bytes of random data: %s", len(b), err)) + } + return b +} diff --git a/sign/safefile.go b/sign/safefile.go new file mode 100644 index 0000000..8a28af8 --- /dev/null +++ b/sign/safefile.go @@ -0,0 +1,124 @@ +// safefile.go - safe file creation and unwinding on error +// +// (c) 2021 Sudhi Herle +// +// Licensing Terms: GPLv2 +// +// If you need a commercial license for this work, please contact +// the author. +// +// This software does not come with any express or implied +// warranty; it is provided "as is". No claim is made to its +// suitability for any purpose. + +package sign + +import ( + "fmt" + "io" + "os" +) + +// SafeFile is an io.WriteCloser which uses a temporary file that +// will be atomically renamed when there are no errors and +// caller invokes Close(). Callers are advised to call +// Abort() in the appropriate error handling (defer) context +// so that the temporary file is properly deleted. +type SafeFile struct { + *os.File + + // error for writes recorded once + err error + name string // actual filename + + closed bool // set if the file is closed properly +} + +var _ io.WriteCloser = &SafeFile{} + +// NewSafeFile creates a new temporary file that would either be +// aborted or safely renamed to the correct name. +// 'nm' is the name of the final file; if 'ovwrite' is true, +// then the file is overwritten if it exists. +func NewSafeFile(nm string, ovwrite bool, flag int, perm os.FileMode) (*SafeFile, error) { + if _, err := os.Stat(nm); err == nil && !ovwrite { + return nil, fmt.Errorf("safefile: won't overwrite existing %s", nm) + } + + // forcibly unlink the old file - so previous artifacts don't exist + os.Remove(nm) + + tmp := fmt.Sprintf("%s.tmp.%d.%x", nm, os.Getpid(), randu32()) + fd, err := os.OpenFile(tmp, flag, perm) + if err != nil { + return nil, err + } + + sf := &SafeFile{ + File: fd, + name: nm, + } + return sf, nil +} + +// Attempt to write everything in 'b' and don't proceed if there was +// a previous error or the file was already closed. +func (sf *SafeFile) Write(b []byte) (int, error) { + if sf.err != nil { + return 0, sf.err + } + + if sf.closed { + return 0, fmt.Errorf("safefile: %s is closed", sf.Name()) + } + + var z, nw int + n := len(b) + for n > 0 { + if nw, sf.err = sf.File.Write(b); sf.err != nil { + return z, sf.err + } + z += nw + n -= nw + b = b[nw:] + } + return z, nil +} + +// Abort the file write and remove any temporary artifacts +func (sf *SafeFile) Abort() { + // if we've successfully closed, nothing to do! + if sf.closed { + return + } + + sf.closed = true + sf.File.Close() + os.Remove(sf.Name()) +} + +// Close flushes all file data & metadata to disk, closes the file and atomically renames +// the temp file to the actual file - ONLY if there were no intervening errors. +func (sf *SafeFile) Close() error { + if sf.err != nil { + sf.Abort() + return sf.err + } + + // mark this file as closed! + sf.closed = true + + if sf.err = sf.Sync(); sf.err != nil { + return sf.err + } + + if sf.err = sf.File.Close(); sf.err != nil { + return sf.err + } + + if sf.err = os.Rename(sf.Name(), sf.name); sf.err != nil { + return sf.err + } + + return nil +} diff --git a/sign/sign.go b/sign/sign.go index c7b0a8a..b8714a0 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -86,12 +86,13 @@ func ReadSignature(fn string) (*Signature, error) { return nil, err } - return MakeSignature(yml) + var sig Signature + return makeSignature(&sig, yml) } // Parse serialized signature from bytes 'b' and construct a // Signature object -func MakeSignature(b []byte) (*Signature, error) { +func makeSignature(sig *Signature, b []byte) (*Signature, error) { var ss signature err := yaml.Unmarshal(b, &ss) if err != nil { @@ -110,29 +111,33 @@ func MakeSignature(b []byte) (*Signature, error) { return nil, fmt.Errorf("can't decode Base64:Pkhash <%s>: %s", ss.Pkhash, err) } - return &Signature{Sig: s, pkhash: p}, nil + sig.Sig = s + sig.pkhash = p + return sig, nil } -// Serialize a signature suitable for storing in durable media -func (sig *Signature) Serialize(comment string) ([]byte, error) { - +// MarshalBinary marshals a signature into a byte stream with +// an optional caller supplied comment. +func (sig *Signature) MarshalBinary(comment string) ([]byte, error) { sigs := base64.StdEncoding.EncodeToString(sig.Sig) pks := base64.StdEncoding.EncodeToString(sig.pkhash) ss := &signature{Comment: comment, Pkhash: pks, Signature: sigs} - out, err := yaml.Marshal(ss) - if err != nil { - return nil, fmt.Errorf("can't marshal signature of %x to YAML: %s", sig.Sig, err) - } - - return out, nil + return yaml.Marshal(ss) } -// SerializeFile serializes the signature to an output file 'f' -func (sig *Signature) SerializeFile(fn, comment string) error { - b, err := sig.Serialize(comment) +// UnmarshalBinary constructs a Signature from a previously +// serialized bytestream +func (sig *Signature) UnmarshalBinary(b []byte) error { + _, err := makeSignature(sig, b) + return err +} + +// Serialize a signature suitable for storing in durable media +func (sig *Signature) Serialize(fn, comment string, ovwrite bool) error { + b, err := sig.MarshalBinary(comment) if err == nil { - err = writeFile(fn, b, 0644) + err = writeFile(fn, b, ovwrite, 0644) } return err } @@ -147,7 +152,6 @@ func (sig *Signature) IsPKMatch(pk *PublicKey) bool { // Verify a signature 'sig' for file 'fn' against public key 'pk' // Return True if signature matches, False otherwise func (pk *PublicKey) VerifyFile(fn string, sig *Signature) (bool, error) { - ck, err := fileCksum(fn, sha512.New()) if err != nil { return false, err diff --git a/sign/sign_test.go b/sign/sign_test.go index b794657..369c4ec 100644 --- a/sign/sign_test.go +++ b/sign/sign_test.go @@ -38,16 +38,20 @@ func tempdir(t *testing.T) string { return tmp } +var fixedPw = []byte("abc") +var badPw = []byte("def") +var nilPw []byte + // return a hardcoded password func hardcodedPw() ([]byte, error) { - return []byte("abc"), nil + return fixedPw, nil } func wrongPw() ([]byte, error) { - return []byte("xyz"), nil + return badPw, nil } func emptyPw() ([]byte, error) { - return nil, nil + return nilPw, nil } // Return true if file exists, false otherwise @@ -80,68 +84,78 @@ p: 1 func TestSignSimple(t *testing.T) { assert := newAsserter(t) - kp, err := NewKeypair() - assert(err == nil, "NewKeyPair() fail") + sk, err := NewPrivateKey() + assert(err == nil, "NewPrivateKey() fail") - dn := tempdir(t) + pk := sk.PublicKey() + + dn := t.TempDir() bn := fmt.Sprintf("%s/t0", dn) - err = kp.Serialize(bn, "", hardcodedPw) - assert(err == nil, "keyPair.Serialize() fail") - pkf := fmt.Sprintf("%s.pub", bn) skf := fmt.Sprintf("%s.key", bn) - // We must find these two files - assert(fileExists(pkf), "missing pkf") - assert(fileExists(skf), "missing skf") + err = pk.Serialize(pkf, "", true) + assert(err == nil, "can't serialize pk %s", pkf) - pk, err := ReadPublicKey(pkf) + // try to overwrite + err = pk.Serialize(pkf, "", false) + assert(err != nil, "pk %s overwritten!", pkf) + + err = sk.Serialize(skf, "", true, fixedPw) + assert(err == nil, "can't serialize sk %s", skf) + + err = sk.Serialize(skf, "", false, nilPw) + assert(err != nil, "sk %s overwritten!", skf) + + // We must find these two files + assert(fileExists(pkf), "missing pkf %s", pkf) + assert(fileExists(skf), "missing skf %s", skf) + + npk, err := ReadPublicKey(pkf) assert(err == nil, "ReadPK() fail") - // -ditto- for Sk - sk, err := ReadPrivateKey(pkf, emptyPw) + // send the public key as private key + nsk, err := ReadPrivateKey(pkf, emptyPw) assert(err != nil, "bad SK ReadSK fail: %s", err) - sk, err = ReadPrivateKey(skf, emptyPw) - assert(err != nil, "ReadSK() empty pw fail: ks", err) + nsk, err = ReadPrivateKey(skf, emptyPw) + assert(err != nil, "ReadSK() worked with empty pw") - sk, err = ReadPrivateKey(skf, wrongPw) - assert(err != nil, "ReadSK() wrong pw fail: %s", err) + nsk, err = ReadPrivateKey(skf, wrongPw) + assert(err != nil, "ReadSK() worked with wrong pw") badf := fmt.Sprintf("%s/badf.key", dn) err = ioutil.WriteFile(badf, []byte(badsk), 0600) - assert(err == nil, "write badsk") + assert(err == nil, "can't write badsk: %s", err) - sk, err = ReadPrivateKey(badf, hardcodedPw) - assert(err != nil, "badsk read fail: %s", err) + nsk, err = ReadPrivateKey(badf, hardcodedPw) + assert(err != nil, "decoded bad SK") // Finally, with correct password it should work. - sk, err = ReadPrivateKey(skf, hardcodedPw) - assert(err == nil, "ReadSK() correct pw fail") + nsk, err = ReadPrivateKey(skf, hardcodedPw) + assert(err == nil, "ReadSK() correct pw fail: %s", err) // And, deserialized keys should be identical - assert(byteEq(pk.Pk, kp.Pub.Pk), "pkbytes unequal") - assert(byteEq(sk.Sk, kp.Sec.Sk), "skbytes unequal") - - os.RemoveAll(dn) + assert(byteEq(pk.Pk, npk.Pk), "pkbytes unequal") + assert(byteEq(sk.Sk, nsk.Sk), "skbytes unequal") } // #2. Create new key pair, sign a rand buffer and verify func TestSignRandBuf(t *testing.T) { assert := newAsserter(t) - kp, err := NewKeypair() - assert(err == nil, "NewKeyPair() fail") + + sk, err := NewPrivateKey() + assert(err == nil, "NewPrivateKey() fail: %s", err) var ck [64]byte // simulates sha512 sum randRead(ck[:]) - pk := &kp.Pub - sk := &kp.Sec + pk := sk.PublicKey() ss, err := sk.SignMessage(ck[:], "") - assert(err == nil, "sk.sign fail") + assert(err == nil, "sk.sign fail: %s", err) assert(ss != nil, "sig is null") // verify sig @@ -160,27 +174,13 @@ func TestSignRandBuf(t *testing.T) { assert(ok, "verify fail") // Now sign a file - dn := tempdir(t) - bn := fmt.Sprintf("%s/k", dn) - - pkf := fmt.Sprintf("%s.pub", bn) - skf := fmt.Sprintf("%s.key", bn) - - err = kp.Serialize(bn, "", emptyPw) - assert(err == nil, "keyPair.Serialize() fail") - - // Now read the private key and sign - sk, err = ReadPrivateKey(skf, emptyPw) - assert(err == nil, "readSK fail") - - pk, err = ReadPublicKey(pkf) - assert(err == nil, "ReadPK fail") + dn := t.TempDir() var buf [8192]byte zf := fmt.Sprintf("%s/file.dat", dn) fd, err := os.OpenFile(zf, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - assert(err == nil, "file.dat creat file") + assert(err == nil, "file.dat creat file: %s", err) for i := 0; i < 8; i++ { randRead(buf[:]) @@ -192,27 +192,31 @@ func TestSignRandBuf(t *testing.T) { fd.Close() sig, err := sk.SignFile(zf) - assert(err == nil, "file.dat sign fail") + assert(err == nil, "file.dat sign fail: %s", err) assert(sig != nil, "file.dat sign nil") ok, err = pk.VerifyFile(zf, sig) - assert(err == nil, "file.dat verify fail") + assert(err == nil, "file.dat verify fail: %s", err) assert(ok, "file.dat verify false") // Now, serialize the signature and read it back sf := fmt.Sprintf("%s/file.sig", dn) - err = sig.SerializeFile(sf, "") - assert(err == nil, "sig serialize fail") + err = sig.Serialize(sf, "", true) + assert(err == nil, "sig serialize fail: %s", err) + + // now try to overwrite it + err = sig.Serialize(sf, "", false) + assert(err != nil, "sig serialize overwrote?!") s2, err := ReadSignature(sf) - assert(err == nil, "file.sig read fail") + assert(err == nil, "file.sig read fail: %s", err) assert(s2 != nil, "file.sig sig nil") assert(byteEq(s2.Sig, sig.Sig), "sig compare fail") // If we give a wrong file, verify must fail st, err := os.Stat(zf) - assert(err == nil, "file.dat stat fail") + assert(err == nil, "file.dat stat fail: %s", err) n := st.Size() assert(n == 8192*8, "file.dat size fail") @@ -220,12 +224,12 @@ func TestSignRandBuf(t *testing.T) { os.Truncate(zf, n-1) st, err = os.Stat(zf) - assert(err == nil, "file.dat stat2 fail") + assert(err == nil, "file.dat stat2 fail: %s", err) assert(st.Size() == (n-1), "truncate fail") // Now verify this corrupt file ok, err = pk.VerifyFile(zf, sig) - assert(err == nil, "file.dat corrupt i/o fail") + assert(err == nil, "file.dat corrupt i/o fail: %s", err) assert(!ok, "file.dat corrupt verify false") os.RemoveAll(dn) @@ -233,7 +237,7 @@ func TestSignRandBuf(t *testing.T) { func Benchmark_Keygen(b *testing.B) { for i := 0; i < b.N; i++ { - _, _ = NewKeypair() + _, _ = NewPrivateKey() } } @@ -250,7 +254,8 @@ func Benchmark_Sig(b *testing.B) { } b.StopTimer() - kp, _ := NewKeypair() + sk, _ := NewPrivateKey() + pk := sk.PublicKey() var sig *Signature for _, sz := range sizes { buf := randbuf(sz) @@ -260,11 +265,11 @@ func Benchmark_Sig(b *testing.B) { b.ResetTimer() b.Run(s0, func(b *testing.B) { - sig = benchSign(b, buf, &kp.Sec) + sig = benchSign(b, buf, sk) }) b.Run(s1, func(b *testing.B) { - benchVerify(b, buf, sig, &kp.Pub) + benchVerify(b, buf, sig, pk) }) } } diff --git a/sign/ssh.go b/sign/ssh.go index 5e59ac6..13bc1e2 100644 --- a/sign/ssh.go +++ b/sign/ssh.go @@ -95,11 +95,13 @@ func parseEncPubKey(in []byte, comm string) (*PublicKey, error) { return nil, ErrBadTrailers } - pk, err := PublicKeyFromBytes(w.KeyBytes) - if err == nil { + var pk PublicKey + + if err = makePublicKeyFromBytes(&pk, w.KeyBytes); err == nil { pk.Comment = strings.TrimSpace(comm) + return &pk, nil } - return pk, err + return nil, err } func parseString(in []byte) (out, rest []byte, ok bool) { @@ -343,8 +345,11 @@ func parseOpenSSHPrivateKey(data []byte, getpw func() ([]byte, error)) (*Private } } - pk, err := PrivateKeyFromBytes(key.Priv) - return pk, err + var sk PrivateKey + if err = makePrivateKeyFromBytes(&sk, key.Priv); err == nil { + return &sk, nil + } + return nil, err default: return nil, fmt.Errorf("ssh: unhandled key type: %v", pk1.Keytype) } diff --git a/sigtool.go b/sigtool.go index ecbcc90..da6dc5b 100644 --- a/sigtool.go +++ b/sigtool.go @@ -96,7 +96,7 @@ func gen(args []string) { 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") + fs.BoolVarP(&force, "overwrite", "", false, "Overwrite the output file if it exists") fs.Parse(args) @@ -120,22 +120,19 @@ Options: bn := args[0] - if exists(bn) && !force { - die("Public/Private key files (%s.key, %s.pub) exist. Won't overwrite!", bn, bn) + pkn := fmt.Sprintf("%s.pub", path.Clean(bn)) + skn := fmt.Sprintf("%s.key", path.Clean(bn)) + + if !force { + if exists(pkn) || exists(skn) { + die("Public/Private key files (%s, %s) exist. won't overwrite!", skn, pkn) + } } var err error + var pw []byte - kp, err := sign.NewKeypair() - if err != nil { - die("%s", err) - } - - err = kp.Serialize(bn, comment, func() ([]byte, error) { - if nopw { - return nil, nil - } - + if !nopw { var pws string if len(envpw) > 0 { pws = os.Getenv(envpw) @@ -145,16 +142,28 @@ Options: die("%s", err) } } - return []byte(pws), nil - }) + + pw = []byte(pws) + } + + sk, err := sign.NewPrivateKey() if err != nil { die("%s", err) } + + if err = sk.Serialize(skn, comment, force, pw); err != nil { + die("%s", err) + } + + pk := sk.PublicKey() + if err = pk.Serialize(pkn, comment, force); err != nil { + die("%s", err) + } } // Run the 'sign' command. func signify(args []string) { - var nopw, help bool + var nopw, help, force bool var output string var envpw string @@ -163,6 +172,7 @@ func signify(args []string) { 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`") + fs.BoolVarP(&force, "overwrite", "", false, "Overwrite previous signature file if it exists") fs.Parse(args) @@ -193,6 +203,19 @@ Options: outf = output } + var fd io.WriteCloser = os.Stdout + + if outf != "-" { + sf, err := sign.NewSafeFile(outf, force, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + die("can't create sig file: %s", err) + } + + // we unlink and remove temp on any error + defer sf.Abort() + fd = sf + } + sk, err := sign.ReadPrivateKey(kn, func() ([]byte, error) { if nopw { return nil, nil @@ -219,20 +242,9 @@ Options: die("%s", err) } - sigo, err := sig.Serialize(fmt.Sprintf("input=%s", fn)) - - var fd io.Writer = os.Stdout - - if outf != "-" { - fdx, err := os.OpenFile(outf, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) - if err != nil { - die("can't create output file %s: %s", outf, err) - } - defer fdx.Close() - fd = fdx - } - - fd.Write(sigo) + sigbytes, err := sig.MarshalBinary(fmt.Sprintf("input=%s", fn)) + fd.Write(sigbytes) + fd.Close() } // Verify signature on a given file @@ -323,14 +335,8 @@ Commands: } // Return true if $bn.key or $bn.pub exist; false otherwise -func exists(bn string) bool { - pk := bn + ".pub" - sk := bn + ".key" - - if _, err := os.Stat(pk); err == nil { - return true - } - if _, err := os.Stat(sk); err == nil { +func exists(nm string) bool { + if _, err := os.Stat(nm); err == nil { return true } diff --git a/tests.sh b/tests.sh new file mode 100644 index 0000000..7e71705 --- /dev/null +++ b/tests.sh @@ -0,0 +1,67 @@ +#! /usr/bin/env bash + + +# simple round-trip tests to verify the tool + +arch=`./build --print-arch` +bin=./bin/$arch/sigtool +Z=`basename $0` + +die() { + echo "$Z: $@" 1>&2 + exit 1 +} + + +[ -x $bin ] || ./build || die "Can't build sigtool for $arch" + +# env name for reading the password +passenv=FOO + +# this is the password for SKs +FOO=bar + +# basename of keyfile +tmpdir=/tmp/sigtool$$ +mkdir -p $tmpdir || die "can't mkdir $tmpdir" + +#trap "rm -rf $tmpdir" EXIT + +bn=$tmpdir/foo +pk=$bn.pub +sk=$bn.key +sig=$tmpdir/$Z.sig +bn2=$tmpdir/bar +pk2=$bn2.pub +sk2=$bn2.key + +encout=$tmpdir/$Z.enc +decout=$tmpdir/$Z.dec + +# exit on any failure +set -e + +# generate keys +$bin g -E FOO $bn || die "can't gen keypair $pk, $sk" +$bin g -E FOO $bn && die "overwrote prev keypair" +$bin g -E FOO --overwrite $bn || die "can't force gen keypair $pk, $sk" +$bin g -E FOO $bn2 || die "can't force gen keypair $pk2, $sk2" + +# sign and verify +$bin s -E FOO $sk $0 -o $sig || die "can't sign $0" +$bin v -q $pk $sig $0 || die "can't verify signature of $0" +$bin v -q $pk2 $sig $0 && die "bad verification with wrong $pk2" + +# encrypt/decrypt +$bin e -E FOO -o $encout $pk2 $0 || die "can't encrypt to $pk2" +$bin d -E FOO -o $decout $sk2 $encout || die "can't decrypt with $sk2" +cmp -s $decout $0 || die "decrypted file mismatch with $0" + +# now with sender verification +$bin e -E FOO --overwrite -o $encout -s $sk $pk2 $0 || die "can't sender-encrypt to $pk2" +$bin d -E FOO --overwrite -o $decout -v $pk $sk2 $encout || die "can't decrypt with $sk2" +cmp -s $decout $0 || die "decrypted file mismatch with $0" + + + +# vim: tw=100 sw=4 ts=4 expandtab