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
|
||||
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.
|
||||
|
||||
|
|
2
build
2
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
|
||||
|
|
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 {
|
||||
die("%s", err)
|
||||
}
|
||||
|
||||
err = d.SetPrivateKey(sk)
|
||||
err = d.SetPrivateKey(sk, pk)
|
||||
if err != nil {
|
||||
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
|
||||
// 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 {
|
||||
|
|
10
sigtool.go
10
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:
|
||||
|
|
Loading…
Add table
Reference in a new issue