Major breaking changes: Reworked file encryption scheme

* all encryption now uses ephmeral curve25519 keys
* sender can identify themselves by providing a signing key
* sign/verify now uses a string prefix for calculating checksum of the
  incoming message + known prefix [prevents us from verifying unknown
  blobs]
* encrypt/decrypt key is now expanded with a known prefix _and_ the
  header checksum
* protobuf definition changed to include an encrypted sender
  identification blob (sender public key)
* moved protobuf files into an internal/pb directory
* general code rearrangement to make it easy to find files
* added extra validation for reading all keys
* bumped version to 1.0.0
This commit is contained in:
Sudhi Herle 2020-03-20 17:40:52 -07:00
parent 36410626dd
commit 00542dec02
11 changed files with 1369 additions and 1111 deletions

View file

@ -140,14 +140,15 @@ recipient can decrypt using their private key.
### How is the private key protected? ### How is the private key protected?
The Ed25519 private key is encrypted in AES-GCM-256 mode using a key The Ed25519 private key is encrypted in AES-GCM-256 mode using a key
derived from the user's pass phrase. derived from the user's pass-phrase.
### How is the Encryption done? ### How is the Encryption done?
The file encryption uses AES-GCM-256 in AEAD mode. The encryption uses 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 a random 32-byte AES-256 key. This key is mixed in with the header checksum
each chunk is individually AEAD encrypted. The default chunk size as a safeguard to protect the header against accidental or malicious corruption.
is 4MB (4 * 1048576 bytes). Each chunk generates its own nonce The input is broken into chunks and each chunk is individually AEAD encrypted.
from a global salt. The nonce is calculated as a SHA256 hash of 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. the salt, the chunk length and the block number.
### What is the public-key cryptography? ### What is the public-key cryptography?
@ -179,16 +180,26 @@ described as a protobuf file (sign/hdr.proto):
```protobuf ```protobuf
message header { message header {
uint32 chunk_size = 1; uint32 chunk_size = 1;
bytes salt = 2; bytes salt = 2;
repeated wrapped_key keys = 3; bytes pk = 3; // sender's ephemeral curve PK
sender sender_pk = 4; // sender's encrypted ed25519 PK
repeated wrapped_key keys = 5;
} }
/*
* Sender info is wrapped using the data encryption key
*/
message sender {
bytes pk = 1;
}
/*
* A file encryption key is wrapped by a recipient specific public
* key. WrappedKey describes such a wrapped key.
*/
message wrapped_key { message wrapped_key {
bytes pk_hash = 1; // hash of Ed25519 PK bytes key = 2;
bytes pk = 2; // curve25519 PK
bytes nonce = 3; // AEAD nonce
bytes key = 4; // AEAD encrypted key
} }
``` ```
@ -203,18 +214,23 @@ chunk is encoded the same way:
AEAD tag AEAD tag
``` ```
The chunk length does _not_ include the AEAD tag length; it is implicitly The chunk length does _not_ include the AEAD tag length; it is implicitly
computed. computed.
The chunk data and AEAD tag are treated as an atomic unit for AEAD The chunk data and AEAD tag are treated as an atomic unit for AEAD
decryption. decryption.
## Understanding the Code ## Understanding the Code
`src/sign` is a library to generate, verify and store Ed25519 keys The core logic is in `src/sign`: it is a library that exposes all the
and signatures. It uses the extended library (golang.org/x/crypto) functionality: key generation, key parsing, signing, encryption, decryption
for the underlying operations. etc.
`src/crypt.go` contains the encryption & decryption code. * `src/encrypt.go` contains the core encryption, decryption code
* `src/sign.go` contains the Ed25519 signing, verification code
* `src/keys.go` contains key generation, serialization, de-serialization
* `src/ssh.go` contains code to parse SSH Ed25519 key files
* `src/stream.go` contains code that provides an `io.Reader` and `io.WriteCloser` interface
for encryption and decryption.
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

@ -17,7 +17,7 @@ Progs=".:sigtool"
# Relative path to protobuf sources # Relative path to protobuf sources
# e.g. src/foo/a.proto # e.g. src/foo/a.proto
Protobufs="sign/hdr.proto" Protobufs="internal/pb/hdr.proto"
# -- DO NOT CHANGE ANYTHING AFTER THIS -- # -- DO NOT CHANGE ANYTHING AFTER THIS --

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@ syntax="proto3";
//import "gogoproto/gogo.proto" //import "gogoproto/gogo.proto"
package sign; package pb;
//option (gogoproto.marshaler_all) = true; //option (gogoproto.marshaler_all) = true;
//option (gogoproto.sizer_all) = true; //option (gogoproto.sizer_all) = true;
@ -16,9 +16,18 @@ package sign;
* protobuf format before writing to disk. * protobuf format before writing to disk.
*/ */
message header { message header {
uint32 chunk_size = 1; uint32 chunk_size = 1;
bytes salt = 2; bytes salt = 2;
repeated wrapped_key keys = 3; bytes pk = 3; // sender's ephemeral curve PK
sender sender_pk = 4; // sender's encrypted ed25519 PK
repeated wrapped_key keys = 5;
}
/*
* Sender info is wrapped using the data encryption key
*/
message sender {
bytes pk = 1;
} }
/* /*
@ -26,8 +35,5 @@ message header {
* key. WrappedKey describes such a wrapped key. * key. WrappedKey describes such a wrapped key.
*/ */
message wrapped_key { message wrapped_key {
bytes pk_hash = 1; // hash of Ed25519 PK bytes key = 2;
bytes pk = 2; // curve25519 PK
bytes nonce = 3; // AEAD nonce
bytes key = 4; // AEAD encrypted key
} }

97
internal/pb/wrap.go Normal file
View file

