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:
parent
21445ba1a1
commit
a27044154a
5 changed files with 152 additions and 108 deletions
129
README.md
129
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
|
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
2
build
|
@ -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
|
||||||
|
|
4
crypt.go
4
crypt.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
115
sign/encrypt.go
115
sign/encrypt.go
|
@ -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 {
|
||||||
|
|
10
sigtool.go
10
sigtool.go
|
@ -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:
|
||||||
|
|
Loading…
Add table
Reference in a new issue