mirror of https://github.com/docker/docs.git
Addressed comments, changed to PBES2, added key rotation
Signed-off-by: Diogo Monica <diogo@docker.com>
This commit is contained in:
parent
c7e421a501
commit
a2472a5a72
|
@ -107,7 +107,7 @@ func main() {
|
||||||
log.Fatalf("failed to open the database: %s, %v", dbURL, err)
|
log.Fatalf("failed to open the database: %s, %v", dbURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyStore, err := trustmanager.NewKeyDBStore(passphraseRetriever, _DBType, dbSQL)
|
keyStore, err := trustmanager.NewKeyDBStore(passphraseRetriever, "", _DBType, dbSQL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create a new keydbstore: %v", err)
|
log.Fatalf("failed to create a new keydbstore: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,12 @@ CREATE TABLE `private_keys` (
|
||||||
`updated_at` datetime NOT NULL,
|
`updated_at` datetime NOT NULL,
|
||||||
`deleted_at` datetime DEFAULT NULL,
|
`deleted_at` datetime DEFAULT NULL,
|
||||||
`key_id` varchar(255) NOT NULL,
|
`key_id` varchar(255) NOT NULL,
|
||||||
`encryption` varchar(255) NOT NULL,
|
`encryption_alg` varchar(255) NOT NULL,
|
||||||
|
`keywrap_alg` varchar(255) NOT NULL,
|
||||||
`algorithm` varchar(50) NOT NULL,
|
`algorithm` varchar(50) NOT NULL,
|
||||||
|
`passphrase_alias` varchar(50) NOT NULL,
|
||||||
`public` blob NOT NULL,
|
`public` blob NOT NULL,
|
||||||
`private` longblob NOT NULL,
|
`private` blob NOT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE (`key_id`),
|
UNIQUE (`key_id`),
|
||||||
UNIQUE (`key_id`,`encryption`)
|
UNIQUE (`key_id`,`encryption`)
|
||||||
|
|
|
@ -6,27 +6,35 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/notary/pkg/passphrase"
|
"github.com/docker/notary/pkg/passphrase"
|
||||||
|
jose "github.com/dvsekhvalnov/jose2go"
|
||||||
"github.com/endophage/gotuf/data"
|
"github.com/endophage/gotuf/data"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
gojose "github.com/square/go-jose"
|
)
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const (
|
||||||
|
EncryptionAlg = jose.A256GCM
|
||||||
|
KeywrapAlg = jose.PBES2_HS256_A128KW
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyDBStore persists and manages private keys on a SQL database
|
// KeyDBStore persists and manages private keys on a SQL database
|
||||||
type KeyDBStore struct {
|
type KeyDBStore struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
db gorm.DB
|
db gorm.DB
|
||||||
passphrase string
|
defaultPassAlias string
|
||||||
encrypter gojose.Encrypter
|
retriever passphrase.Retriever
|
||||||
cachedKeys map[string]data.PrivateKey
|
cachedKeys map[string]data.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// GormPrivateKey represents a PrivateKey in the database
|
// GormPrivateKey represents a PrivateKey in the database
|
||||||
type GormPrivateKey struct {
|
type GormPrivateKey struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
KeyID string `sql:"not null;unique"`
|
KeyID string `sql:"not null;unique;index:key_id_idx"`
|
||||||
Encryption string `sql:"not null"`
|
EncryptionAlg string `sql:"not null"`
|
||||||
|
KeywrapAlg string `sql:"not null"`
|
||||||
Algorithm string `sql:"not null"`
|
Algorithm string `sql:"not null"`
|
||||||
Public []byte `sql:"not null"`
|
PassphraseAlias string `sql:"not null"`
|
||||||
|
Public string `sql:"not null"`
|
||||||
Private string `sql:"not null"`
|
Private string `sql:"not null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,47 +44,40 @@ func (g GormPrivateKey) TableName() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKeyDBStore returns a new KeyDBStore backed by a SQL database
|
// NewKeyDBStore returns a new KeyDBStore backed by a SQL database
|
||||||
func NewKeyDBStore(passphraseRetriever passphrase.Retriever, dbType string, dbSQL *sql.DB) (*KeyDBStore, error) {
|
func NewKeyDBStore(passphraseRetriever passphrase.Retriever, defaultPassAlias, dbType string, dbSQL *sql.DB) (*KeyDBStore, error) {
|
||||||
cachedKeys := make(map[string]data.PrivateKey)
|
cachedKeys := make(map[string]data.PrivateKey)
|
||||||
|
|
||||||
// Retreive the passphrase that will be used to encrypt the keys
|
|
||||||
passphrase, _, err := passphraseRetriever("", "", false, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup our encrypted object
|
|
||||||
encrypter, err := gojose.NewEncrypter(gojose.A256GCMKW, gojose.A256GCM, []byte(passphrase))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open a connection to our database
|
// Open a connection to our database
|
||||||
db, _ := gorm.Open(dbType, dbSQL)
|
db, _ := gorm.Open(dbType, dbSQL)
|
||||||
|
|
||||||
return &KeyDBStore{db: db,
|
return &KeyDBStore{db: db,
|
||||||
passphrase: passphrase,
|
defaultPassAlias: defaultPassAlias,
|
||||||
encrypter: encrypter,
|
retriever: passphraseRetriever,
|
||||||
cachedKeys: cachedKeys}, nil
|
cachedKeys: cachedKeys}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddKey stores the contents of a private key. Both name and alias are ignored,
|
// AddKey stores the contents of a private key. Both name and alias are ignored,
|
||||||
// we always use Key IDs as name, and don't support aliases
|
// we always use Key IDs as name, and don't support aliases
|
||||||
func (s *KeyDBStore) AddKey(name, alias string, privKey data.PrivateKey) error {
|
func (s *KeyDBStore) AddKey(name, alias string, privKey data.PrivateKey) error {
|
||||||
encryptedKey, err := s.encrypter.Encrypt(privKey.Private())
|
|
||||||
|
passphrase, _, err := s.retriever(privKey.ID(), s.defaultPassAlias, false, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the private key material
|
encryptedKey, err := jose.Encrypt(string(privKey.Private()), KeywrapAlg, EncryptionAlg, passphrase)
|
||||||
encryptedPrivKeyStr := encryptedKey.FullSerialize()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
gormPrivKey := GormPrivateKey{
|
gormPrivKey := GormPrivateKey{
|
||||||
KeyID: privKey.ID(),
|
KeyID: privKey.ID(),
|
||||||
Encryption: string(gojose.PBES2_HS512_A256KW),
|
EncryptionAlg: EncryptionAlg,
|
||||||
|
KeywrapAlg: KeywrapAlg,
|
||||||
|
PassphraseAlias: s.defaultPassAlias,
|
||||||
Algorithm: privKey.Algorithm().String(),
|
Algorithm: privKey.Algorithm().String(),
|
||||||
Public: privKey.Public(),
|
Public: string(privKey.Public()),
|
||||||
Private: encryptedPrivKeyStr}
|
Private: encryptedKey}
|
||||||
|
|
||||||
// Add encrypted private key to the database
|
// Add encrypted private key to the database
|
||||||
s.db.Create(&gormPrivKey)
|
s.db.Create(&gormPrivKey)
|
||||||
|
@ -109,18 +110,20 @@ func (s *KeyDBStore) GetKey(name string) (data.PrivateKey, string, error) {
|
||||||
return nil, "", ErrKeyNotFound{}
|
return nil, "", ErrKeyNotFound{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt private bytes from the gorm key
|
// Get the passphrase to use for this key
|
||||||
encryptedPrivKeyJWE, err := gojose.ParseEncrypted(dbPrivateKey.Private)
|
passphrase, _, err := s.retriever(dbPrivateKey.KeyID, dbPrivateKey.PassphraseAlias, false, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
decryptedPrivKeyBytes, err := encryptedPrivKeyJWE.Decrypt([]byte(s.passphrase))
|
|
||||||
|
// Decrypt private bytes from the gorm key
|
||||||
|
decryptedPrivKey, _, err := jose.Decode(dbPrivateKey.Private, passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new PrivateKey with unencrypted bytes
|
// Create a new PrivateKey with unencrypted bytes
|
||||||
privKey := data.NewPrivateKey(data.KeyAlgorithm(dbPrivateKey.Algorithm), dbPrivateKey.Public, decryptedPrivKeyBytes)
|
privKey := data.NewPrivateKey(data.KeyAlgorithm(dbPrivateKey.Algorithm), []byte(dbPrivateKey.Public), []byte(decryptedPrivKey))
|
||||||
|
|
||||||
// Add the key to cache
|
// Add the key to cache
|
||||||
s.cachedKeys[privKey.ID()] = privKey
|
s.cachedKeys[privKey.ID()] = privKey
|
||||||
|
@ -151,3 +154,50 @@ func (s *KeyDBStore) RemoveKey(name string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RotateKeyPassphrase rotates the key-encryption-key
|
||||||
|
func (s *KeyDBStore) RotateKeyPassphrase(name, newPassphraseAlias string) error {
|
||||||
|
// Retrieve the GORM private key from the database
|
||||||
|
dbPrivateKey := GormPrivateKey{}
|
||||||
|
if s.db.Where(&GormPrivateKey{KeyID: name}).First(&dbPrivateKey).RecordNotFound() {
|
||||||
|
return ErrKeyNotFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current passphrase to use for this key
|
||||||
|
passphrase, _, err := s.retriever(dbPrivateKey.KeyID, dbPrivateKey.PassphraseAlias, false, 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Got old passphrase: ", passphrase)
|
||||||
|
|
||||||
|
// Decrypt private bytes from the gorm key
|
||||||
|
decryptedPrivKey, _, err := jose.Decode(dbPrivateKey.Private, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the new passphrase to use for this key
|
||||||
|
newPassphrase, _, err := s.retriever(dbPrivateKey.KeyID, newPassphraseAlias, false, 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("new passphrase: ", newPassphrase)
|
||||||
|
|
||||||
|
// Re-encrypt the private bytes with the new passphrase
|
||||||
|
newEncryptedKey, err := jose.Encrypt(decryptedPrivKey, KeywrapAlg, EncryptionAlg, newPassphrase)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("encrypted key: ", newEncryptedKey)
|
||||||
|
|
||||||
|
// Update the database object
|
||||||
|
dbPrivateKey.Private = newEncryptedKey
|
||||||
|
dbPrivateKey.PassphraseAlias = newPassphraseAlias
|
||||||
|
s.db.Save(dbPrivateKey)
|
||||||
|
|
||||||
|
fmt.Printf("DB Private key: %v", dbPrivateKey)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package trustmanager
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -12,7 +13,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var retriever = func(string, string, bool, int) (string, bool, error) {
|
var retriever = func(string, string, bool, int) (string, bool, error) {
|
||||||
return "abcgdhfjdhfjhfgdhejnfhdfgshdjfbv", false, nil
|
return "passphrase-1", false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var anotherRetriever = func(keyName, alias string, createNew bool, attempts int) (string, bool, error) {
|
||||||
|
switch alias {
|
||||||
|
case "alias-1":
|
||||||
|
return "passphrase-1", false, nil
|
||||||
|
case "alias-2":
|
||||||
|
return "passphrase-2", false, nil
|
||||||
|
}
|
||||||
|
return "", false, errors.New("password alias no found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateRead(t *testing.T) {
|
func TestCreateRead(t *testing.T) {
|
||||||
|
@ -27,7 +38,7 @@ func TestCreateRead(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Create a new KeyDB store
|
// Create a new KeyDB store
|
||||||
dbStore, err := NewKeyDBStore(retriever, "sqlite3", db)
|
dbStore, err := NewKeyDBStore(retriever, "", "sqlite3", db)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Ensure that the private_key table exists
|
// Ensure that the private_key table exists
|
||||||
|
@ -69,7 +80,7 @@ func TestDoubleCreate(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Create a new KeyDB store
|
// Create a new KeyDB store
|
||||||
dbStore, err := NewKeyDBStore(retriever, "sqlite3", db)
|
dbStore, err := NewKeyDBStore(retriever, "", "sqlite3", db)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Ensure that the private_key table exists
|
// Ensure that the private_key table exists
|
||||||
|
@ -100,7 +111,7 @@ func TestCreateDelete(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Create a new KeyDB store
|
// Create a new KeyDB store
|
||||||
dbStore, err := NewKeyDBStore(retriever, "sqlite3", db)
|
dbStore, err := NewKeyDBStore(retriever, "", "sqlite3", db)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Ensure that the private_key table exists
|
// Ensure that the private_key table exists
|
||||||
|
@ -118,3 +129,34 @@ func TestCreateDelete(t *testing.T) {
|
||||||
_, _, err = dbStore.GetKey(testKey.ID())
|
_, _, err = dbStore.GetKey(testKey.ID())
|
||||||
assert.Error(t, err, "signing key not found:")
|
assert.Error(t, err, "signing key not found:")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestKeyRotation(t *testing.T) {
|
||||||
|
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||||
|
defer os.RemoveAll(tempBaseDir)
|
||||||
|
|
||||||
|
testKey, err := GenerateECDSAKey(rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// We are using SQLite for the tests
|
||||||
|
db, err := sql.Open("sqlite3", tempBaseDir+"test_db")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a new KeyDB store
|
||||||
|
dbStore, err := NewKeyDBStore(anotherRetriever, "alias-1", "sqlite3", db)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Ensure that the private_key table exists
|
||||||
|
dbStore.db.CreateTable(&GormPrivateKey{})
|
||||||
|
|
||||||
|
// Test writing new key in database/cache
|
||||||
|
err = dbStore.AddKey("", "", testKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Try rotating the key to alias-2
|
||||||
|
err = dbStore.RotateKeyPassphrase(testKey.ID(), "alias-2")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Try rotating the key to alias-3
|
||||||
|
err = dbStore.RotateKeyPassphrase(testKey.ID(), "alias-3")
|
||||||
|
assert.Error(t, err, "password alias no found")
|
||||||
|
}
|
||||||
|
|
|
@ -50,9 +50,3 @@ type cachedKey struct {
|
||||||
alias string
|
alias string
|
||||||
key data.PrivateKey
|
key data.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// PassphraseRetriever is a callback function that should retrieve a passphrase
|
|
||||||
// for a given named key. If it should be treated as new passphrase (e.g. with
|
|
||||||
// confirmation), createNew will be true. Attempts is passed in so that implementers
|
|
||||||
// decide how many chances to give to a human, for example.
|
|
||||||
type PassphraseRetriever func(keyId, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error)
|
|
||||||
|
|
Loading…
Reference in New Issue