mirror of https://github.com/docker/docs.git
289 lines
8.4 KiB
Go
289 lines
8.4 KiB
Go
package trustmanager
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"fmt"
|
|
|
|
"github.com/docker/notary/pkg/passphrase"
|
|
"github.com/endophage/gotuf/data"
|
|
)
|
|
|
|
const (
|
|
keyExtension = "key"
|
|
)
|
|
|
|
// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key
|
|
type ErrAttemptsExceeded struct{}
|
|
|
|
// ErrAttemptsExceeded is returned when too many attempts have been made to decrypt a key
|
|
func (err ErrAttemptsExceeded) Error() string {
|
|
return "maximum number of passphrase attempts exceeded"
|
|
}
|
|
|
|
// ErrPasswordInvalid is returned when signing fails. It could also mean the signing
|
|
// key file was corrupted, but we have no way to distinguish.
|
|
type ErrPasswordInvalid struct{}
|
|
|
|
// ErrPasswordInvalid is returned when signing fails. It could also mean the signing
|
|
// key file was corrupted, but we have no way to distinguish.
|
|
func (err ErrPasswordInvalid) Error() string {
|
|
return "password invalid, operation has failed."
|
|
}
|
|
|
|
// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key.
|
|
type ErrKeyNotFound struct {
|
|
KeyID string
|
|
}
|
|
|
|
// ErrKeyNotFound is returned when the keystore fails to retrieve a specific key.
|
|
func (err ErrKeyNotFound) Error() string {
|
|
return fmt.Sprintf("signing key not found: %s", err.KeyID)
|
|
}
|
|
|
|
// KeyStore is a generic interface for private key storage
|
|
type KeyStore interface {
|
|
LimitedFileStore
|
|
|
|
AddKey(name, alias string, privKey data.PrivateKey) error
|
|
GetKey(name string) (data.PrivateKey, string, error)
|
|
ListKeys() []string
|
|
RemoveKey(name string) error
|
|
}
|
|
|
|
type cachedKey struct {
|
|
alias string
|
|
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)
|
|
|
|
// KeyFileStore persists and manages private keys on disk
|
|
type KeyFileStore struct {
|
|
sync.Mutex
|
|
SimpleFileStore
|
|
passphrase.Retriever
|
|
cachedKeys map[string]*cachedKey
|
|
}
|
|
|
|
// KeyMemoryStore manages private keys in memory
|
|
type KeyMemoryStore struct {
|
|
sync.Mutex
|
|
MemoryFileStore
|
|
passphrase.Retriever
|
|
cachedKeys map[string]*cachedKey
|
|
}
|
|
|
|
// NewKeyFileStore returns a new KeyFileStore creating a private directory to
|
|
// hold the keys.
|
|
func NewKeyFileStore(baseDir string, passphraseRetriever passphrase.Retriever) (*KeyFileStore, error) {
|
|
fileStore, err := NewPrivateSimpleFileStore(baseDir, keyExtension)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cachedKeys := make(map[string]*cachedKey)
|
|
|
|
return &KeyFileStore{SimpleFileStore: *fileStore,
|
|
Retriever: passphraseRetriever,
|
|
cachedKeys: cachedKeys}, nil
|
|
}
|
|
|
|
// AddKey stores the contents of a PEM-encoded private key as a PEM block
|
|
func (s *KeyFileStore) AddKey(name, alias string, privKey data.PrivateKey) error {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
return addKey(s, s.Retriever, s.cachedKeys, name, alias, privKey)
|
|
}
|
|
|
|
// GetKey returns the PrivateKey given a KeyID
|
|
func (s *KeyFileStore) GetKey(name string) (data.PrivateKey, string, error) {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
return getKey(s, s.Retriever, s.cachedKeys, name)
|
|
}
|
|
|
|
// ListKeys returns a list of unique PublicKeys present on the KeyFileStore.
|
|
// There might be symlinks associating Certificate IDs to Public Keys, so this
|
|
// method only returns the IDs that aren't symlinks
|
|
func (s *KeyFileStore) ListKeys() []string {
|
|
return listKeys(s)
|
|
}
|
|
|
|
// RemoveKey removes the key from the keyfilestore
|
|
func (s *KeyFileStore) RemoveKey(name string) error {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
return removeKey(s, s.cachedKeys, name)
|
|
}
|
|
|
|
// NewKeyMemoryStore returns a new KeyMemoryStore which holds keys in memory
|
|
func NewKeyMemoryStore(passphraseRetriever passphrase.Retriever) *KeyMemoryStore {
|
|
memStore := NewMemoryFileStore()
|
|
cachedKeys := make(map[string]*cachedKey)
|
|
|
|
return &KeyMemoryStore{MemoryFileStore: *memStore,
|
|
Retriever: passphraseRetriever,
|
|
cachedKeys: cachedKeys}
|
|
}
|
|
|
|
// AddKey stores the contents of a PEM-encoded private key as a PEM block
|
|
func (s *KeyMemoryStore) AddKey(name, alias string, privKey data.PrivateKey) error {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
return addKey(s, s.Retriever, s.cachedKeys, name, alias, privKey)
|
|
}
|
|
|
|
// GetKey returns the PrivateKey given a KeyID
|
|
func (s *KeyMemoryStore) GetKey(name string) (data.PrivateKey, string, error) {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
return getKey(s, s.Retriever, s.cachedKeys, name)
|
|
}
|
|
|
|
// ListKeys returns a list of unique PublicKeys present on the KeyFileStore.
|
|
// There might be symlinks associating Certificate IDs to Public Keys, so this
|
|
// method only returns the IDs that aren't symlinks
|
|
func (s *KeyMemoryStore) ListKeys() []string {
|
|
return listKeys(s)
|
|
}
|
|
|
|
// RemoveKey removes the key from the keystore
|
|
func (s *KeyMemoryStore) RemoveKey(name string) error {
|
|
s.Lock()
|
|
defer s.Unlock()
|
|
return removeKey(s, s.cachedKeys, name)
|
|
}
|
|
|
|
func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, cachedKeys map[string]*cachedKey, name, alias string, privKey data.PrivateKey) error {
|
|
pemPrivKey, err := KeyToPEM(privKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
attempts := 0
|
|
passphrase := ""
|
|
giveup := false
|
|
for {
|
|
passphrase, giveup, err = passphraseRetriever(name, alias, true, attempts)
|
|
if err != nil {
|
|
attempts++
|
|
continue
|
|
}
|
|
if giveup {
|
|
return ErrAttemptsExceeded{}
|
|
}
|
|
if attempts > 10 {
|
|
return ErrAttemptsExceeded{}
|
|
}
|
|
break
|
|
}
|
|
|
|
if passphrase != "" {
|
|
pemPrivKey, err = EncryptPrivateKey(privKey, passphrase)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
cachedKeys[name] = &cachedKey{alias: alias, key: privKey}
|
|
return s.Add(name+"_"+alias, pemPrivKey)
|
|
}
|
|
|
|
func getKeyAlias(s LimitedFileStore, keyID string) (string, error) {
|
|
files := s.ListFiles(true)
|
|
name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(keyID), filepath.Ext(keyID)))
|
|
|
|
for _, file := range files {
|
|
filename := filepath.Base(file)
|
|
|
|
if strings.HasPrefix(filename, name) {
|
|
aliasPlusDotKey := strings.TrimPrefix(filename, name+"_")
|
|
retVal := strings.TrimSuffix(aliasPlusDotKey, "."+keyExtension)
|
|
return retVal, nil
|
|
}
|
|
}
|
|
|
|
return "", &ErrKeyNotFound{KeyID: keyID}
|
|
}
|
|
|
|
// GetKey returns the PrivateKey given a KeyID
|
|
func getKey(s LimitedFileStore, 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
|
|
}
|
|
keyAlias, err := getKeyAlias(s, name)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
keyBytes, err := s.Get(name + "_" + keyAlias)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var retErr error
|
|
// See if the key is encrypted. If its encrypted we'll fail to parse the private key
|
|
privKey, err := ParsePEMPrivateKey(keyBytes, "")
|
|
if err != nil {
|
|
// We need to decrypt the key, lets get a passphrase
|
|
for attempts := 0; ; attempts++ {
|
|
passphrase, giveup, err := passphraseRetriever(name, string(keyAlias), 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(keyBytes, passphrase)
|
|
if err != nil {
|
|
retErr = ErrPasswordInvalid{}
|
|
} else {
|
|
// We managed to parse the PrivateKey. We've succeeded!
|
|
retErr = nil
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if retErr != nil {
|
|
return nil, "", retErr
|
|
}
|
|
cachedKeys[name] = &cachedKey{alias: keyAlias, key: privKey}
|
|
return privKey, keyAlias, nil
|
|
}
|
|
|
|
// ListKeys returns a list of unique PublicKeys present on the KeyFileStore.
|
|
// There might be symlinks associating Certificate IDs to Public Keys, so this
|
|
// method only returns the IDs that aren't symlinks
|
|
func listKeys(s LimitedFileStore) []string {
|
|
var keyIDList []string
|
|
|
|
for _, f := range s.ListFiles(false) {
|
|
keyID := strings.TrimSpace(strings.TrimSuffix(f, filepath.Ext(f)))
|
|
keyID = keyID[:strings.LastIndex(keyID, "_")]
|
|
keyIDList = append(keyIDList, keyID)
|
|
}
|
|
return keyIDList
|
|
}
|
|
|
|
// RemoveKey removes the key from the keyfilestore
|
|
func removeKey(s LimitedFileStore, cachedKeys map[string]*cachedKey, name string) error {
|
|
keyAlias, err := getKeyAlias(s, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
delete(cachedKeys, name)
|
|
|
|
return s.Remove(name + "_" + keyAlias)
|
|
}
|