@ -0,0 +1,97 @@
// wrap.go - wrap keys and sender as needed
//
// (c) 2016 Sudhi Herle <sudhi@herle.net>
//
// 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 pb
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
)
const (
WrapReceiverNonce = "Receiver PK"
WrapSenderNonce = "Sender PK"
)
// Wrap sender's PK with the data encryption key
func WrapSenderPK(pk []byte, k, salt []byte) ([]byte, error) {
aes, err := aes.NewCipher(k)
if err != nil {
return nil, fmt.Errorf("wrap: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("wrap: %s", err)
}
nonce := MakeNonce([]byte(WrapSenderNonce), salt)
buf := make([]byte, ae.Overhead()+len(pk))
out := ae.Seal(buf[:0], nonce[:ae.NonceSize()], pk, nil)
return out, nil
}
// Given a wrapped PK of sender 's', unwrap it using the given key and salt
func (s *Sender) UnwrapPK(k, salt []byte) ([]byte, error) {
aes, err := aes.NewCipher(k)
if err != nil {
return nil, fmt.Errorf("uwrap-sender: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("unwrap-sender: %s", err)
}
nonce := MakeNonce([]byte(WrapSenderNonce), salt)
want := 32 + ae.Overhead()
if len(s.Pk) != want {
return nil, fmt.Errorf("unwrap-sender: incorrect decrypt bytes (need %d, saw %d)", want, 32)
}
out := make([]byte, 32)
pk, err := ae.Open(out[:0], nonce[:ae.NonceSize()], s.Pk, nil)
if err != nil {
return nil, fmt.Errorf("unwrap-sender: %s", err)
}
return pk, nil
}
func MakeNonce(v ...[]byte) []byte {
h := sha256.New()
for _, x := range v {
h.Write(x)
}
return h.Sum(nil)[:]
}
func Clamp(k []byte) []byte {
k[0] &= 248
k[31] &= 127
k[31] |= 64
return k
}
func Randread(b []byte) []byte {
_, err := io.ReadFull(rand.Reader, b)
if err != nil {
panic(fmt.Sprintf("can't read %d bytes of random data: %s", len(b), err))
}
return b
}

View file

@ -29,10 +29,16 @@
// - Shasum: 32 bytes (SHA256 of full header) // - Shasum: 32 bytes (SHA256 of full header)
// //
// The variable length segment consists of one or more // The variable length segment consists of one or more
// recipients, their wrapped keys etc. This is encoded as // recipients, each with their wrapped keys. This is encoded as
// a protobuf message. This protobuf encoded message immediately // a protobuf message. This protobuf encoded message immediately
// follows the fixed length header. // follows the fixed length header.
// //
// The input data is encrypted with an expanded random 32-byte key:
// - Prefix_string = "Encrypt Nonce"
// - datakey = SHA256(Prefix_string || header_checksum || random_key)
// - The header checksum is mixed in the above process to ensure we
// catch any malicious modification of the header.
//
// The input data is broken up into "chunks"; each no larger than // The input data is broken up into "chunks"; each no larger than
// maxChunkSize. The default block size is "chunkSize". Each block // maxChunkSize. The default block size is "chunkSize". Each block
// is AEAD encrypted: // is AEAD encrypted:
@ -50,7 +56,6 @@ import (
"bytes" "bytes"
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"crypto/ed25519"
"crypto/sha256" "crypto/sha256"
"crypto/sha512" "crypto/sha512"
"crypto/subtle" "crypto/subtle"
@ -59,7 +64,8 @@ import (
"golang.org/x/crypto/curve25519" "golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
"io" "io"
"math/big"
"github.com/opencoff/sigtool/internal/pb"
) )
// Encryption chunk size = 4MB // Encryption chunk size = 4MB
@ -76,13 +82,18 @@ const (
// Encryptor holds the encryption context // Encryptor holds the encryption context
type Encryptor struct { type Encryptor struct {
Header pb.Header
key [32]byte // file encryption key key []byte // file encryption key
ae cipher.AEAD
// sender ephemeral curve 25519 SK
// the corresponding PK is in Header above
senderSK []byte
ae cipher.AEAD
sender *PrivateKey
started bool started bool
hdrsum []byte
buf []byte buf []byte
stream bool stream bool
} }
@ -102,29 +113,43 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) {
blksz = uint32(blksize) blksz = uint32(blksize)
} }
csk, cpk, err := newSender()
if err != nil {
return nil, fmt.Errorf("encrypt: %s", err)
}
key := make([]byte, 32)
salt := make([]byte, _AEADNonceLen)
pb.Randread(key)
pb.Randread(salt)
// if sender has provided their identity to authenticate, we will use their PK
senderPK := cpk
if sk != nil {
epk := sk.PublicKey()
senderPK = epk.toCurve25519PK()
}
wPk, err := pb.WrapSenderPK(senderPK, key, salt)
if err != nil {
return nil, fmt.Errorf("encrypt: %s", err)
}
e := &Encryptor{ e := &Encryptor{
Header: Header{ Header: pb.Header{
ChunkSize: blksz, ChunkSize: blksz,
Salt: make([]byte, _AEADNonceLen), Salt: salt,
Pk: cpk,
SenderPk: &pb.Sender{
Pk: wPk,
},
}, },
sender: sk, key: key,
senderSK: csk,
} }
randread(e.key[:])
randread(e.Salt)
aes, err := aes.NewCipher(e.key[:])
if err != nil {
return nil, fmt.Errorf("encrypt: %s", err)
}
e.ae, err = cipher.NewGCMWithNonceSize(aes, _AEADNonceLen)
if err != nil {
return nil, fmt.Errorf("encrypt: %s", err)
}
e.buf = make([]byte, blksz+4+uint32(e.ae.Overhead()))
return e, nil return e, nil
} }
@ -134,20 +159,12 @@ func (e *Encryptor) AddRecipient(pk *PublicKey) error {
return fmt.Errorf("encrypt: can't add new recipient after encryption has started") return fmt.Errorf("encrypt: can't add new recipient after encryption has started")
} }
var w *WrappedKey w, err := wrapKey(pk, e.key, e.senderSK, e.Salt)
var err error if err == nil {
e.Keys = append(e.Keys, w)
if e.sender != nil {
w, err = e.sender.WrapKey(pk, e.key[:])
} else {
w, err = pk.WrapKeyEphemeral(e.key[:])
}
if err != nil {
return err
} }
e.Keys = append(e.Keys, w) return err
return nil
} }
// Encrypt the input stream 'rd' and write encrypted stream to 'wr' // Encrypt the input stream 'rd' and write encrypted stream to 'wr'
@ -206,7 +223,7 @@ func (e *Encryptor) start(wr io.Writer) error {
binary.BigEndian.PutUint32(fixHdr[_MagicLen+1:], uint32(varSize)) binary.BigEndian.PutUint32(fixHdr[_MagicLen+1:], uint32(varSize))
// Now marshal the variable portion // Now marshal the variable portion
_, err := e.MarshalToSizedBuffer(varHdr[:varSize]) _, err := e.MarshalTo(varHdr[:varSize])
if err != nil { if err != nil {
return fmt.Errorf("encrypt: can't marshal header: %s", err) return fmt.Errorf("encrypt: can't marshal header: %s", err)
} }
@ -222,6 +239,26 @@ func (e *Encryptor) start(wr io.Writer) error {
return fmt.Errorf("encrypt: %s", err) return fmt.Errorf("encrypt: %s", err)
} }
// we mix the header checksum to create the encryption key
h = sha256.New()
h.Write([]byte("Encrypt Nonce"))
h.Write(e.key)
h.Write(sumHdr)
key := h.Sum(nil)
aes, err := aes.NewCipher(key)
if err != nil {
return fmt.Errorf("encrypt: %s", err)
}
ae, err := cipher.NewGCMWithNonceSize(aes, _AEADNonceLen)
if err != nil {
return fmt.Errorf("encrypt: %s", err)
}
e.buf = make([]byte, e.ChunkSize+4+uint32(ae.Overhead()))
e.ae = ae
e.started = true e.started = true
return nil return nil
} }
@ -280,11 +317,12 @@ func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error
// Decryptor holds the decryption context // Decryptor holds the decryption context
type Decryptor struct { type Decryptor struct {
Header pb.Header
ae cipher.AEAD ae cipher.AEAD
rd io.Reader rd io.Reader
buf []byte buf []byte
hdrsum []byte
// Decrypted key // Decrypted key
key []byte key []byte
@ -340,10 +378,11 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
} }
d := &Decryptor{ d := &Decryptor{
rd: rd, rd: rd,
hdrsum: cksum,
} }
err = d.Header.Unmarshal(varBuf[:varSize]) err = d.Unmarshal(varBuf[:varSize])
if err != nil { if err != nil {
return nil, fmt.Errorf("decrypt: decode error: %s", err) return nil, fmt.Errorf("decrypt: decode error: %s", err)
} }
@ -362,23 +401,9 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
// sanity check on the wrapped keys // sanity check on the wrapped keys
for i, w := range d.Keys { for i, w := range d.Keys {
if len(w.PkHash) != PKHashLength { if len(w.Key) <= 32+12 {
return nil, fmt.Errorf("decrypt: wrapped key %d: invalid PkHash", i) return nil, fmt.Errorf("decrypt: wrapped key %d: wrong-size encrypted key", i)
} }
if len(w.Pk) != 32 {
return nil, fmt.Errorf("decrypt: wrapped key %d: invalid Curve25519 PK", i)
}
// XXX Default AES-256-GCM Nonce size is 12
if len(w.Nonce) != 12 {
return nil, fmt.Errorf("decrypt: wrapped key %d: invalid Nonce", i)
}
if len(w.Key) == 0 {
return nil, fmt.Errorf("decrypt: wrapped key %d: missing encrypted key", i)
}
} }
return d, nil return d, nil
@ -388,22 +413,45 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
// the sender // the sender
func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error { func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error {
var err error var err error
var key []byte
pkh := sk.PublicKey().Hash()
for i, w := range d.Keys { for i, w := range d.Keys {
if subtle.ConstantTimeCompare(pkh, w.PkHash) == 1 { key, err = unwrapKey(w.Key, sk, d.Pk, d.Salt)
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) }
} if key != nil {
goto havekey goto havekey
} }
} }
return fmt.Errorf("decrypt: Can't find any public key to match the given private key") return fmt.Errorf("decrypt: wrong key")
havekey: havekey:
aes, err := aes.NewCipher(d.key) if senderPk != nil {
hpk, err := d.SenderPk.UnwrapPK(key, d.Salt)
if err != nil {
return fmt.Errorf("decrypt: can't unwrap sender PK: %s", err)
}
cpk := senderPk.toCurve25519PK()
if subtle.ConstantTimeCompare(cpk, hpk) == 0 {
return fmt.Errorf("decrypt: sender verification failed")
}
}
// XXX do we need to verify d.Header.Sender.Key vs. d.Header.PK?
d.key = key
// we mix the header checksum into the key
h := sha256.New()
h.Write([]byte("Encrypt Nonce"))
h.Write(d.key)
h.Write(d.hdrsum)
key = h.Sum(nil)
aes, err := aes.NewCipher(key)
if err != nil { if err != nil {
return fmt.Errorf("decrypt: %s", err) return fmt.Errorf("decrypt: %s", err)
} }
@ -416,8 +464,71 @@ havekey:
return nil return nil
} }
// Wrap data encryption key 'k' with the sender's PK and our ephemeral curve SK
func wrapKey(pk *PublicKey, k, ourSK, salt []byte) (*pb.WrappedKey, error) {
shared, err := curve25519.X25519(ourSK, pk.toCurve25519PK())
if err != nil {
return nil, fmt.Errorf("wrap: %s", err)
}
aes, err := aes.NewCipher(shared)
if err != nil {
return nil, fmt.Errorf("wrap: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("wrap: %s", err)
}
tagsize := ae.Overhead()
nonce := pb.MakeNonce([]byte(pb.WrapReceiverNonce), salt)
buf := make([]byte, tagsize+len(shared))
out := ae.Seal(buf[:0], nonce[:ae.NonceSize()], k, pk.Pk)
return &pb.WrappedKey{
Key: out,
}, nil
}
// Unwrap a wrapped key using the receivers Ed25519 secret key 'sk' and
// senders ephemeral PublicKey
func unwrapKey(wkey []byte, sk *PrivateKey, curvePK, salt []byte) ([]byte, error) {
ourSK := sk.toCurve25519SK()
shared, err := curve25519.X25519(ourSK, curvePK)
if err != nil {
return nil, fmt.Errorf("unwrap: %s", err)
}
aes, err := aes.NewCipher(shared)
if err != nil {
return nil, fmt.Errorf("unwrap: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("unwrap: %s", err)
}
want := 32 + ae.Overhead()
if len(wkey) != want {
return nil, fmt.Errorf("unwrap: incorrect decrypt bytes (need %d, saw %d)", want, len(wkey))
}
nonce := pb.MakeNonce([]byte(pb.WrapReceiverNonce), salt)
pk := sk.PublicKey()
out := make([]byte, 32)
c, err := ae.Open(out[:0], nonce[:ae.NonceSize()], wkey, pk.Pk)
// we indicate incorrect receiver SK by returning a nil key
if err != nil {
return nil, nil
}
return c, nil
}
// Return a list of Wrapped keys in the encrypted file header // Return a list of Wrapped keys in the encrypted file header
func (d *Decryptor) WrappedKeys() []*WrappedKey { func (d *Decryptor) WrappedKeys() []*pb.WrappedKey {
return d.Keys return d.Keys
} }
@ -510,129 +621,6 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
return p[:m], eof, nil return p[:m], eof, 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) {
return wrapKey(pk, key, sk.toCurve25519SK())
}
func wrapKey(pk *PublicKey, k []byte, ourSK []byte) (*WrappedKey, error) {
curvePK, err := curve25519.X25519(ourSK, curve25519.Basepoint)
if err != nil {
return nil, fmt.Errorf("wrap: %s", err)
}
shared, err := curve25519.X25519(ourSK, pk.toCurve25519PK())
if err != nil {
return nil, fmt.Errorf("wrap: %s", err)
}
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, senderPk *PublicKey) ([]byte, error) {
ourSK := sk.toCurve25519SK()
shared, err := curve25519.X25519(ourSK, w.Pk)
if err != nil {
return nil, fmt.Errorf("unwrap: %s", err)
}
if senderPk != nil {
shared2, err := curve25519.X25519(ourSK, senderPk.toCurve25519PK())
if err != nil {
return nil, fmt.Errorf("unwrap: %s", err)
}
if subtle.ConstantTimeCompare(shared2, shared) != 1 {
return nil, fmt.Errorf("unwrap: sender validation failed")
}
}
pk := sk.PublicKey()
key, err := aeadOpen(w.Key, w.Nonce, shared[:], pk.Pk)
if err != nil {
return nil, err
}
return key, nil
}
// Convert an Ed25519 Private Key to Curve25519 Private key
func (sk *PrivateKey) toCurve25519SK() []byte {
if sk.ck == nil {
var ek [64]byte
h := sha512.New()
h.Write(sk.Sk[:32])
h.Sum(ek[:0])
sk.ck = clamp(ek[:32])
}
return sk.ck
}
// from github.com/FiloSottile/age
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
// Convert an Ed25519 Public Key to Curve25519 public key
// from github.com/FiloSottile/age
func (pk *PublicKey) toCurve25519PK() []byte {
if pk.ck != nil {
return pk.ck
}
// ed25519.PublicKey is a little endian representation of the y-coordinate,
// with the most significant bit set based on the sign of the x-ccordinate.
bigEndianY := make([]byte, ed25519.PublicKeySize)
for i, b := range pk.Pk {
bigEndianY[ed25519.PublicKeySize-i-1] = b
}
bigEndianY[0] &= 0b0111_1111
// The Montgomery u-coordinate is derived through the bilinear map
//
// u = (1 + y) / (1 - y)
//
// See https://blog.filippo.io/using-ed25519-keys-for-encryption.
y := new(big.Int).SetBytes(bigEndianY)
denom := big.NewInt(1)
denom.ModInverse(denom.Sub(denom, y), curve25519P) // 1 / (1 - y)
u := y.Mul(y.Add(y, big.NewInt(1)), denom)
u.Mod(u, curve25519P)
out := make([]byte, 32)
uBytes := u.Bytes()
n := len(uBytes)
for i, b := range uBytes {
out[n-i-1] = b
}
pk.ck = out
return out
}
// generate a KEK from a shared DH key and a Pub Key // generate a KEK from a shared DH key and a Pub Key
func expand(shared, pk []byte) ([]byte, error) { func expand(shared, pk []byte) ([]byte, error) {
kek := make([]byte, 32) kek := make([]byte, 32)
@ -641,67 +629,12 @@ func expand(shared, pk []byte) ([]byte, error) {
return kek, err return kek, err
} }
// seal the data via AEAD after suitably expanding 'shared' func newSender() (sk, pk []byte, err error) {
func aeadSeal(data, shared, pk []byte) ([]byte, []byte, error) { var csk [32]byte
kek, err := expand(shared[:], pk)
if err != nil {
return nil, nil, fmt.Errorf("wrap: %s", err)
}
aes, err := aes.NewCipher(kek) pb.Randread(csk[:])
if err != nil { pb.Clamp(csk[:])
return nil, nil, fmt.Errorf("wrap: %s", err) pk, err = curve25519.X25519(csk[:], curve25519.Basepoint)
} sk = csk[:]
return
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, nil, fmt.Errorf("wrap: %s", err)
}
noncesize := ae.NonceSize()
tagsize := ae.Overhead()
buf := make([]byte, tagsize+len(kek))
nonce := make([]byte, noncesize)
randread(nonce)
out := ae.Seal(buf[:0], nonce, data, nil)
return out, nonce, nil
}
func aeadOpen(data, nonce, shared, pk []byte) ([]byte, error) {
// hkdf or HMAC-sha-256
kek, err := expand(shared, pk)
if err != nil {
return nil, fmt.Errorf("unwrap: %s", err)
}
aes, err := aes.NewCipher(kek)
if err != nil {
return nil, fmt.Errorf("unwrap: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("unwrap: %s", err)
}
want := 32 + ae.Overhead()
if len(data) != want {
return nil, fmt.Errorf("unwrap: incorrect decrypt bytes (need %d, saw %d)", want, len(data))
}
c, err := ae.Open(data[:0], nonce, data, nil)
if err != nil {
return nil, fmt.Errorf("unwrap: %s", err)
}
return c, nil
}
func clamp(k []byte) []byte {
k[0] &= 248
k[31] &= 127
k[31] |= 64
return k
} }

View file

@ -154,6 +154,13 @@ func TestEncryptSenderVerified(t *testing.T) {
dd, err := NewDecryptor(rd) dd, err := NewDecryptor(rd)
assert(err == nil, "decryptor create fail: %s", err) assert(err == nil, "decryptor create fail: %s", err)
// first send a wrong sender key
randkey, err := NewKeypair()
assert(err == nil, "receiver rand keypair gen failed: %s", err)
err = dd.SetPrivateKey(&receiver.Sec, &randkey.Pub)
assert(err != nil, "decryptor failed to verify sender")
err = dd.SetPrivateKey(&receiver.Sec, &sender.Pub) err = dd.SetPrivateKey(&receiver.Sec, &sender.Pub)
assert(err == nil, "decryptor can't add SK: %s", err) assert(err == nil, "decryptor can't add SK: %s", err)

548
sign/keys.go Normal file
View file

@ -0,0 +1,548 @@
// keys.go -- Ed25519 keys management
//
// (c) 2016 Sudhi Herle <sudhi@herle.net>
//
// 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.
// This file implements:
// - key generation, and key I/O
// - sign/verify of files and byte strings
package sign
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/binary"
"fmt"
"hash"
"io/ioutil"
"math/big"
"os"
Ed "crypto/ed25519"
"golang.org/x/crypto/scrypt"
"gopkg.in/yaml.v2"
"github.com/opencoff/go-utils"
"github.com/opencoff/sigtool/internal/pb"
)
// Private Ed25519 key
type PrivateKey struct {
Sk []byte
// Encryption key: Curve25519 point corresponding to this Ed25519 key
ck []byte
// Cached copy of the public key
pk *PublicKey
}
// Public Ed25519 key
type PublicKey struct {
Pk []byte
// Comment string
Comment string
// Curve25519 point corresponding to this Ed25519 key
ck []byte
hash []byte
}
// Ed25519 key pair
type Keypair struct {
Sec PrivateKey
Pub PublicKey
}
// An Ed25519 Signature
type Signature struct {
Sig []byte // Ed25519 sig bytes
pkhash []byte // [0:16] SHA256 hash of public key needed for verification
}
// Length of Ed25519 Public Key Hash
const PKHashLength = 16
const (
// Scrypt parameters
_N int = 1 << 19
_r int = 8
_p int = 1
// Algorithm used in the encrypted private key
sk_algo = "scrypt-sha256"
sig_algo = "sha512-ed25519"
)
// Encrypted Private key
type serializedPrivKey struct {
Comment string `yaml:"comment,omitempty"`
// Encrypted Sk
Esk string `yaml:"esk"`
Salt string `yaml:"salt,omitempty"`
// Algorithm used for checksum and KDF
Algo string `yaml:"algo,omitempty"`
// These are params for scrypt.Key()
// CPU Cost parameter; must be a power of 2
N int `yaml:"Z,flow,omitempty"`
// r * p should be less than 2^30
R int `yaml:"r,flow,omitempty"`
P int `yaml:"p,flow,omitempty"`
}
// serialized representation of public key
type serializedPubKey struct {
Comment string `yaml:"comment,omitempty"`
Pk string `yaml:"pk"`
Hash string `yaml:"hash"`
}
// Serialized signature
type signature struct {
Comment string `yaml:"comment,omitempty"`
Pkhash string `yaml:"pkhash,omitempty"`
Signature string `yaml:"signature"`
}
func pkhash(pk []byte) []byte {
z := sha256.Sum256(pk)
return z[:PKHashLength]
}
// 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
sk.pk = pk
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)
pk.hash = pkhash(pk.Pk)
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, getpw func() ([]byte, error)) 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, getpw)
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, getpw func() ([]byte, error)) (*PrivateKey, error) {
yml, err := ioutil.ReadFile(fn)
if err != nil {
return nil, err
}
if bytes.Index(yml, []byte("OPENSSH PRIVATE KEY-")) > 0 {
return parseSSHPrivateKey(yml, getpw)
}
if pw, err := getpw(); err == nil {
return MakePrivateKey(yml, pw)
}
return nil, err
}
// 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 []byte) (*PrivateKey, error) {
var ssk serializedPrivKey
err := yaml.Unmarshal(yml, &ssk)
if err != nil {
return nil, fmt.Errorf("make priv key: can't parse YAML: %s", err)
}
if len(ssk.Salt) == 0 || len(ssk.Esk) == 0 {
return nil, fmt.Errorf("sign: not YAML private key")
}
b64 := base64.StdEncoding.DecodeString
salt, err := b64(ssk.Salt)
if err != nil {
return nil, fmt.Errorf("make priv key: can't decode salt: %s", err)
}
esk, err := b64(ssk.Esk)
if err != nil {
return nil, fmt.Errorf("make priv key: can't decode key: %s", err)
}
// We take short passwords and extend them
pwb := sha512.Sum512(pw)
// "32" == Length of AES-256 key
key, err := scrypt.Key(pwb[:], salt, ssk.N, ssk.R, ssk.P, 32)
if err != nil {
return nil, fmt.Errorf("make priv key: can't derive key: %s", err)
}
aes, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("make priv key: aes failure: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("make priv key: aes failure: %s", err)
}
skb, err := ae.Open(nil, salt[:ae.NonceSize()], esk, nil)
if err != nil {
return nil, fmt.Errorf("make priv key: wrong password")
}
return PrivateKeyFromBytes(skb)
}
// Make a private key from 64-bytes of extended Ed25519 key
func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) {
if len(buf) != 64 {
return nil, fmt.Errorf("private key is malformed (len %d!)", len(buf))
}
skb := make([]byte, 64)
copy(skb, buf)
edsk := Ed.PrivateKey(skb)
edpk := edsk.Public().(Ed.PublicKey)
pk := &PublicKey{
Pk: []byte(edpk),
hash: pkhash([]byte(edpk)),
}
sk := &PrivateKey{
Sk: skb,
pk: pk,
}
return sk, nil
}
// Given a secret key, return the corresponding Public Key
func (sk *PrivateKey) PublicKey() *PublicKey {
return sk.pk
}
// Convert an Ed25519 Private Key to Curve25519 Private key
func (sk *PrivateKey) toCurve25519SK() []byte {
if sk.ck == nil {
var ek [64]byte
h := sha512.New()
h.Write(sk.Sk[:32])
h.Sum(ek[:0])
sk.ck = clamp(ek[:32])
}
return sk.ck
}
// from github.com/FiloSottile/age
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
// Convert an Ed25519 Public Key to Curve25519 public key
// from github.com/FiloSottile/age
func (pk *PublicKey) toCurve25519PK() []byte {
if pk.ck != nil {
return pk.ck
}
// ed25519.PublicKey is a little endian representation of the y-coordinate,
// with the most significant bit set based on the sign of the x-ccordinate.
bigEndianY := make([]byte, Ed.PublicKeySize)
for i, b := range pk.Pk {
bigEndianY[Ed.PublicKeySize-i-1] = b
}
bigEndianY[0] &= 0b0111_1111
// The Montgomery u-coordinate is derived through the bilinear map
//
// u = (1 + y) / (1 - y)
//
// See https://blog.filippo.io/using-ed25519-keys-for-encryption.
y := new(big.Int).SetBytes(bigEndianY)
denom := big.NewInt(1)
denom.ModInverse(denom.Sub(denom, y), curve25519P) // 1 / (1 - y)
u := y.Mul(y.Add(y, big.NewInt(1)), denom)
u.Mod(u, curve25519P)
out := make([]byte, 32)
uBytes := u.Bytes()
n := len(uBytes)
for i, b := range uBytes {
out[n-i-1] = b
}
pk.ck = out
return out
}
// Public Key Hash
func (pk *PublicKey) Hash() []byte {
return pk.hash
}
// Serialize the private key to a file
// AEAD encryption for protecting the private key
// Format: YAML
// All []byte are in base64 (RawEncoding)
func (sk *PrivateKey) serialize(fn, comment string, getpw func() ([]byte, error)) error {
pw, err := getpw()
if err != nil {
return err
}
// expand the password into 64 bytes
pass := sha512.Sum512(pw)
salt := make([]byte, 32)
pb.Randread(salt)
// "32" == Length of AES-256 key
key, err := scrypt.Key(pass[:], salt, _N, _r, _p, 32)
if err != nil {
return fmt.Errorf("marshal: can't derive scrypt key: %s", err)
}
aes, err := aes.NewCipher(key)
if err != nil {
return fmt.Errorf("marshal: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return fmt.Errorf("marshal: %s", err)
}
tl := ae.Overhead()
buf := make([]byte, tl+len(sk.Sk))
esk := ae.Seal(buf[:0], salt[:ae.NonceSize()], sk.Sk, nil)
enc := base64.StdEncoding.EncodeToString
ssk := serializedPrivKey{
Comment: comment,
Esk: enc(esk),
Salt: enc(salt),
Algo: sk_algo,
N: _N,
R: _r,
P: _p,
}
// 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.
out, err := yaml.Marshal(&ssk)
if err != nil {
return fmt.Errorf("can't marahal to YAML: %s", err)
}
return writeFile(fn, out, 0600)
}
// --- 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
}
// first try to parse as a ssh key
pk, err := parseSSHPublicKey(yml)
if err != nil {
pk, err = MakePublicKey(yml)
}
return pk, err
}
// 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)
}
if len(spk.Pk) == 0 {
return nil, fmt.Errorf("sign: not a YAML public key")
}
b64 := base64.StdEncoding.DecodeString
var pkb []byte
if pkb, err = b64(spk.Pk); err != nil {
return nil, fmt.Errorf("can't decode YAML:Pk: %s", err)
}
if pk, err := PublicKeyFromBytes(pkb); err == nil {
pk.Comment = spk.Comment
return pk, nil
}
return nil, err
}
// Make a public key from a byte string
func PublicKeyFromBytes(b []byte) (*PublicKey, error) {
if len(b) != 32 {
return nil, fmt.Errorf("public key is malformed (len %d!)", len(b))
}
pk := &PublicKey{
Pk: make([]byte, 32),
hash: pkhash(b),
}
copy(pk.Pk, b)
return pk, nil
}
// Serialize Public Keys
func (pk *PublicKey) serialize(fn, comment string) error {
b64 := base64.StdEncoding.EncodeToString
spk := &serializedPubKey{
Comment: comment,
Pk: b64(pk.Pk),
Hash: b64(pk.hash),
}
out, err := yaml.Marshal(spk)
if err != nil {
return fmt.Errorf("can't marahal to YAML: %s", err)
}
return writeFile(fn, out, 0644)
}
// -- 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
}
func clamp(k []byte) []byte {
k[0] &= 248
k[31] &= 127
k[31] |= 64
return k
}
// EOF
// vim: noexpandtab:ts=8:sw=8:tw=92:

