vault: First pass at a barrier
This commit is contained in:
parent
0cac63234a
commit
e8abe8b0cd
|
@ -0,0 +1,72 @@
|
|||
package vault
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrBarrierSealed is returned if an operation is performed on
|
||||
// a sealed barrier. No operation is expected to succeed before unsealing
|
||||
ErrBarrierSealed = errors.New("Vault is sealed")
|
||||
|
||||
// ErrBarrierAlreadyInit is returned if the barrier is already
|
||||
// initialized. This prevents a re-initialization.
|
||||
ErrBarrierAlreadyInit = errors.New("Vault is already initialized")
|
||||
|
||||
// ErrBarrierNotInit is returned if a non-initialized barrier
|
||||
// is attempted to be unsealed.
|
||||
ErrBarrierNotInit = errors.New("Vault is not initialized")
|
||||
)
|
||||
|
||||
const (
|
||||
// barrierInitPath is the path used to store our init sentinel file
|
||||
barrierInitPath = "barrier/init"
|
||||
)
|
||||
|
||||
// SecurityBarrier is a critical component of Vault. It is used to wrap
|
||||
// an untrusted physical backend and provide a single point of encryption,
|
||||
// decryption and checksum verification. The goal is to ensure that any
|
||||
// data written to the barrier is confidential and that integrity is preserved.
|
||||
// As a real-world analogy, this is the steel and concrete wrapper around
|
||||
// a Vault. The barrier should only be Unlockable given its key.
|
||||
type SecurityBarrier interface {
|
||||
// Initialized checks if the barrier has been initialized
|
||||
// and has a master key set.
|
||||
Initialized() (bool, error)
|
||||
|
||||
// Initialize works only if the barrier has not been initialized
|
||||
// and makes use of the given master key.
|
||||
Initialize([]byte) error
|
||||
|
||||
// GenerateKey is used to generate a new key
|
||||
GenerateKey() ([]byte, error)
|
||||
|
||||
// Sealed checks if the barrier has been unlocked yet. The Barrier
|
||||
// is not expected to be able to perform any CRUD until it is unsealed.
|
||||
Sealed() (bool, error)
|
||||
|
||||
// Unseal is used to provide the master key which permits the barrier
|
||||
// to be unsealed. If the key is not correct, the barrier remains sealed.
|
||||
Unseal(key []byte) error
|
||||
|
||||
// Seal is used to re-seal the barrier. This requires the barrier to
|
||||
// be unsealed again to perform any further operations.
|
||||
Seal() error
|
||||
|
||||
// Put is used to insert or update an entry
|
||||
Put(entry *Entry) error
|
||||
|
||||
// Get is used to fetch an entry
|
||||
Get(key string) (*Entry, error)
|
||||
|
||||
// Delete is used to permanently delete an entry
|
||||
Delete(key string) error
|
||||
|
||||
// List is used ot list all the keys under a given
|
||||
// prefix, up to the next prefix.
|
||||
List(prefix string) ([]string, error)
|
||||
}
|
||||
|
||||
// Entry is used to represent data stored by the security barrier
|
||||
type Entry struct {
|
||||
Key string
|
||||
Value []byte
|
||||
}
|
|
@ -0,0 +1,292 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/vault/physical"
|
||||
)
|
||||
|
||||
const (
|
||||
// aesgcmVersionByte is prefixed to a message to allow for
|
||||
// future versioning of barrier implementations.
|
||||
aesgcmVersionByte = 0x1
|
||||
)
|
||||
|
||||
// AESGCMBarrier is a SecurityBarrier implementation that
|
||||
// uses a 128bit AES encryption cipher with the Galois Counter Mode.
|
||||
// AES-GCM is high performance, and provides both confidentiality
|
||||
// and integrity.
|
||||
type AESGCMBarrier struct {
|
||||
backend physical.Backend
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// NewAESGCMBarrier is used to construct a new barrier that uses
|
||||
// the provided physical backend for storage.
|
||||
func NewAESGCMBarrier(physical physical.Backend) (*AESGCMBarrier, error) {
|
||||
b := &AESGCMBarrier{
|
||||
backend: physical,
|
||||
sealed: true,
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check for initialization: %v", err)
|
||||
}
|
||||
return out != nil, nil
|
||||
}
|
||||
|
||||
// Initialize works only if the barrier has not been initialized
|
||||
// and makes use of the given master key.
|
||||
func (b *AESGCMBarrier) Initialize(key []byte) error {
|
||||
// Verify the key size
|
||||
if len(key) != aes.BlockSize {
|
||||
return fmt.Errorf("Key size must be %d", aes.BlockSize)
|
||||
}
|
||||
|
||||
// Check if already initialized
|
||||
if alreadyInit, err := b.Initialized(); err != nil {
|
||||
return err
|
||||
} else if alreadyInit {
|
||||
return ErrBarrierAlreadyInit
|
||||
}
|
||||
|
||||
// Create the AES-GCM
|
||||
gcm, err := b.aeadFromKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate encryption key
|
||||
encrypt, err := b.GenerateKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate encryption key: %v", err)
|
||||
}
|
||||
defer memzero(encrypt)
|
||||
|
||||
// Generate the barrier init value
|
||||
value := b.encrypt(gcm, encrypt)
|
||||
|
||||
// Create the barrierInitPath
|
||||
init := &physical.Entry{
|
||||
Key: barrierInitPath,
|
||||
Value: value,
|
||||
}
|
||||
if err := b.backend.Put(init); err != nil {
|
||||
return fmt.Errorf("failed to create initialization key: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateKey is used to generate a new key
|
||||
func (b *AESGCMBarrier) GenerateKey() ([]byte, error) {
|
||||
buf := make([]byte, aes.BlockSize)
|
||||
_, err := rand.Read(buf)
|
||||
return buf, err
|
||||
}
|
||||
|
||||
// Sealed checks if the barrier has been unlocked yet. The Barrier
|
||||
// is not expected to be able to perform any CRUD until it is unsealed.
|
||||
func (b *AESGCMBarrier) Sealed() (bool, error) {
|
||||
b.l.RLock()
|
||||
defer b.l.RUnlock()
|
||||
return b.sealed, nil
|
||||
}
|
||||
|
||||
// Unseal is used to provide the master key which permits the barrier
|
||||
// to be unsealed. If the key is not correct, the barrier remains sealed.
|
||||
func (b *AESGCMBarrier) Unseal(key []byte) error {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
// Do nothing if already unsealed
|
||||
if !b.sealed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read the barrier initialization key
|
||||
out, err := b.backend.Get(barrierInitPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check for initialization: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
return ErrBarrierNotInit
|
||||
}
|
||||
|
||||
// Create the AES-GCM
|
||||
gcm, err := b.aeadFromKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Decrypt the barrier init key
|
||||
encryptKey, err := b.decrypt(gcm, out.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer memzero(encryptKey)
|
||||
|
||||
// Initialize the master encryption key
|
||||
b.primary, err = b.aeadFromKey(encryptKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the vault as unsealed
|
||||
b.sealed = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Seal is used to re-seal the barrier. This requires the barrier to
|
||||
// be unsealed again to perform any further operations.
|
||||
func (b *AESGCMBarrier) Seal() error {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
// Remove the primary key, and seal the vault
|
||||
b.primary = nil
|
||||
b.sealed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put is used to insert or update an entry
|
||||
func (b *AESGCMBarrier) Put(entry *Entry) error {
|
||||
b.l.RLock()
|
||||
defer b.l.RUnlock()
|
||||
|
||||
primary := b.primary
|
||||
if primary == nil {
|
||||
return ErrBarrierSealed
|
||||
}
|
||||
|
||||
pe := &physical.Entry{
|
||||
Key: entry.Key,
|
||||
Value: b.encrypt(primary, entry.Value),
|
||||
}
|
||||
return b.backend.Put(pe)
|
||||
}
|
||||
|
||||
// Get is used to fetch an entry
|
||||
func (b *AESGCMBarrier) Get(key string) (*Entry, error) {
|
||||
b.l.RLock()
|
||||
defer b.l.RUnlock()
|
||||
|
||||
primary := b.primary
|
||||
if primary == nil {
|
||||
return nil, ErrBarrierSealed
|
||||
}
|
||||
|
||||
// Read the key from the backend
|
||||
pe, err := b.backend.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if pe == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Decrypt the ciphertext
|
||||
plain, err := b.decrypt(primary, pe.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decryption failed: %v", err)
|
||||
}
|
||||
|
||||
// Wrap in a logical entry
|
||||
entry := &Entry{
|
||||
Key: key,
|
||||
Value: plain,
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// Delete is used to permanently delete an entry
|
||||
func (b *AESGCMBarrier) Delete(key string) error {
|
||||
b.l.RLock()
|
||||
defer b.l.RUnlock()
|
||||
if b.sealed {
|
||||
return ErrBarrierSealed
|
||||
}
|
||||
|
||||
return b.backend.Delete(key)
|
||||
}
|
||||
|
||||
// List is used ot list all the keys under a given
|
||||
// prefix, up to the next prefix.
|
||||
func (b *AESGCMBarrier) List(prefix string) ([]string, error) {
|
||||
b.l.RLock()
|
||||
defer b.l.RUnlock()
|
||||
if b.sealed {
|
||||
return nil, ErrBarrierSealed
|
||||
}
|
||||
|
||||
return b.backend.List(prefix)
|
||||
}
|
||||
|
||||
// aeadFromKey returns an AES-GCM AEAD using the given key.
|
||||
func (b *AESGCMBarrier) aeadFromKey(key []byte) (cipher.AEAD, error) {
|
||||
// Create the AES cipher
|
||||
aesCipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %v", err)
|
||||
}
|
||||
|
||||
// Create the GCM mode AEAD
|
||||
gcm, err := cipher.NewGCM(aesCipher)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize GCM mode")
|
||||
}
|
||||
return gcm, nil
|
||||
}
|
||||
|
||||
// encrypt is used to encrypt a value
|
||||
func (b *AESGCMBarrier) encrypt(gcm cipher.AEAD, plain []byte) []byte {
|
||||
// Allocate the output buffer with room for version byte,
|
||||
// nonce, GCM tag and the plaintext
|
||||
capacity := 1 + gcm.NonceSize() + gcm.Overhead() + len(plain)
|
||||
size := 1 + gcm.NonceSize()
|
||||
out := make([]byte, size, capacity)
|
||||
|
||||
// Set the version byte
|
||||
out[0] = aesgcmVersionByte
|
||||
|
||||
// Generate a random nonce
|
||||
nonce := out[1 : 1+gcm.NonceSize()]
|
||||
rand.Read(nonce)
|
||||
|
||||
// Seal the output
|
||||
out = gcm.Seal(out, nonce, plain, nil)
|
||||
return out
|
||||
}
|
||||
|
||||
// decrypt is used to decrypt a value
|
||||
func (b *AESGCMBarrier) decrypt(gcm cipher.AEAD, cipher []byte) ([]byte, error) {
|
||||
// Verify the version byte
|
||||
if cipher[0] != aesgcmVersionByte {
|
||||
return nil, fmt.Errorf("version bytes mis-match")
|
||||
}
|
||||
|
||||
// Capture the parts
|
||||
nonce := cipher[1 : 1+gcm.NonceSize()]
|
||||
raw := cipher[1+gcm.NonceSize():]
|
||||
out := make([]byte, 0, len(raw)-gcm.NonceSize())
|
||||
|
||||
// Attempt to open
|
||||
return gcm.Open(out, nonce, raw, nil)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/physical"
|
||||
)
|
||||
|
||||
func TestAESGCMBarrier_Basic(t *testing.T) {
|
||||
inm := physical.NewInmem()
|
||||
b, err := NewAESGCMBarrier(inm)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
testBarrier(t, b)
|
||||
}
|
||||
|
||||
func TestAESGCMBarrier_Confidential(t *testing.T) {
|
||||
// TODO: Verify data sent through is encrypted
|
||||
}
|
||||
|
||||
func TestAESGCMBarrier_Integrity(t *testing.T) {
|
||||
// TODO: Verify data sent through is cannot be tampered
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testBarrier(t *testing.T, b SecurityBarrier) {
|
||||
// Should not be initialized
|
||||
init, err := b.Initialized()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if init {
|
||||
t.Fatalf("should not be initialized")
|
||||
}
|
||||
|
||||
// Should start sealed
|
||||
sealed, err := b.Sealed()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !sealed {
|
||||
t.Fatalf("should be sealed")
|
||||
}
|
||||
|
||||
// Sealing should be a no-op
|
||||
if err := b.Seal(); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// All operations should fail
|
||||
e := &Entry{Key: "test", Value: []byte("test")}
|
||||
if err := b.Put(e); err != ErrBarrierSealed {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if _, err := b.Get("test"); err != ErrBarrierSealed {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if err := b.Delete("test"); err != ErrBarrierSealed {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if _, err := b.List(""); err != ErrBarrierSealed {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Get a new key
|
||||
key, err := b.GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Unseal should not work
|
||||
if err := b.Unseal(key); err != ErrBarrierNotInit {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Initialize the vault
|
||||
if err := b.Initialize(key); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Double Initialize should fail
|
||||
if err := b.Initialize(key); err != ErrBarrierAlreadyInit {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Should be initialized
|
||||
init, err = b.Initialized()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !init {
|
||||
t.Fatalf("should be initialized")
|
||||
}
|
||||
|
||||
// Should still be sealed
|
||||
sealed, err = b.Sealed()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !sealed {
|
||||
t.Fatalf("should sealed")
|
||||
}
|
||||
|
||||
// Unseal should work
|
||||
if err := b.Unseal(key); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Should no longer be sealed
|
||||
sealed, err = b.Sealed()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if sealed {
|
||||
t.Fatalf("should be unsealed")
|
||||
}
|
||||
|
||||
// Operations should work
|
||||
out, err := b.Get("test")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatalf("bad: %v", out)
|
||||
}
|
||||
|
||||
// List should have only "barrier/"
|
||||
keys, err := b.List("")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(keys) != 1 || keys[0] != "barrier/" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
// Try to write
|
||||
if err := b.Put(e); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Should be equal
|
||||
out, err = b.Get("test")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(out, e) {
|
||||
t.Fatalf("bad: %v exp: %v", out, e)
|
||||
}
|
||||
|
||||
// List should show the items
|
||||
keys, err = b.List("")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(keys) != 2 {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
if keys[0] != "barrier/" || keys[1] != "test" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
// Delete should clear
|
||||
err = b.Delete("test")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Double Delete is fine
|
||||
err = b.Delete("test")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Should be nil
|
||||
out, err = b.Get("test")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatalf("bad: %v", out)
|
||||
}
|
||||
|
||||
// List should have nothing
|
||||
keys, err = b.List("")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(keys) != 1 || keys[0] != "barrier/" {
|
||||
t.Fatalf("bad: %v", keys)
|
||||
}
|
||||
|
||||
// Add the item back
|
||||
if err := b.Put(e); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Reseal should prevent any updates
|
||||
if err := b.Seal(); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// No access allowed
|
||||
if _, err := b.Get("test"); err != ErrBarrierSealed {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Unseal should work
|
||||
if err := b.Unseal(key); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Should be equal
|
||||
out, err = b.Get("test")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(out, e) {
|
||||
t.Fatalf("bad: %v exp: %v", out, e)
|
||||
}
|
||||
|
||||
// Final cleanup
|
||||
err = b.Delete("test")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue