From a27044154aae267f6034c8ff9450c3993b3b6b1d Mon Sep 17 00:00:00 2001 From: Sudhi Herle Date: Fri, 18 Oct 2019 15:42:08 -0700 Subject: [PATCH] Working version with enc/dec of all key types. * Updated README * fix non-ephemeral key wrap/unwrap * fix out of bounds error in decrypt --- README.md | 129 ++++++++++++++++++++++++++++++++---------------- build | 2 +- crypt.go | 4 +- sign/encrypt.go | 115 +++++++++++++++++++++--------------------- sigtool.go | 10 ++-- 5 files changed, 152 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 5921fef..ebd56f2 100644 --- a/README.md +++ b/README.md @@ -13,45 +13,14 @@ It can sign and verify very large files - it prehashes the files with SHA-512 and then signs the SHA-512 checksum. The keys and signatures are YAML files and so, human readable. -It can encrypt & decrypt files by converting the Ed25519 keys to their -corresponding Curve25519 variants. This elliptic co-ordinate transform -follows [FiloSottile's writeup][2]. The file encryption uses -AES-GCM-256 (AEAD); the input is broken into chunks and each chunk is -AEAD encrypted. The default chunk size is 4MB (4 * 1048576 bytes). +It can encrypt files for multiple recipients - each of whom is identified +by their Ed25519 public key. The encryption by default generates ephmeral +Curve25519 keys and creates pair-wise shared secret for each recipient of +the encrypted file. The caller can optionally use a specific secret key +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). -A random 32-byte key is used to actually encrypt the file contents in -AES-GCM mode. This file-encryption key is **wrapped** using the recipient's -public key. Thus, a given input file (or stream) can be encrypted to be -read by multiple recipients - each of whom is identified by their Ed25519 -public keys. The file-encryptionb-key can optionally be wrapped using the -sender's Private Key - this authenticates the sender. If this private key is -not provided for the encrypt operation, then `sigtool` generates ephemeral -Curve25519 keys and wraps the file-encryption key using the ephemeral -private key and the recipient's public key. - -Every encrypted file starts with a header: - - 7 byte magic ("SigTool") - 1 byte version number - 4 byte header length - 32 byte SHA256 of the encryption-header - -The encryption-header is described as a protobuf file (sign/hdr.proto): - -```protobuf - message header { - uint32 chunk_size = 1; - bytes salt = 2; - repeated wrapped_key keys = 3; - } - - message wrapped_key { - bytes pk_hash = 1; // hash of Ed25519 PK - bytes pk = 2; // curve25519 PK - bytes nonce = 3; // AEAD nonce - bytes key = 4; // AEAD encrypted key - } -``` ## How do I build it? With Go 1.5 and later: @@ -120,19 +89,35 @@ e.g., to verify the signature of *archive.tar.gz* against If the sender wishes to prove to the recipient that they encrypted a file: - sigtool encrypt -s mykey.key theirkey.pub -o archive.tar.gz.enc archive.tar.gz + sigtool encrypt -s sender.key to.pub -o archive.tar.gz.enc archive.tar.gz This will create an encrypted file *archive.tar.gz.enc* such that the -recipient in possession of *theikey.key* can decrypt it. Furthermore, if -the recipient has *mykey.pub*, they can verify that the sender is indeed +recipient in possession of *to.key* can decrypt it. Furthermore, if +the recipient has *sender.pub*, they can verify that the sender is indeed who they expect. +### Decrypt a file and verify the sender +If the receiver has the public key of the sender, they can verify that +they indeed sent the file by cryptographically checking the output: + + sigtool decrypt -o archive.tar.gz -v sender.pub to.key archive.tar.gz.enc + +Note that the verification is optional and if the `-v` option is not +used, then decryption will proceed without verifying the sender. + ### Encrypt a file *without* authenticating the sender +`sigtool` can generate ephemeral keys for encrypting a file such that +the receiver doesn't need to authenticate the sender: -### Decrypt a file + sigtool encrypt to.pub -o archive.tar.gz.enc archive.tar.gz -## How is the private key protected? +This will create an encrypted file *archive.tar.gz.enc* such that the +recipient in possession of *to.key* can decrypt it. + +## Technical Details + +### 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 @@ -144,11 +129,69 @@ 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. +### How is the Encryption done? +The file encryption uses AES-GCM-256 in AEAD mode. The encryption uses +a random 32-byte AES-256 key. The input is broken into chunks and +each chunk is individually AEAD encrypted. The default chunk size +is 4MB (4 * 1048576 bytes). Each chunk generates its own nonce +from a global salt. The nonce is calculated as a SHA256 hash of +the salt, the chunk length and the block number. + +### What is the public-key cryptography used? +`sigtool` uses Curve25519 ECC to generate shared secrets between +pairs of sender & recipients. This pairwise shared secret is expanded +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 +corresponding Curve25519 points in order to generate the shared secret. +This elliptic co-ordinate transform follows [FiloSottile's writeup][2]. + +### Format of the Encrypted File +Every encrypted file starts with a header: + + 7 byte magic ("SigTool") + 1 byte version number + 4 byte header length (big endian encoding) + 32 byte SHA256 of the encryption-header + +The encryption-header is described as a protobuf file (sign/hdr.proto): + +```protobuf + message header { + uint32 chunk_size = 1; + bytes salt = 2; + repeated wrapped_key keys = 3; + } + + message wrapped_key { + bytes pk_hash = 1; // hash of Ed25519 PK + bytes pk = 2; // curve25519 PK + bytes nonce = 3; // AEAD nonce + bytes key = 4; // AEAD encrypted key + } +``` + +The encrypted data immediately follows the headers above. Each encrypted +chunk is encoded the same way: + +```C + 4 byte chunk length (big endian encoding) + chunk data + AEAD tag +``` + +The chunk data and AEAD tag are treated as an atomic unit for AEAD +decryption. + ## 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. +`src/crypt.go` contains the encryption & decryption code. + The generated keys and signatures are proper YAML files and human readable. diff --git a/build b/build index 8869d0a..232e01e 100755 --- a/build +++ b/build @@ -354,7 +354,7 @@ case $Tool in all="$@" fi - echo "Building $rev, $cross $msg .." + echo "Building $msg $Prodver ($rev) for $cross .." for p in $all; do if echo $p | grep -q ':' ; then diff --git a/crypt.go b/crypt.go index a12612a..e02376a 100644 --- a/crypt.go +++ b/crypt.go @@ -221,12 +221,12 @@ func decrypt(args []string) { } - d, err := sign.NewDecryptor(infd, pk) + d, err := sign.NewDecryptor(infd) if err != nil { die("%s", err) } - err = d.SetPrivateKey(sk) + err = d.SetPrivateKey(sk, pk) if err != nil { die("%s", err) } diff --git a/sign/encrypt.go b/sign/encrypt.go index a5877c6..454200a 100644 --- a/sign/encrypt.go +++ b/sign/encrypt.go @@ -241,7 +241,7 @@ type Decryptor struct { // Create a new decryption context and if 'pk' is given, check that it matches // the sender -func NewDecryptor(rd io.Reader, pk *PublicKey) (*Decryptor, error) { +func NewDecryptor(rd io.Reader) (*Decryptor, error) { var b [12]byte _, err := io.ReadFull(rd, b[:]) @@ -322,32 +322,18 @@ func NewDecryptor(rd io.Reader, pk *PublicKey) (*Decryptor, error) { } - d.buf = make([]byte, d.ChunkSize) - if pk != nil { - validSender := false - pkh := pk.Hash() - for _, w := range d.Keys { - if subtle.ConstantTimeCompare(pkh, w.PkHash) == 1 { - validSender = true - } - } - - if !validSender { - return nil, fmt.Errorf("decrypt: Can't find sender's public key in the header") - } - } - return d, nil } -// Use Private Key 'sk' to decrypt the encrypted keys in the header -func (d *Decryptor) SetPrivateKey(sk *PrivateKey) error { +// Use Private Key 'sk' to decrypt the encrypted keys in the header and optionally validate +// the sender +func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error { var err error pkh := sk.PublicKey().Hash() for i, w := range d.Keys { if subtle.ConstantTimeCompare(pkh, w.PkHash) == 1 { - d.key, err = w.UnwrapKey(sk) + d.key, err = w.UnwrapKey(sk, senderPk) if err != nil { return fmt.Errorf("decrypt: can't unwrap key %d: %s", i, err) } @@ -368,6 +354,7 @@ havekey: if err != nil { return fmt.Errorf("decrypt: %s", err) } + d.buf = make([]byte, int(d.ChunkSize) + d.ae.Overhead()) return nil } @@ -440,29 +427,73 @@ func (d *Decryptor) decrypt(i int) ([]byte, error) { return p, nil } +// Wrap a shared key with the recipient's public key 'pk' by generating an ephemeral +// Curve25519 keypair. This function does not identify the sender (non-repudiation). +func (pk *PublicKey) WrapKeyEphemeral(key []byte) (*WrappedKey, error) { + var newSK [32]byte + + randread(newSK[:]) + clamp(newSK[:]) + + return wrapKey(pk, key, &newSK) +} + // given a file-encryption-key, wrap it in the identity of the recipient 'pk' using our // secret key. This function identifies the sender. func (sk *PrivateKey) WrapKey(pk *PublicKey, key []byte) (*WrappedKey, error) { - var shared, theirPK, ourSK [32]byte + var ourSK [32]byte copy(ourSK[:], sk.toCurve25519SK()) - copy(theirPK[:], pk.toCurve25519PK()) - - curve25519.ScalarMult(&shared, &ourSK, &theirPK) - - return wrapKey(pk, key, theirPK[:], shared[:]) + return wrapKey(pk, key, &ourSK) } + +func wrapKey(pk *PublicKey, k []byte, ourSK *[32]byte) (*WrappedKey, error) { + var curvePK, theirPK, shared [32]byte + + copy(theirPK[:], pk.toCurve25519PK()) + curve25519.ScalarBaseMult(&curvePK, ourSK) + curve25519.ScalarMult(&shared, ourSK, &theirPK) + + ek, nonce, err := aeadSeal(k, shared[:], pk.Pk) + if err != nil { + return nil, fmt.Errorf("wrap: %s", err) + } + + return &WrappedKey{ + PkHash: pk.hash, + Pk: curvePK[:], + Nonce: nonce, + Key: ek, + }, nil +} + + + // Unwrap a wrapped key using the private key 'sk' -func (w *WrappedKey) UnwrapKey(sk *PrivateKey) ([]byte, error) { +func (w *WrappedKey) UnwrapKey(sk *PrivateKey, senderPk *PublicKey) ([]byte, error) { var shared, theirPK, ourSK [32]byte pk := sk.PublicKey() + copy(ourSK[:], sk.toCurve25519SK()) copy(theirPK[:], w.Pk) curve25519.ScalarMult(&shared, &ourSK, &theirPK) + if senderPk != nil { + var cPK, shared2 [32]byte + + curvePK := senderPk.toCurve25519PK() + + copy(cPK[:], curvePK) + curve25519.ScalarMult(&shared2, &ourSK, &cPK) + + if subtle.ConstantTimeCompare(shared2[:], shared[:]) != 1 { + return nil, fmt.Errorf("unwrap: sender validation failed") + } + } + key, err := aeadOpen(w.Key, w.Nonce, shared[:], pk.Pk) if err != nil { return nil, err @@ -471,38 +502,6 @@ func (w *WrappedKey) UnwrapKey(sk *PrivateKey) ([]byte, error) { } -// Wrap a shared key with the recipient's public key 'pk' by generating an ephemeral -// Curve25519 keypair. This function does not identify the sender (non-repudiation). -func (pk *PublicKey) WrapKeyEphemeral(key []byte) (*WrappedKey, error) { - var shared, newSK, newPK, theirPK [32]byte - - randread(newSK[:]) - - copy(theirPK[:], pk.toCurve25519PK()) - curve25519.ScalarBaseMult(&newPK, &newSK) - curve25519.ScalarMult(&shared, &newSK, &theirPK) - - // we throw away newSK after deriving the shared key. - // The recipient can derive the same key using theirSK and newPK. - // (newPK will be marshalled and returned by this function) - - return wrapKey(pk, key, newPK[:], shared[:]) -} - -func wrapKey(pk *PublicKey, k, theirPK, shared []byte) (*WrappedKey, error) { - ek, nonce, err := aeadSeal(k, shared[:], pk.Pk) - if err != nil { - return nil, fmt.Errorf("wrap: %s", err) - } - - return &WrappedKey{ - PkHash: pk.hash, - Pk: theirPK, - Nonce: nonce, - Key: ek, - }, nil -} - // Convert an Ed25519 Private Key to Curve25519 Private key func (sk *PrivateKey) toCurve25519SK() []byte { if sk.ck == nil { diff --git a/sigtool.go b/sigtool.go index c21013b..f022b51 100644 --- a/sigtool.go +++ b/sigtool.go @@ -26,9 +26,6 @@ import ( "github.com/opencoff/sigtool/sign" ) -// This will be filled in by "build" -var Version string = "1.1" - var Z string = path.Base(os.Args[0]) func main() { @@ -42,7 +39,7 @@ func main() { mf.Parse(os.Args[1:]) if ver { - fmt.Printf("%s: %s\n", Z, Version) + fmt.Printf("%s - %s [%s; %s]\n", Z, ProductVersion, RepoVersion, Buildtime) os.Exit(0) } @@ -346,4 +343,9 @@ func warn(f string, v ...interface{}) { os.Stderr.Sync() } +// This will be filled in by "build" +var RepoVersion string = "UNDEFINED" +var Buildtime string = "UNDEFINED" +var ProductVersion string = "UNDEFINED" + // vim: ft=go:sw=8:ts=8:noexpandtab:tw=98: