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:
parent
36410626dd
commit
00542dec02
11 changed files with 1369 additions and 1111 deletions
44
README.md
44
README.md
|
@ -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?
|
||||||
|
@ -181,14 +182,24 @@ described as a protobuf file (sign/hdr.proto):
|
||||||
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
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -210,11 +221,16 @@ 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
2
build
|
@ -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
|
@ -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;
|
||||||
|
@ -18,7 +18,16 @@ package sign;
|
||||||
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
97
internal/pb/wrap.go
Normal 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
|
||||||
|
}
|
427
sign/encrypt.go
427
sign/encrypt.go
|
@ -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
|
ae cipher.AEAD
|
||||||
sender *PrivateKey
|
|
||||||
|
// sender ephemeral curve 25519 SK
|
||||||
|
// the corresponding PK is in Header above
|
||||||
|
senderSK []byte
|
||||||
|
|
||||||
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 {
|
||||||
|
|
||||||
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)
|
e.Keys = append(e.Keys, w)
|
||||||
return nil
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -341,9 +379,10 @@ 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 {
|
pb.Randread(csk[:])
|
||||||
return nil, nil, fmt.Errorf("wrap: %s", err)
|
pb.Clamp(csk[:])
|
||||||
}
|
pk, err = curve25519.X25519(csk[:], curve25519.Basepoint)
|
||||||
|
sk = csk[:]
|
||||||
aes, err := aes.NewCipher(kek)
|
return
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("wrap: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
548
sign/keys.go
Normal 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:
|
464
sign/sign.go
464
sign/sign.go
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
||||||
0.9.1
|
1.0.0
|
||||||
|
|
Loading…
Add table
Reference in a new issue