docs/signer/keydbstore/rethink_keydbstore.go

266 lines
8.1 KiB
Go

package keydbstore
import (
"errors"
"fmt"
"sync"
"time"
"github.com/docker/notary"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/storage/rethinkdb"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/utils"
jose "github.com/dvsekhvalnov/jose2go"
"gopkg.in/dancannon/gorethink.v2"
)
// RethinkDBKeyStore persists and manages private keys on a RethinkDB database
type RethinkDBKeyStore struct {
lock *sync.Mutex
sess *gorethink.Session
dbName string
defaultPassAlias string
retriever passphrase.Retriever
cachedKeys map[string]data.PrivateKey
}
// RDBPrivateKey represents a PrivateKey in the rethink database
type RDBPrivateKey struct {
rethinkdb.Timing
KeyID string `gorethink:"key_id"`
EncryptionAlg string `gorethink:"encryption_alg"`
KeywrapAlg string `gorethink:"keywrap_alg"`
Algorithm string `gorethink:"algorithm"`
PassphraseAlias string `gorethink:"passphrase_alias"`
Public string `gorethink:"public"`
Private string `gorethink:"private"`
}
var privateKeys = rethinkdb.Table{
Name: RDBPrivateKey{}.TableName(),
PrimaryKey: RDBPrivateKey{}.KeyID,
}
// TableName sets a specific table name for our RDBPrivateKey
func (g RDBPrivateKey) TableName() string {
return "private_keys"
}
// NewRethinkDBKeyStore returns a new RethinkDBKeyStore backed by a RethinkDB database
func NewRethinkDBKeyStore(dbName string, passphraseRetriever passphrase.Retriever, defaultPassAlias string, rethinkSession *gorethink.Session) *RethinkDBKeyStore {
cachedKeys := make(map[string]data.PrivateKey)
return &RethinkDBKeyStore{
lock: &sync.Mutex{},
sess: rethinkSession,
defaultPassAlias: defaultPassAlias,
dbName: dbName,
retriever: passphraseRetriever,
cachedKeys: cachedKeys,
}
}
// Name returns a user friendly name for the storage location
func (rdb *RethinkDBKeyStore) Name() string {
return "RethinkDB"
}
// AddKey stores the contents of a private key. Both role and gun are ignored,
// we always use Key IDs as name, and don't support aliases
func (rdb *RethinkDBKeyStore) AddKey(keyInfo trustmanager.KeyInfo, privKey data.PrivateKey) error {
passphrase, _, err := rdb.retriever(privKey.ID(), rdb.defaultPassAlias, false, 1)
if err != nil {
return err
}
encryptedKey, err := jose.Encrypt(string(privKey.Private()), KeywrapAlg, EncryptionAlg, passphrase)
if err != nil {
return err
}
now := time.Now()
rethinkPrivKey := RDBPrivateKey{
Timing: rethinkdb.Timing{
CreatedAt: now,
UpdatedAt: now,
},
KeyID: privKey.ID(),
EncryptionAlg: EncryptionAlg,
KeywrapAlg: KeywrapAlg,
PassphraseAlias: rdb.defaultPassAlias,
Algorithm: privKey.Algorithm(),
Public: string(privKey.Public()),
Private: encryptedKey}
// Add encrypted private key to the database
_, err = gorethink.DB(rdb.dbName).Table(rethinkPrivKey.TableName()).Insert(rethinkPrivKey).RunWrite(rdb.sess)
if err != nil {
return fmt.Errorf("failed to add private key to database: %s", privKey.ID())
}
// Add the private key to our cache
rdb.lock.Lock()
defer rdb.lock.Unlock()
rdb.cachedKeys[privKey.ID()] = privKey
return nil
}
// GetKey returns the PrivateKey given a KeyID
func (rdb *RethinkDBKeyStore) GetKey(name string) (data.PrivateKey, string, error) {
rdb.lock.Lock()
defer rdb.lock.Unlock()
cachedKeyEntry, ok := rdb.cachedKeys[name]
if ok {
return cachedKeyEntry, "", nil
}
// Retrieve the RethinkDB private key from the database
dbPrivateKey := RDBPrivateKey{}
res, err := gorethink.DB(rdb.dbName).Table(dbPrivateKey.TableName()).Filter(gorethink.Row.Field("key_id").Eq(name)).Run(rdb.sess)
if err != nil {
return nil, "", trustmanager.ErrKeyNotFound{}
}
defer res.Close()
err = res.One(&dbPrivateKey)
if err != nil {
return nil, "", trustmanager.ErrKeyNotFound{}
}
// Get the passphrase to use for this key
passphrase, _, err := rdb.retriever(dbPrivateKey.KeyID, dbPrivateKey.PassphraseAlias, false, 1)
if err != nil {
return nil, "", err
}
// Decrypt private bytes from the gorm key
decryptedPrivKey, _, err := jose.Decode(dbPrivateKey.Private, passphrase)
if err != nil {
return nil, "", err
}
pubKey := data.NewPublicKey(dbPrivateKey.Algorithm, []byte(dbPrivateKey.Public))
// Create a new PrivateKey with unencrypted bytes
privKey, err := data.NewPrivateKey(pubKey, []byte(decryptedPrivKey))
if err != nil {
return nil, "", err
}
// Add the key to cache
rdb.cachedKeys[privKey.ID()] = privKey
return privKey, "", nil
}
// GetKeyInfo always returns empty and an error. This method is here to satisfy the KeyStore interface
func (rdb RethinkDBKeyStore) GetKeyInfo(name string) (trustmanager.KeyInfo, error) {
return trustmanager.KeyInfo{}, fmt.Errorf("GetKeyInfo currently not supported for RethinkDBKeyStore, as it does not track roles or GUNs")
}
// ListKeys always returns nil. This method is here to satisfy the KeyStore interface
func (rdb RethinkDBKeyStore) ListKeys() map[string]trustmanager.KeyInfo {
return nil
}
// RemoveKey removes the key from the table
func (rdb RethinkDBKeyStore) RemoveKey(keyID string) error {
rdb.lock.Lock()
defer rdb.lock.Unlock()
delete(rdb.cachedKeys, keyID)
// Delete the key from the database
dbPrivateKey := RDBPrivateKey{KeyID: keyID}
_, err := gorethink.DB(rdb.dbName).Table(dbPrivateKey.TableName()).Filter(gorethink.Row.Field("key_id").Eq(keyID)).Delete().RunWrite(rdb.sess)
if err != nil {
return fmt.Errorf("unable to delete private key from database: %s", err.Error())
}
return nil
}
// RotateKeyPassphrase rotates the key-encryption-key
func (rdb RethinkDBKeyStore) RotateKeyPassphrase(name, newPassphraseAlias string) error {
// Retrieve the RethinkDB private key from the database
dbPrivateKey := RDBPrivateKey{KeyID: name}
res, err := gorethink.DB(rdb.dbName).Table(dbPrivateKey.TableName()).Get(dbPrivateKey).Run(rdb.sess)
if err != nil {
return trustmanager.ErrKeyNotFound{}
}
defer res.Close()
err = res.One(&dbPrivateKey)
if err != nil {
return trustmanager.ErrKeyNotFound{}
}
// Get the current passphrase to use for this key
passphrase, _, err := rdb.retriever(dbPrivateKey.KeyID, dbPrivateKey.PassphraseAlias, false, 1)
if err != nil {
return err
}
// Decrypt private bytes from the rethinkDB key
decryptedPrivKey, _, err := jose.Decode(dbPrivateKey.Private, passphrase)
if err != nil {
return err
}
// Get the new passphrase to use for this key
newPassphrase, _, err := rdb.retriever(dbPrivateKey.KeyID, newPassphraseAlias, false, 1)
if err != nil {
return err
}
// Re-encrypt the private bytes with the new passphrase
newEncryptedKey, err := jose.Encrypt(decryptedPrivKey, KeywrapAlg, EncryptionAlg, newPassphrase)
if err != nil {
return err
}
// Update the database object
dbPrivateKey.Private = newEncryptedKey
dbPrivateKey.PassphraseAlias = newPassphraseAlias
if _, err := gorethink.DB(rdb.dbName).Table(dbPrivateKey.TableName()).Get(RDBPrivateKey{KeyID: name}).Update(dbPrivateKey).RunWrite(rdb.sess); err != nil {
return err
}
return nil
}
// ExportKey is currently unimplemented and will always return an error
func (rdb RethinkDBKeyStore) ExportKey(keyID string) ([]byte, error) {
return nil, errors.New("Exporting from a RethinkDBKeyStore is not supported.")
}
// Bootstrap sets up the database and tables, also creating the notary signer user with appropriate db permission
func (rdb RethinkDBKeyStore) Bootstrap() error {
if err := rethinkdb.SetupDB(rdb.sess, rdb.dbName, []rethinkdb.Table{
privateKeys,
}); err != nil {
return err
}
return rethinkdb.CreateAndGrantDBUser(rdb.sess, rdb.dbName, notary.NotarySignerUser, "")
}
// CheckHealth verifies that DB exists and is query-able
func (rdb RethinkDBKeyStore) CheckHealth() error {
var tables []string
dbPrivateKey := RDBPrivateKey{}
res, err := gorethink.DB(rdb.dbName).TableList().Run(rdb.sess)
if err != nil {
return err
}
defer res.Close()
err = res.All(&tables)
if err != nil || !utils.StrSliceContains(tables, dbPrivateKey.TableName()) {
return fmt.Errorf(
"Cannot access table: %s", dbPrivateKey.TableName())
}
return nil
}