View file

@ -18,332 +18,29 @@
package sign package sign
import ( import (
"bytes"
"crypto" "crypto"
"crypto/aes"
"crypto/cipher"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"crypto/sha512" "crypto/sha512"
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"encoding/binary"
"fmt" "fmt"
"hash"
"io"
"io/ioutil" "io/ioutil"
"os"
Ed "crypto/ed25519" Ed "crypto/ed25519"
"golang.org/x/crypto/scrypt"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"github.com/opencoff/go-utils"
) )
// Private Ed25519 key
type PrivateKey struct {
Sk []byte
// Encryption key: Curve25519 point corresponding to this Ed25519 key
ck []byte
// Cached copy of the public key
pk *PublicKey
}
// Public Ed25519 key
type PublicKey struct {
Pk []byte
// Comment string
Comment string
// Curve25519 point corresponding to this Ed25519 key
ck []byte
hash []byte
}
// Ed25519 key pair
type Keypair struct {
Sec PrivateKey
Pub PublicKey
}
// An Ed25519 Signature
type Signature struct {
Sig []byte // Ed25519 sig bytes
pkhash []byte // [0:16] SHA256 hash of public key needed for verification
}
// Length of Ed25519 Public Key Hash
const PKHashLength = 16
const (
// Scrypt parameters
_N int = 1 << 19
_r int = 8
_p int = 1
// Algorithm used in the encrypted private key
sk_algo = "scrypt-sha256"
sig_algo = "sha512-ed25519"
)
// Encrypted Private key
type serializedPrivKey struct {
Comment string `yaml:"comment,omitempty"`
// Encrypted Sk
Esk string `yaml:"esk"`
Salt string `yaml:"salt,omitempty"`
// Algorithm used for checksum and KDF
Algo string `yaml:"algo,omitempty"`
// These are params for scrypt.Key()
// CPU Cost parameter; must be a power of 2
N int `yaml:"Z,flow,omitempty"`
// r * p should be less than 2^30
R int `yaml:"r,flow,omitempty"`
P int `yaml:"p,flow,omitempty"`
}
// serialized representation of public key
type serializedPubKey struct {
Comment string `yaml:"comment,omitempty"`
Pk string `yaml:"pk"`
Hash string `yaml:"hash"`
}
// Serialized signature
type signature struct {
Comment string `yaml:"comment,omitempty"`
Pkhash string `yaml:"pkhash,omitempty"`
Signature string `yaml:"signature"`
}
func pkhash(pk []byte) []byte {
z := sha256.Sum256(pk)
return z[:PKHashLength]
}
// 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
sk.pk = pk
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)
pk.hash = pkhash(pk.Pk)
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, getpw func() ([]byte, error)) 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, getpw)
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, getpw func() ([]byte, error)) (*PrivateKey, error) {
yml, err := ioutil.ReadFile(fn)
if err != nil {
return nil, err
}
if bytes.Index(yml, []byte("OPENSSH PRIVATE KEY-")) > 0 {
return parseSSHPrivateKey(yml, getpw)
}
if pw, err := getpw(); err == nil {
return MakePrivateKey(yml, pw)
}
return nil, err
}
// 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 []byte) (*PrivateKey, error) {
var ssk serializedPrivKey
err := yaml.Unmarshal(yml, &ssk)
if err != nil {
return nil, fmt.Errorf("make priv key: can't parse YAML: %s", err)
}
b64 := base64.StdEncoding.DecodeString
salt, err := b64(ssk.Salt)
if err != nil {
return nil, fmt.Errorf("make priv key: can't decode salt: %s", err)
}
esk, err := b64(ssk.Esk)
if err != nil {
return nil, fmt.Errorf("make priv key: can't decode key: %s", err)
}
// We take short passwords and extend them
pwb := sha512.Sum512(pw)
// "32" == Length of AES-256 key
key, err := scrypt.Key(pwb[:], salt, ssk.N, ssk.R, ssk.P, 32)
if err != nil {
return nil, fmt.Errorf("make priv key: can't derive key: %s", err)
}
aes, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("make priv key: aes failure: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("make priv key: aes failure: %s", err)
}
skb, err := ae.Open(nil, salt[:ae.NonceSize()], esk, nil)
if err != nil {
return nil, fmt.Errorf("make priv key: wrong password")
}
return PrivateKeyFromBytes(skb)
}
// Make a private key from 64-bytes of extended Ed25519 key
func PrivateKeyFromBytes(buf []byte) (*PrivateKey, error) {
if len(buf) != 64 {
return nil, fmt.Errorf("private key is malformed (len %d!)", len(buf))
}
skb := make([]byte, 64)
copy(skb, buf)
edsk := Ed.PrivateKey(skb)
edpk := edsk.Public().(Ed.PublicKey)
pk := &PublicKey{
Pk: []byte(edpk),
hash: pkhash([]byte(edpk)),
}
sk := &PrivateKey{
Sk: skb,
pk: pk,
}
return sk, nil
}
// Given a secret key, return the corresponding Public Key
func (sk *PrivateKey) PublicKey() *PublicKey {
return sk.pk
}
// Public Key Hash
func (pk *PublicKey) Hash() []byte {
return pk.hash
}
// Serialize the private key to a file
// AEAD encryption for protecting the private key
// Format: YAML
// All []byte are in base64 (RawEncoding)
func (sk *PrivateKey) serialize(fn, comment string, getpw func() ([]byte, error)) error {
pw, err := getpw()
if err != nil {
return err
}
// expand the password into 64 bytes
pass := sha512.Sum512(pw)
salt := make([]byte, 32)
randread(salt)
// "32" == Length of AES-256 key
key, err := scrypt.Key(pass[:], salt, _N, _r, _p, 32)
if err != nil {
return fmt.Errorf("marshal: can't derive scrypt key: %s", err)
}
aes, err := aes.NewCipher(key)
if err != nil {
return fmt.Errorf("marshal: %s", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return fmt.Errorf("marshal: %s", err)
}
tl := ae.Overhead()
buf := make([]byte, tl+len(sk.Sk))
esk := ae.Seal(buf[:0], salt[:ae.NonceSize()], sk.Sk, nil)
enc := base64.StdEncoding.EncodeToString
ssk := serializedPrivKey{
Comment: comment,
Esk: enc(esk),
Salt: enc(salt),
Algo: sk_algo,
N: _N,
R: _r,
P: _p,
}
// 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.
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 // Sign a prehashed Message; return the signature as opaque bytes
// Signature is an YAML file: // Signature is an YAML file:
// Comment: source file path // Comment: source file path
// Signature: Ed25519 signature // Signature: Ed25519 signature
func (sk *PrivateKey) SignMessage(ck []byte, comment string) (*Signature, error) { func (sk *PrivateKey) SignMessage(ck []byte, comment string) (*Signature, error) {
x := Ed.PrivateKey(sk.Sk) h := sha512.New()
h.Write([]byte("sigtool signed message"))
h.Write(ck)
ck = h.Sum(nil)[:]
x := Ed.PrivateKey(sk.Sk)
sig, err := x.Sign(rand.Reader, ck, crypto.Hash(0)) sig, err := x.Sign(rand.Reader, ck, crypto.Hash(0))
if err != nil { if err != nil {
return nil, fmt.Errorf("can't sign %x: %s", ck, err) return nil, fmt.Errorf("can't sign %x: %s", ck, err)
@ -441,82 +138,6 @@ func (sig *Signature) IsPKMatch(pk *PublicKey) bool {
return subtle.ConstantTimeCompare(pk.hash, sig.pkhash) == 1 return subtle.ConstantTimeCompare(pk.hash, 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
}
// first try to parse as a ssh key
pk, err := parseSSHPublicKey(yml)
if err != nil {
pk, err = MakePublicKey(yml)
}
return pk, err
}
// 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)
}
b64 := base64.StdEncoding.DecodeString
var pkb []byte
if pkb, err = b64(spk.Pk); err != nil {
return nil, fmt.Errorf("can't decode YAML:Pk: %s", err)
}
if pk, err := PublicKeyFromBytes(pkb); err == nil {
pk.Comment = spk.Comment
return pk, nil
}
return nil, err
}
// Make a public key from a byte string
func PublicKeyFromBytes(b []byte) (*PublicKey, error) {
if len(b) != 32 {
return nil, fmt.Errorf("public key is malformed (len %d!)", len(b))
}
pk := &PublicKey{
Pk: make([]byte, 32),
hash: pkhash(b),
}
copy(pk.Pk, b)
return pk, nil
}
// Serialize Public Keys
func (pk *PublicKey) serialize(fn, comment string) error {
b64 := base64.StdEncoding.EncodeToString
spk := &serializedPubKey{
Comment: comment,
Pk: b64(pk.Pk),
Hash: b64(pk.hash),
}
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' // Verify a signature 'sig' for file 'fn' against public key 'pk'
// Return True if signature matches, False otherwise // Return True if signature matches, False otherwise
func (pk *PublicKey) VerifyFile(fn string, sig *Signature) (bool, error) { func (pk *PublicKey) VerifyFile(fn string, sig *Signature) (bool, error) {
@ -532,80 +153,13 @@ func (pk *PublicKey) VerifyFile(fn string, sig *Signature) (bool, error) {
// Verify a signature 'sig' for a pre-calculated checksum 'ck' against public key 'pk' // Verify a signature 'sig' for a pre-calculated checksum 'ck' against public key 'pk'
// Return True if signature matches, False otherwise // Return True if signature matches, False otherwise
func (pk *PublicKey) VerifyMessage(ck []byte, sig *Signature) (bool, error) { func (pk *PublicKey) VerifyMessage(ck []byte, sig *Signature) (bool, error) {
h := sha512.New()
h.Write([]byte("sigtool signed message"))
h.Write(ck)
ck = h.Sum(nil)[:]
x := Ed.PublicKey(pk.Pk) x := Ed.PublicKey(pk.Pk)
return Ed.Verify(x, ck, sig.Sig), nil 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
}
func randread(b []byte) []byte {
_, err := io.ReadFull(rand.Reader, b)
if err != nil {
panic(fmt.Sprintf("can't read %d bytes of random data: %s", len(b), err))
}
return b
}
// EOF
// vim: noexpandtab:ts=8:sw=8:tw=92: // vim: noexpandtab:ts=8:sw=8:tw=92:

View file

@ -19,6 +19,8 @@ import (
"os" "os"
"path" "path"
"testing" "testing"
"github.com/opencoff/sigtool/internal/pb"
) )
// Return a temp dir in a temp-dir // Return a temp dir in a temp-dir
@ -28,7 +30,7 @@ func tempdir(t *testing.T) string {
var b [10]byte var b [10]byte
dn := os.TempDir() dn := os.TempDir()
randread(b[:]) pb.Randread(b[:])
tmp := path.Join(dn, fmt.Sprintf("%x", b[:])) tmp := path.Join(dn, fmt.Sprintf("%x", b[:]))
err := os.MkdirAll(tmp, 0755) err := os.MkdirAll(tmp, 0755)
@ -135,7 +137,7 @@ func TestSignRandBuf(t *testing.T) {
var ck [64]byte // simulates sha512 sum var ck [64]byte // simulates sha512 sum
randread(ck[:]) pb.Randread(ck[:])
pk := &kp.Pub pk := &kp.Pub
sk := &kp.Sec sk := &kp.Sec
@ -148,7 +150,7 @@ func TestSignRandBuf(t *testing.T) {
assert(ss.IsPKMatch(pk), "pk match fail") assert(ss.IsPKMatch(pk), "pk match fail")
// Corrupt the pkhash and see // Corrupt the pkhash and see
randread(ss.pkhash) pb.Randread(ss.pkhash)
assert(!ss.IsPKMatch(pk), "corrupt pk match fail") assert(!ss.IsPKMatch(pk), "corrupt pk match fail")
// Incorrect checksum == should fail verification // Incorrect checksum == should fail verification
@ -185,7 +187,7 @@ func TestSignRandBuf(t *testing.T) {
assert(err == nil, "file.dat creat file") assert(err == nil, "file.dat creat file")
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
randread(buf[:]) pb.Randread(buf[:])
n, err := fd.Write(buf[:]) n, err := fd.Write(buf[:])
assert(err == nil, fmt.Sprintf("file.dat write fail: %s", err)) 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)) assert(n == 8192, fmt.Sprintf("file.dat i/o fail: exp 8192 saw %v", n))
@ -286,7 +288,7 @@ func benchVerify(b *testing.B, buf []byte, sig *Signature, pk *PublicKey) {
func randbuf(sz uint) []byte { func randbuf(sz uint) []byte {
b := make([]byte, sz) b := make([]byte, sz)
randread(b) pb.Randread(b)
return b return b
} }

View file

@ -1 +1 @@
0.9.1 1.0.0