mirror of https://github.com/docker/docs.git
498 lines
14 KiB
Go
498 lines
14 KiB
Go
package trustmanager
|
|
|
|
import (
|
|
"encoding/pem"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/notary"
|
|
"github.com/docker/notary/passphrase"
|
|
"github.com/docker/notary/tuf/data"
|
|
)
|
|
|
|
type keyInfoMap map[string]KeyInfo
|
|
|
|
// KeyFileStore persists and manages private keys on disk
|
|
type KeyFileStore struct {
|
|
sync.Mutex
|
|
SimpleFileStore
|
|
passphrase.Retriever
|
|
cachedKeys map[string]*cachedKey
|
|
keyInfoMap
|
|
}
|
|
|
|
// KeyMemoryStore manages private keys in memory
|
|
type KeyMemoryStore struct {
|
|
sync.Mutex
|
|
MemoryFileStore
|
|
passphrase.Retriever
|
|
cachedKeys map[string]*cachedKey
|
|
keyInfoMap
|
|
}
|
|
|
|
// KeyInfo stores the role, path, and gun for a corresponding private key ID
|
|
// It is assumed that each private key ID is unique
|
|
type KeyInfo struct {
|
|
Gun string
|
|
Role string
|
|
}
|
|
|
|
// NewKeyFileStore returns a new KeyFileStore creating a private directory to
|
|
// hold the keys.
|
|
func NewKeyFileStore(baseDir string, passphraseRetriever passphrase.Retriever) (*KeyFileStore, error) {
|
|
baseDir = filepath.Join(baseDir, notary.PrivDir)
|
|
fileStore, err := NewPrivateSimpleFileStore(baseDir, keyExtension)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cachedKeys := make(map[string]*cachedKey)
|
|
keyInfoMap := make(keyInfoMap)
|
|
|
|
keyStore := &KeyFileStore{SimpleFileStore: *fileStore,
|
|
Retriever: passphraseRetriever,
|
|
cachedKeys: cachedKeys,
|
|
keyInfoMap: keyInfoMap,
|
|
}
|
|
|
|
// Load this keystore's ID --> gun/role map
|
|
keyStore.loadKeyInfo()
|
|
return keyStore, nil
|
|
}
|
|
|
|
func generateKeyInfoMap(s Storage) map[string]KeyInfo {
|
|
keyInfoMap := make(map[string]KeyInfo)
|
|
for _, keyPath := range s.ListFiles() {
|
|
d, err := s.Get(keyPath)
|
|
if err != nil {
|
|
logrus.Error(err)
|
|
continue
|
|
}
|
|
keyID, keyInfo, err := KeyInfoFromPEM(d, keyPath)
|
|
if err != nil {
|
|
logrus.Error(err)
|
|
continue
|
|
}
|
|
keyInfoMap[keyID] = keyInfo
|
|
}
|
|
return keyInfoMap
|
|
}
|
|
|
|
// Attempts to infer the keyID, role, and GUN from the specified key path.
|
|
// Note that non-root roles can only be inferred if this is a legacy style filename: KEYID_ROLE.key
|
|
func inferKeyInfoFromKeyPath(keyPath string) (string, string, string) {
|
|
var keyID, role, gun string
|
|
keyID = filepath.Base(keyPath)
|
|
underscoreIndex := strings.LastIndex(keyID, "_")
|
|
|
|
// This is the legacy KEYID_ROLE filename
|
|
// The keyID is the first part of the keyname
|
|
// The keyRole is the second part of the keyname
|
|
// in a key named abcde_root, abcde is the keyID and root is the KeyAlias
|
|
if underscoreIndex != -1 {
|
|
role = keyID[underscoreIndex+1:]
|
|
keyID = keyID[:underscoreIndex]
|
|
}
|
|
|
|
if filepath.HasPrefix(keyPath, notary.RootKeysSubdir+"/") {
|
|
return keyID, data.CanonicalRootRole, ""
|
|
}
|
|
|
|
keyPath = strings.TrimPrefix(keyPath, notary.NonRootKeysSubdir+"/")
|
|
gun = getGunFromFullID(keyPath)
|
|
return keyID, role, gun
|
|
}
|
|
|
|
func getGunFromFullID(fullKeyID string) string {
|
|
keyGun := filepath.Dir(fullKeyID)
|
|
// If the gun is empty, Dir will return .
|
|
if keyGun == "." {
|
|
keyGun = ""
|
|
}
|
|
return keyGun
|
|
}
|
|
|
|
func (s *KeyFileStore) loadKeyInfo() {
|
|
s.keyInfoMap = generateKeyInfoMap(s)
|
|
}
|
|
|
|
func (s *KeyMemoryStore) loadKeyInfo() {
|
|
s.keyInfoMap = generateKeyInfoMap(s)
|
|
}
|
|
|
|
// GetKeyInfo returns the corresponding gun and role key info for a keyID
|
|
func (s *KeyFileStore) GetKeyInfo(keyID string) (KeyInfo, error) {
|
|
if info, ok := s.keyInfoMap[keyID]; ok {
|
|
return info, nil
|
|
}
|
|
return KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID)
|
|
}
|
|
|
|
// GetKeyInfo returns the corresponding gun and role key info for a keyID
|
|
func (s *KeyMemoryStore) GetKeyInfo(keyID string) (KeyInfo, error) {
|
|
if info, ok := s.keyInfoMap[keyID]; ok {
|
|
return info, nil
|
|
}
|
|
return KeyInfo{}, fmt.Errorf("Could not find info for keyID %s", keyID)
|
|
}
|
|
|
|
// Name returns a user friendly name for the location this store
|
|
// keeps its data
|
|
func (s *KeyFileStore) Name() string {
|
|
return fmt.Sprintf("file (%s)", s.SimpleFileStore.BaseDir())
|
|
}
|
|
|
|
// AddKey stores the contents of a PEM-encoded private key as a PEM block
|
|
func (s *KeyFileStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
if keyInfo.Role == data.CanonicalRootRole || data.IsDelegation(keyInfo.Role) || !data.ValidRole(keyInfo.Role) {
|
|
keyInfo.Gun = ""
|
|
}
|
|
err := addKey(s, s.Retriever, s.cachedKeys, filepath.Join(keyInfo.Gun, privKey.ID()), keyInfo.Role, privKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.keyInfoMap[privKey.ID()] = keyInfo
|
|
return nil
|
|
}
|
|
|
|
// GetKey returns the PrivateKey given a KeyID
|
|
func (s *KeyFileStore) GetKey(name string) (data.PrivateKey, string, error) {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
// If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds
|
|
if keyInfo, ok := s.keyInfoMap[name]; ok {
|
|
name = filepath.Join(keyInfo.Gun, name)
|
|
}
|
|
return getKey(s, s.Retriever, s.cachedKeys, name)
|
|
}
|
|
|
|
// ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap
|
|
func (s *KeyFileStore) ListKeys() map[string]KeyInfo {
|
|
return copyKeyInfoMap(s.keyInfoMap)
|
|
}
|
|
|
|
// RemoveKey removes the key from the keyfilestore
|
|
func (s *KeyFileStore) RemoveKey(keyID string) error {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
// If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds
|
|
if keyInfo, ok := s.keyInfoMap[keyID]; ok {
|
|
keyID = filepath.Join(keyInfo.Gun, keyID)
|
|
}
|
|
err := removeKey(s, s.cachedKeys, keyID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Remove this key from our keyInfo map if we removed from our filesystem
|
|
delete(s.keyInfoMap, filepath.Base(keyID))
|
|
return nil
|
|
}
|
|
|
|
// ExportKey exports the encrypted bytes from the keystore
|
|
func (s *KeyFileStore) ExportKey(keyID string) ([]byte, error) {
|
|
if keyInfo, ok := s.keyInfoMap[keyID]; ok {
|
|
keyID = filepath.Join(keyInfo.Gun, keyID)
|
|
}
|
|
keyBytes, _, err := getRawKey(s, keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return keyBytes, nil
|
|
}
|
|
|
|
// NewKeyMemoryStore returns a new KeyMemoryStore which holds keys in memory
|
|
func NewKeyMemoryStore(passphraseRetriever passphrase.Retriever) *KeyMemoryStore {
|
|
memStore := NewMemoryFileStore()
|
|
cachedKeys := make(map[string]*cachedKey)
|
|
|
|
keyInfoMap := make(keyInfoMap)
|
|
|
|
keyStore := &KeyMemoryStore{MemoryFileStore: *memStore,
|
|
Retriever: passphraseRetriever,
|
|
cachedKeys: cachedKeys,
|
|
keyInfoMap: keyInfoMap,
|
|
}
|
|
|
|
// Load this keystore's ID --> gun/role map
|
|
keyStore.loadKeyInfo()
|
|
return keyStore
|
|
}
|
|
|
|
// Name returns a user friendly name for the location this store
|
|
// keeps its data
|
|
func (s *KeyMemoryStore) Name() string {
|
|
return "memory"
|
|
}
|
|
|
|
// AddKey stores the contents of a PEM-encoded private key as a PEM block
|
|
func (s *KeyMemoryStore) AddKey(keyInfo KeyInfo, privKey data.PrivateKey) error {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
if keyInfo.Role == data.CanonicalRootRole || data.IsDelegation(keyInfo.Role) || !data.ValidRole(keyInfo.Role) {
|
|
keyInfo.Gun = ""
|
|
}
|
|
err := addKey(s, s.Retriever, s.cachedKeys, filepath.Join(keyInfo.Gun, privKey.ID()), keyInfo.Role, privKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.keyInfoMap[privKey.ID()] = keyInfo
|
|
return nil
|
|
}
|
|
|
|
// GetKey returns the PrivateKey given a KeyID
|
|
func (s *KeyMemoryStore) GetKey(name string) (data.PrivateKey, string, error) {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
// If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds
|
|
if keyInfo, ok := s.keyInfoMap[name]; ok {
|
|
name = filepath.Join(keyInfo.Gun, name)
|
|
}
|
|
return getKey(s, s.Retriever, s.cachedKeys, name)
|
|
}
|
|
|
|
// ListKeys returns a list of unique PublicKeys present on the KeyFileStore, by returning a copy of the keyInfoMap
|
|
func (s *KeyMemoryStore) ListKeys() map[string]KeyInfo {
|
|
return copyKeyInfoMap(s.keyInfoMap)
|
|
}
|
|
|
|
// copyKeyInfoMap returns a deep copy of the passed-in keyInfoMap
|
|
func copyKeyInfoMap(keyInfoMap map[string]KeyInfo) map[string]KeyInfo {
|
|
copyMap := make(map[string]KeyInfo)
|
|
for keyID, keyInfo := range keyInfoMap {
|
|
copyMap[keyID] = KeyInfo{Role: keyInfo.Role, Gun: keyInfo.Gun}
|
|
}
|
|
return copyMap
|
|
}
|
|
|
|
// RemoveKey removes the key from the keystore
|
|
func (s *KeyMemoryStore) RemoveKey(keyID string) error {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
// If this is a bare key ID without the gun, prepend the gun so the filestore lookup succeeds
|
|
if keyInfo, ok := s.keyInfoMap[keyID]; ok {
|
|
keyID = filepath.Join(keyInfo.Gun, keyID)
|
|
}
|
|
err := removeKey(s, s.cachedKeys, keyID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Remove this key from our keyInfo map if we removed from our filesystem
|
|
delete(s.keyInfoMap, filepath.Base(keyID))
|
|
return nil
|
|
}
|
|
|
|
// ExportKey exports the encrypted bytes from the keystore
|
|
func (s *KeyMemoryStore) ExportKey(keyID string) ([]byte, error) {
|
|
keyBytes, _, err := getRawKey(s, keyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return keyBytes, nil
|
|
}
|
|
|
|
// KeyInfoFromPEM attempts to get a keyID and KeyInfo from the filename and PEM bytes of a key
|
|
func KeyInfoFromPEM(pemBytes []byte, filename string) (string, KeyInfo, error) {
|
|
keyID, role, gun := inferKeyInfoFromKeyPath(filename)
|
|
if role == "" {
|
|
block, _ := pem.Decode(pemBytes)
|
|
if block == nil {
|
|
return "", KeyInfo{}, fmt.Errorf("could not decode PEM block for key %s", filename)
|
|
}
|
|
if keyRole, ok := block.Headers["role"]; ok {
|
|
role = keyRole
|
|
}
|
|
}
|
|
return keyID, KeyInfo{Gun: gun, Role: role}, nil
|
|
}
|
|
|
|
func addKey(s Storage, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error {
|
|
|
|
var (
|
|
chosenPassphrase string
|
|
giveup bool
|
|
err error
|
|
)
|
|
|
|
for attempts := 0; ; attempts++ {
|
|
chosenPassphrase, giveup, err = passphraseRetriever(name, role, true, attempts)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if giveup {
|
|
return ErrAttemptsExceeded{}
|
|
}
|
|
if attempts > 10 {
|
|
return ErrAttemptsExceeded{}
|
|
}
|
|
break
|
|
}
|
|
|
|
return encryptAndAddKey(s, chosenPassphrase, cachedKeys, name, role, privKey)
|
|
}
|
|
|
|
// getKeyRole finds the role for the given keyID. It attempts to look
|
|
// both in the newer format PEM headers, and also in the legacy filename
|
|
// format. It returns: the role, whether it was found in the legacy format
|
|
// (true == legacy), and an error
|
|
func getKeyRole(s Storage, keyID string) (string, bool, error) {
|
|
name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID)))
|
|
|
|
for _, file := range s.ListFiles() {
|
|
filename := filepath.Base(file)
|
|
|
|
if strings.HasPrefix(filename, name) {
|
|
d, err := s.Get(file)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
block, _ := pem.Decode(d)
|
|
if block != nil {
|
|
if role, ok := block.Headers["role"]; ok {
|
|
return role, false, nil
|
|
}
|
|
}
|
|
|
|
role := strings.TrimPrefix(filename, name+"_")
|
|
return role, true, nil
|
|
}
|
|
}
|
|
|
|
return "", false, ErrKeyNotFound{KeyID: keyID}
|
|
}
|
|
|
|
// GetKey returns the PrivateKey given a KeyID
|
|
func getKey(s Storage, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name string) (data.PrivateKey, string, error) {
|
|
cachedKeyEntry, ok := cachedKeys[name]
|
|
if ok {
|
|
return cachedKeyEntry.key, cachedKeyEntry.alias, nil
|
|
}
|
|
|
|
keyBytes, keyAlias, err := getRawKey(s, name)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// See if the key is encrypted. If its encrypted we'll fail to parse the private key
|
|
privKey, err := ParsePEMPrivateKey(keyBytes, "")
|
|
if err != nil {
|
|
privKey, _, err = GetPasswdDecryptBytes(passphraseRetriever, keyBytes, name, string(keyAlias))
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
}
|
|
cachedKeys[name] = &cachedKey{alias: keyAlias, key: privKey}
|
|
return privKey, keyAlias, nil
|
|
}
|
|
|
|
// RemoveKey removes the key from the keyfilestore
|
|
func removeKey(s Storage, cachedKeys map[string]*cachedKey, name string) error {
|
|
role, legacy, err := getKeyRole(s, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
delete(cachedKeys, name)
|
|
|
|
if legacy {
|
|
name = name + "_" + role
|
|
}
|
|
|
|
// being in a subdirectory is for backwards compatibliity
|
|
err = s.Remove(filepath.Join(getSubdir(role), name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Assumes 2 subdirectories, 1 containing root keys and 1 containing tuf keys
|
|
func getSubdir(alias string) string {
|
|
if alias == data.CanonicalRootRole {
|
|
return notary.RootKeysSubdir
|
|
}
|
|
return notary.NonRootKeysSubdir
|
|
}
|
|
|
|
// Given a key ID, gets the bytes and alias belonging to that key if the key
|
|
// exists
|
|
func getRawKey(s Storage, name string) ([]byte, string, error) {
|
|
role, legacy, err := getKeyRole(s, name)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
if legacy {
|
|
name = name + "_" + role
|
|
}
|
|
|
|
var keyBytes []byte
|
|
keyBytes, err = s.Get(filepath.Join(getSubdir(role), name))
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
return keyBytes, role, nil
|
|
}
|
|
|
|
// GetPasswdDecryptBytes gets the password to decrypt the given pem bytes.
|
|
// Returns the password and private key
|
|
func GetPasswdDecryptBytes(passphraseRetriever passphrase.Retriever, pemBytes []byte, name, alias string) (data.PrivateKey, string, error) {
|
|
var (
|
|
passwd string
|
|
retErr error
|
|
privKey data.PrivateKey
|
|
)
|
|
for attempts := 0; ; attempts++ {
|
|
var (
|
|
giveup bool
|
|
err error
|
|
)
|
|
passwd, giveup, err = passphraseRetriever(name, alias, false, attempts)
|
|
// Check if the passphrase retriever got an error or if it is telling us to give up
|
|
if giveup || err != nil {
|
|
return nil, "", ErrPasswordInvalid{}
|
|
}
|
|
if attempts > 10 {
|
|
return nil, "", ErrAttemptsExceeded{}
|
|
}
|
|
|
|
// Try to convert PEM encoded bytes back to a PrivateKey using the passphrase
|
|
privKey, err = ParsePEMPrivateKey(pemBytes, passwd)
|
|
if err != nil {
|
|
retErr = ErrPasswordInvalid{}
|
|
} else {
|
|
// We managed to parse the PrivateKey. We've succeeded!
|
|
retErr = nil
|
|
break
|
|
}
|
|
}
|
|
if retErr != nil {
|
|
return nil, "", retErr
|
|
}
|
|
return privKey, passwd, nil
|
|
}
|
|
|
|
func encryptAndAddKey(s Storage, passwd string, cachedKeys map[string]*cachedKey, name, role string, privKey data.PrivateKey) error {
|
|
|
|
var (
|
|
pemPrivKey []byte
|
|
err error
|
|
)
|
|
|
|
if passwd != "" {
|
|
pemPrivKey, err = EncryptPrivateKey(privKey, role, passwd)
|
|
} else {
|
|
pemPrivKey, err = KeyToPEM(privKey, role)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cachedKeys[name] = &cachedKey{alias: role, key: privKey}
|
|
return s.Add(filepath.Join(getSubdir(role), name), pemPrivKey)
|
|
}
|