Working version with enc/dec of all key types.

* Updated README
* fix non-ephemeral key wrap/unwrap
* fix out of bounds error in decrypt
This commit is contained in:
Sudhi Herle 2019-10-18 15:42:08 -07:00
parent 21445ba1a1
commit a27044154a
5 changed files with 152 additions and 108 deletions

129
README.md
View file

@ -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 with SHA-512 and then signs the SHA-512 checksum. The keys and signatures
are YAML files and so, human readable. are YAML files and so, human readable.
It can encrypt & decrypt files by converting the Ed25519 keys to their It can encrypt files for multiple recipients - each of whom is identified
corresponding Curve25519 variants. This elliptic co-ordinate transform by their Ed25519 public key. The encryption by default generates ephmeral
follows [FiloSottile's writeup][2]. The file encryption uses Curve25519 keys and creates pair-wise shared secret for each recipient of
AES-GCM-256 (AEAD); the input is broken into chunks and each chunk is the encrypted file. The caller can optionally use a specific secret key
AEAD encrypted. The default chunk size is 4MB (4 * 1048576 bytes). 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? ## How do I build it?
With Go 1.5 and later: 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 If the sender wishes to prove to the recipient that they encrypted
a file: 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 This will create an encrypted file *archive.tar.gz.enc* such that the
recipient in possession of *theikey.key* can decrypt it. Furthermore, if recipient in possession of *to.key* can decrypt it. Furthermore, if
the recipient has *mykey.pub*, they can verify that the sender is indeed the recipient has *sender.pub*, they can verify that the sender is indeed
who they expect. 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 ### 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 The Ed25519 private key is encrypted using a key derived from the
user supplied pass phrase. This pass phrase is used to derive an user supplied pass phrase. This pass phrase is used to derive an
encryption key using the Scrypt key derivation algorithm. The 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 As an additional security measure, the user supplied pass phrase is
hashed with SHA512. 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 ## Understanding the Code
`src/sign` is a library to generate, verify and store Ed25519 keys `src/sign` is a library to generate, verify and store Ed25519 keys
and signatures. It uses the extended library (golang.org/x/crypto) and signatures. It uses the extended library (golang.org/x/crypto)
for the underlying operations. for the underlying operations.
`src/crypt.go` contains the encryption & decryption code.
The generated keys and signatures are proper YAML files and human The generated keys and signatures are proper YAML files and human
readable. readable.

2
build
View file

@ -354,7 +354,7 @@ case $Tool in
all="$@" all="$@"
fi fi
echo "Building $rev, $cross $msg .." echo "Building $msg $Prodver ($rev) for $cross .."
for p in $all; do for p in $all; do
if echo $p | grep -q ':' ; then if echo $p | grep -q ':' ; then

View file

@ -221,12 +221,12 @@ func decrypt(args []string) {
} }
d, err := sign.NewDecryptor(infd, pk) d, err := sign.NewDecryptor(infd)
if err != nil { if err != nil {
die("%s", err) die("%s", err)
} }
err = d.SetPrivateKey(sk) err = d.SetPrivateKey(sk, pk)
if err != nil { if err != nil {
die("%s", err) die("%s", err)
} }

View file

@ -241,7 +241,7 @@ type Decryptor struct {
// Create a new decryption context and if 'pk' is given, check that it matches // Create a new decryption context and if 'pk' is given, check that it matches
// the sender // the sender
func NewDecryptor(rd io.Reader, pk *PublicKey) (*Decryptor, error) { func NewDecryptor(rd io.Reader) (*Decryptor, error) {
var b [12]byte var b [12]byte
_, err := io.ReadFull(rd, b[:]) _, 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 return d, nil
} }
// Use Private Key 'sk' to decrypt the encrypted keys in the header // Use Private Key 'sk' to decrypt the encrypted keys in the header and optionally validate
func (d *Decryptor) SetPrivateKey(sk *PrivateKey) error { // the sender
func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error {
var err error var err error
pkh := sk.PublicKey().Hash() pkh := sk.PublicKey().Hash()
for i, w := range d.Keys { for i, w := range d.Keys {
if subtle.ConstantTimeCompare(pkh, w.PkHash) == 1 { if subtle.ConstantTimeCompare(pkh, w.PkHash) == 1 {
d.key, err = w.UnwrapKey(sk) d.key, err = w.UnwrapKey(sk, senderPk)
if err != nil { if err != nil {
return fmt.Errorf("decrypt: can't unwrap key %d: %s", i, err) return fmt.Errorf("decrypt: can't unwrap key %d: %s", i, err)
} }
@ -368,6 +354,7 @@ havekey:
if err != nil { if err != nil {
return fmt.Errorf("decrypt: %s", err) return fmt.Errorf("decrypt: %s", err)
} }
d.buf = make([]byte, int(d.ChunkSize) + d.ae.Overhead())
return nil return nil
} }
@ -440,29 +427,73 @@ func (d *Decryptor) decrypt(i int) ([]byte, error) {
return p, nil 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 // given a file-encryption-key, wrap it in the identity of the recipient 'pk' using our
// secret key. This function identifies the sender. // secret key. This function identifies the sender.
func (sk *PrivateKey) WrapKey(pk *PublicKey, key []byte) (*WrappedKey, error) { 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(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' // 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 var shared, theirPK, ourSK [32]byte
pk := sk.PublicKey() pk := sk.PublicKey()
copy(ourSK[:], sk.toCurve25519SK()) copy(ourSK[:], sk.toCurve25519SK())
copy(theirPK[:], w.Pk) copy(theirPK[:], w.Pk)
curve25519.ScalarMult(&shared, &ourSK, &theirPK) 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) key, err := aeadOpen(w.Key, w.Nonce, shared[:], pk.Pk)
if err != nil { if err != nil {
return nil, err 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 // Convert an Ed25519 Private Key to Curve25519 Private key
func (sk *PrivateKey) toCurve25519SK() []byte { func (sk *PrivateKey) toCurve25519SK() []byte {
if sk.ck == nil { if sk.ck == nil {

View file

@ -26,9 +26,6 @@ import (
"github.com/opencoff/sigtool/sign" "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]) var Z string = path.Base(os.Args[0])
func main() { func main() {
@ -42,7 +39,7 @@ func main() {
mf.Parse(os.Args[1:]) mf.Parse(os.Args[1:])
if ver { if ver {
fmt.Printf("%s: %s\n", Z, Version) fmt.Printf("%s - %s [%s; %s]\n", Z, ProductVersion, RepoVersion, Buildtime)
os.Exit(0) os.Exit(0)
} }
@ -346,4 +343,9 @@ func warn(f string, v ...interface{}) {
os.Stderr.Sync() 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: // vim: ft=go:sw=8:ts=8:noexpandtab:tw=98: