vault: first pass at keyring integration
This commit is contained in:
parent
50dc6a471e
commit
0e9136d14c
|
@ -4,6 +4,7 @@ import (
|
|||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
@ -15,10 +16,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// keyTerm is the hard coded key term. Eventually, when
|
||||
// a key ring is supported for online key rotation, the term
|
||||
// will be incremented and represents the encryption key to use.
|
||||
keyTerm = 1
|
||||
// initialKeyTerm is the hard coded initial key term. This is
|
||||
// used only for values that are not encrypted with the keyring.
|
||||
initialKeyTerm = 1
|
||||
|
||||
// termSize the number of bytes used for the key term.
|
||||
termSize = 4
|
||||
|
@ -45,11 +45,14 @@ type AESGCMBarrier struct {
|
|||
l sync.RWMutex
|
||||
sealed bool
|
||||
|
||||
// primary is the AEAD keyed from the encryption key.
|
||||
// This is the cipher that should be used to encrypt/decrypt
|
||||
// all the underlying values. It will be available if the
|
||||
// barrier is unsealed.
|
||||
primary cipher.AEAD
|
||||
// keyring is used to maintain all of the encryption keys, including
|
||||
// the active key used for encryption, but also prior keys to allow
|
||||
// decryption of keys encrypted under previous terms.
|
||||
keyring *Keyring
|
||||
|
||||
// cache is used to reduce the number of AEAD constructions we do
|
||||
cache map[uint32]cipher.AEAD
|
||||
cacheLock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewAESGCMBarrier is used to construct a new barrier that uses
|
||||
|
@ -58,6 +61,7 @@ func NewAESGCMBarrier(physical physical.Backend) (*AESGCMBarrier, error) {
|
|||
b := &AESGCMBarrier{
|
||||
backend: physical,
|
||||
sealed: true,
|
||||
cache: make(map[uint32]cipher.AEAD),
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
@ -65,8 +69,17 @@ func NewAESGCMBarrier(physical physical.Backend) (*AESGCMBarrier, error) {
|
|||
// Initialized checks if the barrier has been initialized
|
||||
// and has a master key set.
|
||||
func (b *AESGCMBarrier) Initialized() (bool, error) {
|
||||
// Read the init sentinel file
|
||||
out, err := b.backend.Get(barrierInitPath)
|
||||
// Read the keyring file
|
||||
out, err := b.backend.Get(keyringPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check for initialization: %v", err)
|
||||
}
|
||||
if out != nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Fallback, check for the old sentinel file
|
||||
out, err = b.backend.Get(barrierInitPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check for initialization: %v", err)
|
||||
}
|
||||
|
@ -100,29 +113,33 @@ func (b *AESGCMBarrier) Initialize(key []byte) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to generate encryption key: %v", err)
|
||||
}
|
||||
defer memzero(encrypt)
|
||||
|
||||
// Create the barrier init entry
|
||||
init := barrierInit{
|
||||
// Create a new keyring, install the keys
|
||||
keyring := NewKeyring()
|
||||
keyring.SetMasterKey(key)
|
||||
keyring.AddKey(&Key{
|
||||
Term: 1,
|
||||
Version: 1,
|
||||
Key: encrypt,
|
||||
}
|
||||
buf, err := json.Marshal(init)
|
||||
Value: encrypt,
|
||||
})
|
||||
|
||||
// Create the keyring entry
|
||||
buf, err := keyring.Serialize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create barrier entry: %v", err)
|
||||
return fmt.Errorf("failed to serialize keyring: %v", err)
|
||||
}
|
||||
defer memzero(buf)
|
||||
|
||||
// Encrypt the barrier init value
|
||||
value := b.encrypt(gcm, buf)
|
||||
value := b.encrypt(initialKeyTerm, gcm, buf)
|
||||
|
||||
// Create the barrierInitPath
|
||||
pe := &physical.Entry{
|
||||
Key: barrierInitPath,
|
||||
Key: keyringPath,
|
||||
Value: value,
|
||||
}
|
||||
if err := b.backend.Put(pe); err != nil {
|
||||
return fmt.Errorf("failed to create initialization key: %v", err)
|
||||
return fmt.Errorf("failed to persist keyring: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -159,8 +176,42 @@ func (b *AESGCMBarrier) Unseal(key []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Create the AES-GCM
|
||||
gcm, err := b.aeadFromKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read in the keyring
|
||||
out, err := b.backend.Get(keyringPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check for keyring: %v", err)
|
||||
}
|
||||
if out != nil {
|
||||
// Decrypt the barrier init key
|
||||
plain, err := b.decrypt(gcm, out.Value)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "message authentication failed") {
|
||||
return ErrBarrierInvalidKey
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer memzero(plain)
|
||||
|
||||
// Recover the keyring
|
||||
keyring, err := DeserializeKeyring(plain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("keyring deserialization failed: %v", err)
|
||||
}
|
||||
|
||||
// Setup the keyring and finish
|
||||
b.keyring = keyring
|
||||
b.sealed = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read the barrier initialization key
|
||||
out, err := b.backend.Get(barrierInitPath)
|
||||
out, err = b.backend.Get(barrierInitPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check for initialization: %v", err)
|
||||
}
|
||||
|
@ -168,12 +219,6 @@ func (b *AESGCMBarrier) Unseal(key []byte) error {
|
|||
return ErrBarrierNotInit
|
||||
}
|
||||
|
||||
// Create the AES-GCM
|
||||
gcm, err := b.aeadFromKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Decrypt the barrier init key
|
||||
plain, err := b.decrypt(gcm, out.Value)
|
||||
if err != nil {
|
||||
|
@ -191,13 +236,36 @@ func (b *AESGCMBarrier) Unseal(key []byte) error {
|
|||
}
|
||||
defer memzero(init.Key)
|
||||
|
||||
// Initialize the master encryption key
|
||||
b.primary, err = b.aeadFromKey(init.Key)
|
||||
// Setup a new keyring, this is for backwards compatability
|
||||
keyring := NewKeyring()
|
||||
keyring.SetMasterKey(key)
|
||||
keyring.AddKey(&Key{
|
||||
Term: 1,
|
||||
Version: 1,
|
||||
Value: init.Key,
|
||||
})
|
||||
|
||||
// Serialize the keyring
|
||||
buf, err := keyring.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to serialize keyring: %v", err)
|
||||
}
|
||||
defer memzero(buf)
|
||||
|
||||
// Encrypt the barrier init value
|
||||
value := b.encrypt(initialKeyTerm, gcm, buf)
|
||||
|
||||
// Create the barrierInitPath
|
||||
pe := &physical.Entry{
|
||||
Key: keyringPath,
|
||||
Value: value,
|
||||
}
|
||||
if err := b.backend.Put(pe); err != nil {
|
||||
return fmt.Errorf("failed to persist keyring: %v", err)
|
||||
}
|
||||
|
||||
// Set the vault as unsealed
|
||||
b.keyring = keyring
|
||||
b.sealed = false
|
||||
return nil
|
||||
}
|
||||
|
@ -209,7 +277,11 @@ func (b *AESGCMBarrier) Seal() error {
|
|||
defer b.l.Unlock()
|
||||
|
||||
// Remove the primary key, and seal the vault
|
||||
b.primary = nil
|
||||
b.cache = make(map[uint32]cipher.AEAD)
|
||||
if b.keyring != nil {
|
||||
b.keyring.Wipe()
|
||||
b.keyring = nil
|
||||
}
|
||||
b.sealed = true
|
||||
return nil
|
||||
}
|
||||
|
@ -220,14 +292,19 @@ func (b *AESGCMBarrier) Put(entry *Entry) error {
|
|||
b.l.RLock()
|
||||
defer b.l.RUnlock()
|
||||
|
||||
primary := b.primary
|
||||
if primary == nil {
|
||||
keyring := b.keyring
|
||||
if keyring == nil {
|
||||
return ErrBarrierSealed
|
||||
}
|
||||
term := keyring.ActiveTerm()
|
||||
primary, err := b.aeadForTerm(term)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pe := &physical.Entry{
|
||||
Key: entry.Key,
|
||||
Value: b.encrypt(primary, entry.Value),
|
||||
Value: b.encrypt(term, primary, entry.Value),
|
||||
}
|
||||
return b.backend.Put(pe)
|
||||
}
|
||||
|
@ -238,8 +315,8 @@ func (b *AESGCMBarrier) Get(key string) (*Entry, error) {
|
|||
b.l.RLock()
|
||||
defer b.l.RUnlock()
|
||||
|
||||
primary := b.primary
|
||||
if primary == nil {
|
||||
keyring := b.keyring
|
||||
if keyring == nil {
|
||||
return nil, ErrBarrierSealed
|
||||
}
|
||||
|
||||
|
@ -252,7 +329,7 @@ func (b *AESGCMBarrier) Get(key string) (*Entry, error) {
|
|||
}
|
||||
|
||||
// Decrypt the ciphertext
|
||||
plain, err := b.decrypt(primary, pe.Value)
|
||||
plain, err := b.decryptKeyring(pe.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decryption failed: %v", err)
|
||||
}
|
||||
|
@ -290,6 +367,41 @@ func (b *AESGCMBarrier) List(prefix string) ([]string, error) {
|
|||
return b.backend.List(prefix)
|
||||
}
|
||||
|
||||
// aeadForTerm returns the AES-GCM AEAD for the given term
|
||||
func (b *AESGCMBarrier) aeadForTerm(term uint32) (cipher.AEAD, error) {
|
||||
// Check for the keyring
|
||||
keyring := b.keyring
|
||||
if keyring == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Check the cache for the aead
|
||||
b.cacheLock.RLock()
|
||||
aead, ok := b.cache[term]
|
||||
b.cacheLock.RUnlock()
|
||||
if ok {
|
||||
return aead, nil
|
||||
}
|
||||
|
||||
// Read the underlying key
|
||||
key := keyring.TermKey(term)
|
||||
if key == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create a new aead
|
||||
aead, err := b.aeadFromKey(key.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update the cache
|
||||
b.cacheLock.Lock()
|
||||
b.cache[term] = aead
|
||||
b.cacheLock.Unlock()
|
||||
return aead, nil
|
||||
}
|
||||
|
||||
// aeadFromKey returns an AES-GCM AEAD using the given key.
|
||||
func (b *AESGCMBarrier) aeadFromKey(key []byte) (cipher.AEAD, error) {
|
||||
// Create the AES cipher
|
||||
|
@ -307,15 +419,15 @@ func (b *AESGCMBarrier) aeadFromKey(key []byte) (cipher.AEAD, error) {
|
|||
}
|
||||
|
||||
// encrypt is used to encrypt a value
|
||||
func (b *AESGCMBarrier) encrypt(gcm cipher.AEAD, plain []byte) []byte {
|
||||
func (b *AESGCMBarrier) encrypt(term uint32, gcm cipher.AEAD, plain []byte) []byte {
|
||||
// Allocate the output buffer with room for tern, version byte,
|
||||
// nonce, GCM tag and the plaintext
|
||||
capacity := termSize + 1 + gcm.NonceSize() + gcm.Overhead() + len(plain)
|
||||
size := termSize + 1 + gcm.NonceSize()
|
||||
out := make([]byte, size, capacity)
|
||||
|
||||
// Set the term to 1
|
||||
out[3] = keyTerm
|
||||
// Set the key term
|
||||
binary.BigEndian.PutUint32(out[:4], term)
|
||||
|
||||
// Set the version byte
|
||||
out[4] = aesgcmVersionByte
|
||||
|
@ -331,8 +443,9 @@ func (b *AESGCMBarrier) encrypt(gcm cipher.AEAD, plain []byte) []byte {
|
|||
|
||||
// decrypt is used to decrypt a value
|
||||
func (b *AESGCMBarrier) decrypt(gcm cipher.AEAD, cipher []byte) ([]byte, error) {
|
||||
// Verify the term
|
||||
if cipher[0] != 0 || cipher[1] != 0 || cipher[2] != 0 || cipher[3] != keyTerm {
|
||||
// Verify the term is always just one
|
||||
term := binary.BigEndian.Uint32(cipher[:4])
|
||||
if term != initialKeyTerm {
|
||||
return nil, fmt.Errorf("term mis-match")
|
||||
}
|
||||
|
||||
|
@ -349,3 +462,31 @@ func (b *AESGCMBarrier) decrypt(gcm cipher.AEAD, cipher []byte) ([]byte, error)
|
|||
// Attempt to open
|
||||
return gcm.Open(out, nonce, raw, nil)
|
||||
}
|
||||
|
||||
// decryptKeyring is used to decrypt a value using the keyring
|
||||
func (b *AESGCMBarrier) decryptKeyring(cipher []byte) ([]byte, error) {
|
||||
// Verify the term
|
||||
term := binary.BigEndian.Uint32(cipher[:4])
|
||||
|
||||
// Verify the version byte
|
||||
if cipher[4] != aesgcmVersionByte {
|
||||
return nil, fmt.Errorf("version bytes mis-match")
|
||||
}
|
||||
|
||||
// Get the GCM by term
|
||||
gcm, err := b.aeadForTerm(term)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gcm == nil {
|
||||
return nil, fmt.Errorf("no decryption key available for term %d", term)
|
||||
}
|
||||
|
||||
// Capture the parts
|
||||
nonce := cipher[5 : 5+gcm.NonceSize()]
|
||||
raw := cipher[5+gcm.NonceSize():]
|
||||
out := make([]byte, 0, len(raw)-gcm.NonceSize())
|
||||
|
||||
// Attempt to open
|
||||
return gcm.Open(out, nonce, raw, nil)
|
||||
}
|
||||
|
|
|
@ -114,15 +114,16 @@ func TestEncrypt_Unique(t *testing.T) {
|
|||
b.Initialize(key)
|
||||
b.Unseal(key)
|
||||
|
||||
entry := &Entry{Key: "test", Value: []byte("test")}
|
||||
primary := b.primary
|
||||
|
||||
if primary == nil {
|
||||
if b.keyring == nil {
|
||||
t.Fatalf("barrier is sealed")
|
||||
}
|
||||
|
||||
first := b.encrypt(primary, entry.Value)
|
||||
second := b.encrypt(primary, entry.Value)
|
||||
entry := &Entry{Key: "test", Value: []byte("test")}
|
||||
term := b.keyring.ActiveTerm()
|
||||
primary, _ := b.aeadForTerm(term)
|
||||
|
||||
first := b.encrypt(term, primary, entry.Value)
|
||||
second := b.encrypt(term, primary, entry.Value)
|
||||
|
||||
if bytes.Equal(first, second) == true {
|
||||
t.Fatalf("improper random seeding detected")
|
||||
|
|
|
@ -120,12 +120,12 @@ func testBarrier(t *testing.T, b SecurityBarrier) {
|
|||
t.Fatalf("bad: %v", out)
|
||||
}
|
||||
|
||||
// List should have only "barrier/"
|
||||
// List should have only "core/"
|
||||
keys, err := b.List("")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(keys) != 1 || keys[0] != "barrier/" {
|
||||
if len(keys) != 1 || keys[0] != "core/" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
|
@ -151,7 +151,7 @@ func testBarrier(t *testing.T, b SecurityBarrier) {
|
|||
if len(keys) != 2 {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
if keys[0] != "barrier/" || keys[1] != "test" {
|
||||
if keys[0] != "core/" || keys[1] != "test" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ func testBarrier(t *testing.T, b SecurityBarrier) {
|
|||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(keys) != 1 || keys[0] != "barrier/" {
|
||||
if len(keys) != 1 || keys[0] != "core/" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
|
|
|
@ -142,6 +142,18 @@ func (k *Keyring) Serialize() ([]byte, error) {
|
|||
return buf, err
|
||||
}
|
||||
|
||||
// Wipe is used to prepare for sealing my removing everything from memory
|
||||
func (k *Keyring) Wipe() {
|
||||
if k.masterKey != nil {
|
||||
memzero(k.masterKey)
|
||||
k.masterKey = nil
|
||||
}
|
||||
for idx, key := range k.keys {
|
||||
memzero(key.Value)
|
||||
k.keys[idx] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// DeserializeKeyring is used to deserialize and return a new keyring
|
||||
func DeserializeKeyring(buf []byte) (*Keyring, error) {
|
||||
// Deserialize the keyring
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestKeyring(t *testing.T) {
|
|||
|
||||
// Add a key
|
||||
testKey := []byte("testing")
|
||||
key1 := &Key{1, testKey, time.Now()}
|
||||
key1 := &Key{Term: 1, Version: 1, Value: testKey, InstallTime: time.Now()}
|
||||
err := k.AddKey(key1)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
|
@ -53,7 +53,7 @@ func TestKeyring(t *testing.T) {
|
|||
|
||||
// Should not allow conficting set
|
||||
testConflict := []byte("nope")
|
||||
key1Conf := &Key{1, testConflict, time.Now()}
|
||||
key1Conf := &Key{Term: 1, Version: 1, Value: testConflict, InstallTime: time.Now()}
|
||||
err = k.AddKey(key1Conf)
|
||||
if err == nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
|
@ -61,7 +61,7 @@ func TestKeyring(t *testing.T) {
|
|||
|
||||
// Add a new key
|
||||
testSecond := []byte("second")
|
||||
key2 := &Key{2, testSecond, time.Now()}
|
||||
key2 := &Key{Term: 2, Version: 1, Value: testSecond, InstallTime: time.Now()}
|
||||
err = k.AddKey(key2)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
|
@ -140,8 +140,8 @@ func TestKeyring_Serialize(t *testing.T) {
|
|||
|
||||
testKey := []byte("testing")
|
||||
testSecond := []byte("second")
|
||||
k.AddKey(&Key{1, testKey, time.Now()})
|
||||
k.AddKey(&Key{2, testSecond, time.Now()})
|
||||
k.AddKey(&Key{Term: 1, Version: 1, Value: testKey, InstallTime: time.Now()})
|
||||
k.AddKey(&Key{Term: 2, Version: 1, Value: testSecond, InstallTime: time.Now()})
|
||||
|
||||
buf, err := k.Serialize()
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in New Issue