diff --git a/.gitignore b/.gitignore index 9caec6b..713b439 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,6 @@ vendor/* # vendor management vendor/src/* vendor/pkg/* -src/* bin/* .??*.sw? diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..24a55ff --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ + +pwd = $(shell pwd) +GOPATH := $(pwd)/vendor:$(pwd) +export GOPATH + +all: + mkdir -p bin + go get -d . + go build -o bin/sigtool . + +test: + go test sign +clean: + rm -f bin/sigtool + +realclean: clean + rm -rf vendor diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8a0fa3 --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +[![GoDoc](https://godoc.org/github.com/opencoff/go-sign?status.svg)](https://godoc.org/github.com/opencoff/go-sign) + +# README for sigtool + + +## What is this? +`sigtool` is an opinionated tool to generate, sign and verify Ed25519 +signatures on files. In many ways, it is like like OpenBSD's signify_ +-- except written in Golang and definitely easier to use. + +It can sign and verify very large files - it prehashes the files +with SHA-512 and then signs the SHA-512 checksum. + +All the artifacts produced by this tool are standard YAML files - +thus, human readable. + +## How do I build it? +With Go 1.5 and later: + + git clone https://github.com/opencoff/sigtool + cd sigtool + make + +The binary will be in `./sigtool`. + +## How do I use it? +Broadly, the tool can: + +- generate new key pairs (public key and private key) +- sign a file +- verify a file against its signature + +### Generate Key pair +To start with, you generate a new key pair (a public key used for +verification and a private key used for signing). e.g., + + sigtool gen /tmp/testkey + +The tool then generates */tmp/testkey.pub* and */tmp/testkey.key*. The secret +key (".key") can optionally be encrypted with a user supplied pass +phrase - which the user has to enter via interactive prompt: + + sigtool gen -p /tmp/testkey + +### Sign a file +Signing a file requires the user to provide a previously generated +Ed25519 private key. The signature (YAML) is written to STDOUT. +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: + + sigtool sign -p /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 + + +### Verify a signature against a file +Verifying a signature of a file requires the user to supply three +pieces of information: + +- the Ed25519 public key to be used for verification +- the Ed25519 signature +- the file whose signature must be verified + +e.g., to verify the signature of *archive.tar.gz* against +*testkey.pub* using the signature *archive.sig* + + sigtool verify /tmp/testkey.pub archive.sig archive.tar.gz + +## How is the private key protected? +The Ed25519 private key is encrypted using a key derived from the +user supplied pass phrase. This pass phrase is used to derive an +encryption key using the Scrypt key derivation algorithm. The +resulting derived key is XOR'd with the Ed25519 private key before +being committed to disk. To protect the integrity of the process, +the essential parameters used for deriving the key, and the derived +key are hashed via SHA256 and stored along with the encrypted key. + +As an additional security measure, the user supplied pass phrase is +hashed with SHA512. + +## Understanding the Code +`src/sign` is a library to generate, verify and store Ed25519 keys +and signatures. It uses the extended library (golang.org/x/crypto) +for the underlying operations. + +The generated keys and signatures are proper YAML files and human +readable. + +The signature file contains a hash of the public key - so that at +verification time, the right private key may be used (in situations +where there are lots of keys). + +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. + +## Example of Keys, Signature + +### Ed25519 Public Key +A serialized Ed25519 public key looks like so: + + pk: uxpDh+gqXojAmxA/6vxZHzA+Uk+8wogUwvEhPBlWgvo= + +### Ed25519 Private Key +And, a serialized Ed25519 private key looks like so: + + esk: t3vfqHbgUiA733KKPymFjWT8DdnBEkiMfsDHolPUdQWpvVn/F1Z4J6KYV3M5rGO9xgKxh5RAmqt+6LKgOiJAMQ== + salt: pPHKG55UJYtJ5wU0G9hBvNQJ0DvT0a7T4Fmj4aPB84s= + algo: scrypt-sha256 + verify: JvjRjJMKhJhBmZngC3Pvq7x3KCLKt7gar1AAz7HB4qM= + Z: 131072 + r: 16 + p: 1 + +The Ed25519 private key is encrypted using Scrypt password hashing +mechanism. A user supplied passphrase to protect the private key +is first pre-hashed using SHA-512 before being used in +```scrypt()```. In pseudo code, this operation looks like below: + + passphrase = get_user_passphrase() + hpass = SHA512(passphrase) + salt = randombytes(32) + xorkey = Scrypt(hpass, salt, N, r, p) + verify = SHA256(salt, xorkey) + esk = ed25519_private_key ^ xorkey + +Where, ```N```, ```r```, ```p``` are Scrypt parameters. In our +implementation: + + N = 131072 + r = 16 + p = 1 + +```verify``` is used during the decryption of the Ed25519 private +key - *before* actually doing the "xor" operation. This check +ensures that the supplied passphrase yields the same value as +```verify```. + +### Ed25519 Signature +A generated signature looks like below after serialization: + + comment: inpfile=/tmp/file.txt + pkhash: 36z9tCwTIVNwwDlExrB0SQ== + signature: ow2oBP+buDbEvlNakOrsxgB5Yc/7PYyPVZCkfyu7oahw8BakF4Qf32uswPaKGZ8RVz4uXboYHdZtfrEjCgP/Cg== + +Here, ```pkhash`` is a SHA256 of the public key needed to verify +this signature. + +## Licensing Terms +The tool and code is licensed under the terms of the +GNU Public License v2.0 (strictly v2.0). If you need a commercial +license or a different license, please get in touch with me. + +See the file ``LICENSE.md`` for the full terms of the license. + +## Author +Sudhi Herle + +.. _signify: https://www.openbsd.org/papers/bsdcan-signify.html diff --git a/README.rst b/README.rst deleted file mode 100644 index e734955..0000000 --- a/README.rst +++ /dev/null @@ -1,137 +0,0 @@ -================== -README for sigtool -================== - - -What is this? -============= -This is a tool to generate, sign and verify Ed25519 signatures. In -many ways, it is like like OpenBSD's signify_ -- except written in Golang -and designed to be easier to use. - -It can sign and verify very large files - it prehashes the files -with SHA-512 and then signs the SHA-512 checksum. - -All the artifacts produced by this tool are standard YAML files - -thus, human readable. - -How do I build it? -================== -With Go 1.5 and later:: - - mkdir sigtool - cd sigtool - env GOPATH=`pwd` go get -u github.com/opencoff/sigtool - -The binary will be in ``bin/sigtool``. - -How do I use it? -================ -Broadly, the tool can: - -- generate new key pairs (public key and private key) -- sign a file -- verify a file against its signature - -Generate Key pair ------------------ -To start with, you generate a new key pair (a public key used for -verification and a private key used for signing). e.g., :: - - sigtool gen /tmp/testkey - -The tool then generates */tmp/testkey.pub* and */tmp/testkey.key*. The secret -key (".key") can optionally be encrypted with a user supplied pass -phrase - which the user has to enter via interactive prompt:: - - sigtool gen -p /tmp/testkey - -Sign a file ------------ -Signing a file requires the user to provide a previously generated -Ed25519 private key. The signature (YAML) is written to STDOUT. -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:: - - sigtool sign -p /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 - - -Verify a signature against a file ---------------------------------- -Verifying a signature of a file requires the user to supply three -pieces of information: - -- the Ed25519 public key to be used for verification -- the Ed25519 signature -- the file whose signature must be verified - -e.g., to verify the signature of *archive.tar.gz* against -*testkey.pub* using the signature *archive.sig*:: - - sigtool verify /tmp/testkey.pub archive.sig archive.tar.gz - -How is the private key protected? -================================= -The Ed25519 private key is encrypted using a key derived from the -user supplied pass phrase. This pass phrase is used to derive an -encryption key using the Scrypt key derivation algorithm. The -resulting derived key is XOR'd with the Ed25519 private key before -being committed to disk. To protect the integrity of the process, -the essential parameters used for deriving the key, and the derived -key are hashed via SHA256 and stored along with the encrypted key. - -As an additional security measure, the user supplied pass phrase is -hashed with SHA512. - -In Pseudo-code:: - - passwd = get_user_password() - hpass = SHA512(passwd) - salt = randombytes(32) - xorkey = Scrypt(hpass, salt, N, r, p) - - cksum = SHA256(salt, xorkey) - enckey = ed25519_private_key ^ xorkey - -And, ``cksum``, ``enckey`` are the entities stored as on-disk -private key. - -The Scrypt parameters used by the ``sign`` library are: - -- N: 131072 -- r: 16 -- p: 1 - -Understanding the Code -====================== -The tool uses a companion library that manages the keys and -signatures. It is part of a growing set of Golang libraries that are -useful in multiple projects. You can find them on github_. - -The core code is in the ``sign`` library. This library is -can be reused in any of your projects. - -.. _github: https://github.com/opencoff/go-sign/ - -Licensing Terms -=============== -The tool is licensed under the terms of the GNU Public License v2.0 -(strictly v2.0). If you need a commercial license or a different -license, please get in touch with me. - -See the file ``LICENSE.md`` for the full terms of the license. - -Author -====== -Sudhi Herle - -.. _signify: https://www.openbsd.org/papers/bsdcan-signify.html diff --git a/sigtool.go b/sigtool.go index 54e206e..b20c2b6 100644 --- a/sigtool.go +++ b/sigtool.go @@ -21,8 +21,10 @@ import ( "path" flag "github.com/ogier/pflag" - "github.com/opencoff/go-sign" "github.com/opencoff/go-utils" + + // My signing library + "sign" ) // This will be filled in by "build" diff --git a/src/sign/.gitignore b/src/sign/.gitignore new file mode 100644 index 0000000..a743dc2 --- /dev/null +++ b/src/sign/.gitignore @@ -0,0 +1,46 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +bin/* +.*.sw? +.idea +logs/* + +# gg ignores +vendor/src/* +vendor/pkg/* +servers.iml +*.DS_Store + +# vagrant ignores +tools/vagrant/.vagrant +tools/vagrant/adsrv-conf/.frontend +tools/vagrant/adsrv-conf/.bidder +tools/vagrant/adsrv-conf/.transcoder +tools/vagrant/redis-cluster-conf/7777/nodes.conf +tools/vagrant/redis-cluster-conf/7778/nodes.conf +tools/vagrant/redis-cluster-conf/7779/nodes.conf +*.aof +*.rdb diff --git a/src/sign/sign.go b/src/sign/sign.go new file mode 100644 index 0000000..ab64c2d --- /dev/null +++ b/src/sign/sign.go @@ -0,0 +1,541 @@ +// sign.go -- Ed25519 keys and signature handling +// +// (c) 2016 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 implements Ed25519 signing, verification on files. +// It builds upon golang.org/x/crypto/ed25519 by adding methods +// for serializing and deserializing Ed25519 private & public keys. +// In addition, it works with large files - by precalculating their +// SHA512 checksum in mmap'd mode and sending the 64 byte signature +// for Ed25519 signing. +package sign + +import ( + "crypto" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "crypto/subtle" + "encoding/base64" + "encoding/binary" + "fmt" + "hash" + "io/ioutil" + "os" + + Ed "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/scrypt" + "gopkg.in/yaml.v2" + + "github.com/opencoff/go-utils" +) + +// Private Ed25519 key +type PrivateKey struct { + Sk []byte + + // Cached copy of the public key + // In reality, it is a pointer to Sk[32:] + pk []byte +} + +// Public Ed25519 key +type PublicKey struct { + Pk []byte +} + +// Ed25519 key pair +type Keypair struct { + Sec PrivateKey + Pub PublicKey +} + +// An Ed25519 Signature +type Signature struct { + Sig []byte // 32 byte digital signature + pkhash []byte // [0:16] SHA256 hash of public key needed for verification +} + +// Algorithm used in the encrypted private key +const sk_algo = "scrypt-sha256" +const sig_algo = "sha512-ed25519" + +// Scrypt parameters +const _N = 1 << 17 +const _r = 16 +const _p = 1 + +// Encrypted Private key +type encPrivKey struct { + // Encrypted Sk + Esk []byte + + // parameters for Sk serialization + Salt []byte + + // Algorithm used for checksum and KDF + Algo string + + // Checksum to verify passphrase before we xor it + Verify []byte + + // These are params for scrypt.Key() + // CPU Cost parameter; must be a power of 2 + N uint32 + // r * p should be less than 2^30 + r uint32 + p uint32 +} + +// Serialized representation of private key +type serializedPrivKey struct { + Comment string `yaml:"comment,omitempty"` + Esk string `yaml:"esk"` + Salt string `yaml:"salt,omitempty"` + Algo string `yaml:"algo,omitempty"` + Verify string `yaml:"verify,omitempty"` + N uint32 `yaml:"Z,flow,omitempty"` + R uint32 `yaml:"r,flow,omitempty"` + P uint32 `yaml:"p,flow,omitempty"` +} + +// serialized representation of public key +type serializedPubKey struct { + Comment string `yaml:"comment,omitempty"` + Pk string `yaml:"pk"` +} + +// Serialized signature +type signature struct { + Comment string `yaml:"comment,omitempty"` + Pkhash string `yaml:"pkhash,omitempty"` + Signature string `yaml:"signature"` +} + +// 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 + + p, s, err := Ed.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("Can't generate Ed25519 keys: %s", err) + } + + pk.Pk = []byte(p) + sk.Sk = []byte(s) + + 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, pw string) 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) + } + + err = sk.serialize(skf, comment, pw) + if err != nil { + return fmt.Errorf("Can't serialize to %s: %s", pkf, err) + } + + return nil +} + +// 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) { + yml, err := ioutil.ReadFile(fn) + if err != nil { + return nil, err + } + + return MakePrivateKey(yml, pw) +} + +// 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) { + var ssk serializedPrivKey + + err := yaml.Unmarshal(yml, &ssk) + if err != nil { + return nil, fmt.Errorf("can't parse YAML: %s", err) + } + + esk := &encPrivKey{N: ssk.N, r: ssk.R, p: ssk.P, Algo: ssk.Algo} + b64 := base64.StdEncoding.DecodeString + + esk.Esk, err = b64(ssk.Esk) + if err != nil { + return nil, fmt.Errorf("can't decode YAML:Esk: %s", err) + } + + esk.Salt, err = b64(ssk.Salt) + if err != nil { + return nil, fmt.Errorf("can't decode YAML:Salt: %s", err) + } + + esk.Verify, err = b64(ssk.Verify) + if err != nil { + return nil, fmt.Errorf("can't decode YAML:Verify: %s", err) + } + + sk := &PrivateKey{} + + // We take short passwords and extend them + pwb := sha512.Sum512([]byte(pw)) + + xork, err := scrypt.Key(pwb[:], esk.Salt, int(esk.N), int(esk.r), int(esk.p), len(esk.Esk)) + if err != nil { + return nil, fmt.Errorf("can't derive key: %s", err) + } + + hh := sha256.New() + hh.Write(esk.Salt) + hh.Write(xork) + ck := hh.Sum(nil) + + if subtle.ConstantTimeCompare(esk.Verify, ck) != 1 { + return nil, fmt.Errorf("incorrect private key password") + } + + // Everything works. Now, decode the key + sk.Sk = make([]byte, len(esk.Esk)) + for i := 0; i < len(esk.Esk); i++ { + sk.Sk[i] = esk.Esk[i] ^ xork[i] + } + + return sk, nil +} + +// 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 { + + 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)) + + esk.N = _N + esk.r = _r + esk.p = _p + + esk.Salt = make([]byte, 32) + esk.Esk = make([]byte, len(sk.Sk)) + + _, err := rand.Read(esk.Salt) + if err != nil { + return fmt.Errorf("Can't read random salt: %s", err) + } + + xork, err := scrypt.Key(pwb[:], esk.Salt, int(esk.N), int(esk.r), int(esk.p), len(sk.Sk)) + if err != nil { + return fmt.Errorf("Can't derive scrypt key: %s", err) + } + + hh := sha256.New() + hh.Write(esk.Salt) + hh.Write(xork) + esk.Verify = hh.Sum(nil) + + // 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. + + esk.Algo = sk_algo // global var + + // Finally setup the encrypted key + for i := 0; i < len(sk.Sk); i++ { + esk.Esk[i] = sk.Sk[i] ^ xork[i] + } + + ssk.Esk = b64(esk.Esk) + ssk.Salt = b64(esk.Salt) + ssk.Verify = b64(esk.Verify) + ssk.Algo = esk.Algo + ssk.N = esk.N + ssk.R = esk.r + ssk.P = esk.p + + out, err := yaml.Marshal(ssk) + if err != nil { + return fmt.Errorf("can't marahal to YAML: %s", err) + } + + return writeFile(fn, out, 0600) +} + +// Sign a prehashed Message; return the signature as opaque bytes +// Signature is an YAML file: +// Comment: source file path +// Signature: Ed25519 signature +func (sk *PrivateKey) SignMessage(ck []byte, comment string) (*Signature, error) { + x := Ed.PrivateKey(sk.Sk) + + sig, err := x.Sign(rand.Reader, ck, crypto.Hash(0)) + if err != nil { + return nil, fmt.Errorf("can't sign %x: %s", ck, err) + } + + esk := Ed.PrivateKey(sk.Sk) // type cast + epk := esk.Public() // interface + xpk := epk.(Ed.PublicKey) // type assertion + pk := []byte(xpk) // cast + pkh := sha256.Sum256(pk) + + return &Signature{Sig: sig, pkhash: pkh[:16]}, nil +} + +// Read and sign a file +// +// We calculate the signature differently here: We first calculate +// the SHA-512 checksum of the file and its size. We sign the +// checksum. +func (sk *PrivateKey) SignFile(fn string) (*Signature, error) { + + ck, err := fileCksum(fn, sha512.New()) + if err != nil { + return nil, err + } + + return sk.SignMessage(ck, fn) +} + +// -- Signature Methods -- + +// Read serialized signature from file 'fn' and construct a +// Signature object +func ReadSignature(fn string) (*Signature, error) { + yml, err := ioutil.ReadFile(fn) + if err != nil { + return nil, err + } + + return MakeSignature(yml) +} + +// Parse serialized signature from bytes 'b' and construct a +// Signature object +func MakeSignature(b []byte) (*Signature, error) { + var ss signature + err := yaml.Unmarshal(b, &ss) + if err != nil { + return nil, fmt.Errorf("can't parse YAML signature: %s", err) + } + + b64 := base64.StdEncoding.DecodeString + + s, err := b64(ss.Signature) + if err != nil { + return nil, fmt.Errorf("can't decode Base64:Signature <%s>: %s", ss.Signature, err) + } + + p, err := b64(ss.Pkhash) + if err != nil { + return nil, fmt.Errorf("can't decode Base64:Pkhash <%s>: %s", ss.Pkhash, err) + } + + return &Signature{Sig: s, pkhash: p}, nil +} + +// Serialize a signature suitable for storing in durable media +func (sig *Signature) Serialize(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 +} + +// SerializeFile serializes the signature to an output file 'f' +func (sig *Signature) SerializeFile(fn, comment string) error { + b, err := sig.Serialize(comment) + if err == nil { + err = writeFile(fn, b, 0644) + } + return err +} + +// IsPKMatch returns true if public key 'pk' can potentially validate +// the signature. It does this by comparing the hash of 'pk' against +// 'Pkhash' of 'sig'. +func (sig *Signature) IsPKMatch(pk *PublicKey) bool { + h := sha256.Sum256(pk.Pk) + return subtle.ConstantTimeCompare(h[:16], sig.pkhash) == 1 +} + +// --- 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 + } + + return MakePublicKey(yml) +} + +// 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) + } + + pk := &PublicKey{} + b64 := base64.StdEncoding.DecodeString + + if pk.Pk, err = b64(spk.Pk); err != nil { + return nil, fmt.Errorf("can't decode YAML:Pk: %s", err) + } + + // Simple sanity checks + if len(pk.Pk) == 0 { + return nil, fmt.Errorf("public key data is empty?") + } + + return pk, nil +} + +// Serialize Public Keys +func (pk *PublicKey) serialize(fn, comment string) error { + b64 := base64.StdEncoding.EncodeToString + spk := &serializedPubKey{Comment: comment} + + spk.Pk = b64(pk.Pk) + + out, err := yaml.Marshal(spk) + if err != nil { + return fmt.Errorf("Can't marahal to YAML: %s", err) + } + + return writeFile(fn, out, 0644) +} + +// 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 + } + + return pk.VerifyMessage(ck, sig) +} + +// Verify a signature 'sig' for a pre-calculated checksum 'ck' against public key 'pk' +// Return True if signature matches, False otherwise +func (pk *PublicKey) VerifyMessage(ck []byte, sig *Signature) (bool, error) { + + x := Ed.PublicKey(pk.Pk) + return Ed.Verify(x, ck, sig.Sig), nil +} + +// -- 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)) + if err != nil { + return fmt.Errorf("Can't create file %s: %s", tmp, err) + } + + _, 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 +} + +// 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) + } + + defer fd.Close() + + sz, err := utils.MmapReader(fd, 0, 0, h) + if err != nil { + return nil, err + } + + var b [8]byte + binary.BigEndian.PutUint64(b[:], uint64(sz)) + h.Write(b[:]) + + return h.Sum(nil), nil +} + +// EOF +// vim: noexpandtab:ts=8:sw=8:tw=92: diff --git a/src/sign/sign_test.go b/src/sign/sign_test.go new file mode 100644 index 0000000..524824f --- /dev/null +++ b/src/sign/sign_test.go @@ -0,0 +1,310 @@ +// sign_test.go -- Test harness for sign +// +// (c) 2016 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" + "crypto/subtle" + "fmt" + "io/ioutil" + "os" + "path" + "runtime" + "testing" + // module under test + //"github.com/sign" +) + +func newAsserter(t *testing.T) func(cond bool, msg string, args ...interface{}) { + return func(cond bool, msg string, args ...interface{}) { + if cond { + return + } + + _, file, line, ok := runtime.Caller(1) + if !ok { + file = "???" + line = 0 + } + + s := fmt.Sprintf(msg, args...) + t.Fatalf("%s: %d: Assertion failed: %s\n", file, line, s) + } +} + +// Return true if two byte arrays are equal +func byteEq(x, y []byte) bool { + return subtle.ConstantTimeCompare(x, y) == 1 +} + +// Return a temp dir in a temp-dir +func tempdir(t *testing.T) string { + assert := newAsserter(t) + + var b [10]byte + + dn := os.TempDir() + rand.Read(b[:]) + + tmp := path.Join(dn, fmt.Sprintf("%x", b[:])) + err := os.MkdirAll(tmp, 0755) + assert(err == nil, fmt.Sprintf("mkdir -p %s: %s", tmp, err)) + + //t.Logf("Tempdir is %s", tmp) + return tmp +} + +// Return true if file exists, false otherwise +func fileExists(fn string) bool { + st, err := os.Stat(fn) + if err != nil { + if os.IsNotExist(err) { + return false + } + return false + } + + if st.Mode().IsRegular() { + return true + } + return false +} + +const badsk string = ` +esk: q8AP3/6C5F0zB8CLiuJsidx2gJYmrnyOmuoazEbKL5Uh+Jn/Zgw85fTbYfhjcbt48CJejBzsgPYRYR7wWECFRA== +salt: uIdTQZotfnkaLkth9jsHvoQKMWdNZuE7dgVNADrRoeY= +algo: scrypt-sha256 +verify: AOFLLC6h29+mvstWtMU1/zZFwHLBMMiI4mlW9DHpYdM= +Z: 65536 +r: 8 +p: 1 +` + +// #1. Create new key pair, and read them back. +func Test0(t *testing.T) { + assert := newAsserter(t) + + kp, err := NewKeypair() + assert(err == nil, "NewKeyPair() fail") + + dn := tempdir(t) + bn := fmt.Sprintf("%s/t0", dn) + + err = kp.Serialize(bn, "", "abc") + 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") + + // send wrong file and see what happens + pk, err := ReadPublicKey(skf) + assert(err != nil, "bad PK ReadPK fail") + + pk, err = ReadPublicKey(pkf) + assert(err == nil, "ReadPK() fail") + + // -ditto- for Sk + sk, err := ReadPrivateKey(pkf, "") + assert(err != nil, "bad SK ReadSK fail") + + sk, err = ReadPrivateKey(skf, "") + assert(err != nil, "ReadSK() empty pw fail") + + sk, err = ReadPrivateKey(skf, "abcdef") + assert(err != nil, "ReadSK() wrong pw fail") + + badf := fmt.Sprintf("%s/badf.key", dn) + err = ioutil.WriteFile(badf, []byte(badsk), 0600) + assert(err == nil, "write badsk") + + sk, err = ReadPrivateKey(badf, "abc") + assert(err != nil, "badsk read fail") + + // Finally, with correct password it should work. + sk, err = ReadPrivateKey(skf, "abc") + assert(err == nil, "ReadSK() correct pw fail") + + // 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) +} + +// #2. Create new key pair, sign a rand buffer and verify +func Test1(t *testing.T) { + assert := newAsserter(t) + kp, err := NewKeypair() + assert(err == nil, "NewKeyPair() fail") + + var ck [64]byte // simulates sha512 sum + + rand.Read(ck[:]) + + pk := &kp.Pub + sk := &kp.Sec + + ss, err := sk.SignMessage(ck[:], "") + assert(err == nil, "sk.sign fail") + assert(ss != nil, "sig is null") + + // verify sig + assert(ss.IsPKMatch(pk), "pk match fail") + + // Corrupt the pkhash and see + rand.Read(ss.pkhash[:]) + assert(!ss.IsPKMatch(pk), "corrupt pk match fail") + + // Incorrect checksum == should fail verification + ok, err := pk.VerifyMessage(ck[:16], ss) + assert(err == nil, "bad ck verify err fail") + assert(!ok, "bad ck verify fail") + + // proper checksum == should work + ok, err = pk.VerifyMessage(ck[:], ss) + assert(err == nil, "verify err") + 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, "", "") + assert(err == nil, "keyPair.Serialize() fail") + + // Now read the private key and sign + sk, err = ReadPrivateKey(skf, "") + assert(err == nil, "readSK fail") + + pk, err = ReadPublicKey(pkf) + assert(err == nil, "ReadPK fail") + + 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") + + for i := 0; i < 8; i++ { + rand.Read(buf[:]) + n, err := fd.Write(buf[:]) + assert(err == nil, fmt.Sprintf("file.dat write fail: %s", err)) + assert(n == 8192, fmt.Sprintf("file.dat i/o fail: exp 8192 saw %v", n)) + } + fd.Sync() + fd.Close() + + sig, err := sk.SignFile(zf) + assert(err == nil, "file.dat sign fail") + assert(sig != nil, "file.dat sign nil") + + ok, err = pk.VerifyFile(zf, sig) + assert(err == nil, "file.dat verify fail") + 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") + + s2, err := ReadSignature(sf) + assert(err == nil, "file.sig read fail") + 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") + + n := st.Size() + assert(n == 8192*8, "file.dat size fail") + + os.Truncate(zf, n-1) + + st, err = os.Stat(zf) + assert(err == nil, "file.dat stat2 fail") + 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(!ok, "file.dat corrupt verify false") + + os.RemoveAll(dn) +} + + +func Benchmark_Keygen(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = NewKeypair() + } +} + + +func Benchmark_Sig(b *testing.B) { + var sizes = [...]uint{ + 16, + 32, + 64, + } + + b.StopTimer() + kp, _ := NewKeypair() + var sig *Signature + for _, sz := range sizes { + buf := randbuf(sz) + s0 := fmt.Sprintf("%d byte sign", sz) + s1 := fmt.Sprintf("%d byte verify", sz) + + b.ResetTimer() + + b.Run(s0, func (b *testing.B) { + sig = benchSign(b, buf, &kp.Sec) + }) + + b.Run(s1, func (b *testing.B) { + benchVerify(b, buf, sig, &kp.Pub) + }) + } +} + +func benchSign(b *testing.B, buf []byte, sk *PrivateKey) (sig *Signature) { + for i := 0; i < b.N; i++ { + sig, _ = sk.SignMessage(buf, "") + } + return sig +} + +func benchVerify(b *testing.B, buf []byte, sig *Signature, pk *PublicKey) { + for i := 0; i < b.N; i++ { + pk.VerifyMessage(buf, sig) + } +} + + +func randbuf(sz uint) []byte { + b := make([]byte, sz) + rand.Read(b) + return b +} + +// vim: noexpandtab:ts=8:sw=8:tw=92: