mirror of https://github.com/docker/docs.git
403 lines
15 KiB
Go
403 lines
15 KiB
Go
package cryptoservice
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/docker/notary/passphrase"
|
|
"github.com/docker/notary/trustmanager"
|
|
"github.com/docker/notary/tuf/data"
|
|
"github.com/docker/notary/tuf/signed"
|
|
)
|
|
|
|
var algoToSigType = map[string]data.SigAlgorithm{
|
|
data.ECDSAKey: data.ECDSASignature,
|
|
data.ED25519Key: data.EDDSASignature,
|
|
data.RSAKey: data.RSAPSSSignature,
|
|
}
|
|
|
|
var passphraseRetriever = func(string, string, bool, int) (string, bool, error) { return "", false, nil }
|
|
|
|
type CryptoServiceTester struct {
|
|
role string
|
|
keyAlgo string
|
|
gun string
|
|
}
|
|
|
|
func (c CryptoServiceTester) cryptoServiceFactory() *CryptoService {
|
|
return NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever))
|
|
}
|
|
|
|
// asserts that created key exists
|
|
func (c CryptoServiceTester) TestCreateAndGetKey(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
|
|
// Test Create
|
|
tufKey, err := cryptoService.Create(c.role, c.gun, c.keyAlgo)
|
|
require.NoError(t, err, c.errorMsg("error creating key"))
|
|
|
|
// Test GetKey
|
|
retrievedKey := cryptoService.GetKey(tufKey.ID())
|
|
require.NotNil(t, retrievedKey,
|
|
c.errorMsg("Could not find key ID %s", tufKey.ID()))
|
|
require.Equal(t, tufKey.Public(), retrievedKey.Public(),
|
|
c.errorMsg("retrieved public key didn't match"))
|
|
|
|
// Test GetPrivateKey
|
|
retrievedKey, alias, err := cryptoService.GetPrivateKey(tufKey.ID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, tufKey.ID(), retrievedKey.ID(),
|
|
c.errorMsg("retrieved private key didn't have the right ID"))
|
|
require.Equal(t, c.role, alias)
|
|
}
|
|
|
|
// If there are multiple keystores, ensure that a key is only added to one -
|
|
// the first in the list of keyStores (which is in order of preference)
|
|
func (c CryptoServiceTester) TestCreateAndGetWhenMultipleKeystores(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
cryptoService.keyStores = append(cryptoService.keyStores,
|
|
trustmanager.NewKeyMemoryStore(passphraseRetriever))
|
|
|
|
// Test Create
|
|
tufKey, err := cryptoService.Create(c.role, c.gun, c.keyAlgo)
|
|
require.NoError(t, err, c.errorMsg("error creating key"))
|
|
|
|
// Only the first keystore should have the key
|
|
keyPath := tufKey.ID()
|
|
if c.role != data.CanonicalRootRole && c.gun != "" {
|
|
keyPath = filepath.Join(c.gun, keyPath)
|
|
}
|
|
_, _, err = cryptoService.keyStores[0].GetKey(keyPath)
|
|
require.NoError(t, err, c.errorMsg(
|
|
"First keystore does not have the key %s", keyPath))
|
|
_, _, err = cryptoService.keyStores[1].GetKey(keyPath)
|
|
require.Error(t, err, c.errorMsg(
|
|
"Second keystore has the key %s", keyPath))
|
|
|
|
// GetKey works across multiple keystores
|
|
retrievedKey := cryptoService.GetKey(tufKey.ID())
|
|
require.NotNil(t, retrievedKey,
|
|
c.errorMsg("Could not find key ID %s", tufKey.ID()))
|
|
}
|
|
|
|
// asserts that getting key fails for a non-existent key
|
|
func (c CryptoServiceTester) TestGetNonexistentKey(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
|
|
require.Nil(t, cryptoService.GetKey("boguskeyid"),
|
|
c.errorMsg("non-nil result for bogus keyid"))
|
|
|
|
_, _, err := cryptoService.GetPrivateKey("boguskeyid")
|
|
require.Error(t, err)
|
|
// The underlying error has been correctly propagated.
|
|
_, ok := err.(*trustmanager.ErrKeyNotFound)
|
|
require.True(t, ok)
|
|
}
|
|
|
|
// asserts that signing with a created key creates a valid signature
|
|
func (c CryptoServiceTester) TestSignWithKey(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
content := []byte("this is a secret")
|
|
|
|
tufKey, err := cryptoService.Create(c.role, c.gun, c.keyAlgo)
|
|
require.NoError(t, err, c.errorMsg("error creating key"))
|
|
|
|
// Test Sign
|
|
privKey, role, err := cryptoService.GetPrivateKey(tufKey.ID())
|
|
require.NoError(t, err, c.errorMsg("failed to get private key"))
|
|
require.Equal(t, c.role, role)
|
|
|
|
signature, err := privKey.Sign(rand.Reader, content, nil)
|
|
require.NoError(t, err, c.errorMsg("signing failed"))
|
|
|
|
verifier, ok := signed.Verifiers[algoToSigType[c.keyAlgo]]
|
|
require.True(t, ok, c.errorMsg("Unknown verifier for algorithm"))
|
|
|
|
err = verifier.Verify(tufKey, signature, content)
|
|
require.NoError(t, err,
|
|
c.errorMsg("verification failed for %s key type", c.keyAlgo))
|
|
}
|
|
|
|
// asserts that signing, if there are no matching keys, produces no signatures
|
|
func (c CryptoServiceTester) TestSignNoMatchingKeys(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
|
|
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
|
|
require.NoError(t, err, c.errorMsg("error creating key"))
|
|
|
|
// Test Sign
|
|
_, _, err = cryptoService.GetPrivateKey(privKey.ID())
|
|
require.Error(t, err, c.errorMsg("Should not have found private key"))
|
|
}
|
|
|
|
// Test GetPrivateKey succeeds when multiple keystores have the same key
|
|
func (c CryptoServiceTester) TestGetPrivateKeyMultipleKeystores(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
cryptoService.keyStores = append(cryptoService.keyStores,
|
|
trustmanager.NewKeyMemoryStore(passphraseRetriever))
|
|
|
|
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
|
|
require.NoError(t, err, c.errorMsg("error creating key"))
|
|
|
|
for _, store := range cryptoService.keyStores {
|
|
err := store.AddKey(trustmanager.KeyInfo{Role: c.role, Gun: c.gun}, privKey)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
foundKey, role, err := cryptoService.GetPrivateKey(privKey.ID())
|
|
require.NoError(t, err, c.errorMsg("failed to get private key"))
|
|
require.Equal(t, c.role, role)
|
|
require.Equal(t, privKey.ID(), foundKey.ID())
|
|
}
|
|
|
|
func giveUpPassphraseRetriever(_, _ string, _ bool, _ int) (string, bool, error) {
|
|
return "", true, nil
|
|
}
|
|
|
|
// Test that ErrPasswordInvalid is correctly propagated
|
|
func (c CryptoServiceTester) TestGetPrivateKeyPasswordInvalid(t *testing.T) {
|
|
tempBaseDir, err := ioutil.TempDir("", "cs-test-")
|
|
require.NoError(t, err, "failed to create a temporary directory: %s", err)
|
|
defer os.RemoveAll(tempBaseDir)
|
|
|
|
// Do not use c.cryptoServiceFactory(), we need a KeyFileStore.
|
|
retriever := passphrase.ConstantRetriever("password")
|
|
store, err := trustmanager.NewKeyFileStore(tempBaseDir, retriever)
|
|
require.NoError(t, err)
|
|
cryptoService := NewCryptoService(store)
|
|
pubKey, err := cryptoService.Create(c.role, c.gun, c.keyAlgo)
|
|
require.NoError(t, err, "error generating key: %s", err)
|
|
|
|
// cryptoService's FileKeyStore caches the unlocked private key, so to test
|
|
// private key unlocking we need a new instance.
|
|
store, err = trustmanager.NewKeyFileStore(tempBaseDir, giveUpPassphraseRetriever)
|
|
require.NoError(t, err)
|
|
cryptoService = NewCryptoService(store)
|
|
|
|
_, _, err = cryptoService.GetPrivateKey(pubKey.ID())
|
|
require.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error())
|
|
}
|
|
|
|
// Test that ErrAtttemptsExceeded is correctly propagated
|
|
func (c CryptoServiceTester) TestGetPrivateKeyAttemptsExceeded(t *testing.T) {
|
|
tempBaseDir, err := ioutil.TempDir("", "cs-test-")
|
|
require.NoError(t, err, "failed to create a temporary directory: %s", err)
|
|
defer os.RemoveAll(tempBaseDir)
|
|
|
|
// Do not use c.cryptoServiceFactory(), we need a KeyFileStore.
|
|
retriever := passphrase.ConstantRetriever("password")
|
|
store, err := trustmanager.NewKeyFileStore(tempBaseDir, retriever)
|
|
require.NoError(t, err)
|
|
cryptoService := NewCryptoService(store)
|
|
pubKey, err := cryptoService.Create(c.role, c.gun, c.keyAlgo)
|
|
require.NoError(t, err, "error generating key: %s", err)
|
|
|
|
// trustmanager.KeyFileStore and trustmanager.KeyMemoryStore both cache the unlocked
|
|
// private key, so to test private key unlocking we need a new instance using the
|
|
// same underlying storage; this also makes trustmanager.KeyMemoryStore (and
|
|
// c.cryptoServiceFactory()) unsuitable.
|
|
retriever = passphrase.ConstantRetriever("incorrect password")
|
|
store, err = trustmanager.NewKeyFileStore(tempBaseDir, retriever)
|
|
require.NoError(t, err)
|
|
cryptoService = NewCryptoService(store)
|
|
|
|
_, _, err = cryptoService.GetPrivateKey(pubKey.ID())
|
|
require.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error())
|
|
}
|
|
|
|
// asserts that removing key that exists succeeds
|
|
func (c CryptoServiceTester) TestRemoveCreatedKey(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
|
|
tufKey, err := cryptoService.Create(c.role, c.gun, c.keyAlgo)
|
|
require.NoError(t, err, c.errorMsg("error creating key"))
|
|
require.NotNil(t, cryptoService.GetKey(tufKey.ID()))
|
|
|
|
// Test RemoveKey
|
|
err = cryptoService.RemoveKey(tufKey.ID())
|
|
require.NoError(t, err, c.errorMsg("could not remove key"))
|
|
retrievedKey := cryptoService.GetKey(tufKey.ID())
|
|
require.Nil(t, retrievedKey, c.errorMsg("remove didn't work"))
|
|
}
|
|
|
|
// asserts that removing key will remove it from all keystores
|
|
func (c CryptoServiceTester) TestRemoveFromMultipleKeystores(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
cryptoService.keyStores = append(cryptoService.keyStores,
|
|
trustmanager.NewKeyMemoryStore(passphraseRetriever))
|
|
|
|
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
|
|
require.NoError(t, err, c.errorMsg("error creating key"))
|
|
|
|
for _, store := range cryptoService.keyStores {
|
|
err := store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, privKey)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
require.NotNil(t, cryptoService.GetKey(privKey.ID()))
|
|
|
|
// Remove removes it from all key stores
|
|
err = cryptoService.RemoveKey(privKey.ID())
|
|
require.NoError(t, err, c.errorMsg("could not remove key"))
|
|
|
|
for _, store := range cryptoService.keyStores {
|
|
_, _, err := store.GetKey(privKey.ID())
|
|
require.Error(t, err)
|
|
}
|
|
}
|
|
|
|
// asserts that listing keys works with multiple keystores, and that the
|
|
// same keys are deduplicated
|
|
func (c CryptoServiceTester) TestListFromMultipleKeystores(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
cryptoService.keyStores = append(cryptoService.keyStores,
|
|
trustmanager.NewKeyMemoryStore(passphraseRetriever))
|
|
|
|
expectedKeysIDs := make(map[string]bool) // just want to be able to index by key
|
|
|
|
for i := 0; i < 3; i++ {
|
|
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
|
|
require.NoError(t, err, c.errorMsg("error creating key"))
|
|
expectedKeysIDs[privKey.ID()] = true
|
|
|
|
// adds one different key to each keystore, and then one key to
|
|
// both keystores
|
|
for j, store := range cryptoService.keyStores {
|
|
if i == j || i == 2 {
|
|
store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: ""}, privKey)
|
|
}
|
|
}
|
|
}
|
|
// sanity check - each should have 2
|
|
for _, store := range cryptoService.keyStores {
|
|
require.Len(t, store.ListKeys(), 2, c.errorMsg("added keys wrong"))
|
|
}
|
|
|
|
keyList := cryptoService.ListKeys("root")
|
|
require.Len(t, keyList, 4,
|
|
c.errorMsg(
|
|
"ListKeys should have 4 keys (not necesarily unique) but does not: %v", keyList))
|
|
for _, k := range keyList {
|
|
_, ok := expectedKeysIDs[k]
|
|
require.True(t, ok, c.errorMsg("Unexpected key %s", k))
|
|
}
|
|
|
|
keyMap := cryptoService.ListAllKeys()
|
|
require.Len(t, keyMap, 3,
|
|
c.errorMsg("ListAllKeys should have 3 unique keys but does not: %v", keyMap))
|
|
|
|
for k, role := range keyMap {
|
|
_, ok := expectedKeysIDs[k]
|
|
require.True(t, ok)
|
|
require.Equal(t, "root", role)
|
|
}
|
|
}
|
|
|
|
// asserts that adding a key adds to just the first keystore
|
|
// and adding an existing key either succeeds if the role matches or fails if it does not
|
|
func (c CryptoServiceTester) TestAddKey(t *testing.T) {
|
|
cryptoService := c.cryptoServiceFactory()
|
|
cryptoService.keyStores = append(cryptoService.keyStores,
|
|
trustmanager.NewKeyMemoryStore(passphraseRetriever))
|
|
|
|
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
// Add the key to the targets role
|
|
require.NoError(t, cryptoService.AddKey(data.CanonicalTargetsRole, c.gun, privKey))
|
|
|
|
// Check that we added the key and its info to only the first keystore
|
|
retrievedKey, retrievedRole, err := cryptoService.keyStores[0].GetKey(privKey.ID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, privKey.Private(), retrievedKey.Private())
|
|
require.Equal(t, data.CanonicalTargetsRole, retrievedRole)
|
|
|
|
retrievedKeyInfo, err := cryptoService.keyStores[0].GetKeyInfo(privKey.ID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, data.CanonicalTargetsRole, retrievedKeyInfo.Role)
|
|
require.Equal(t, c.gun, retrievedKeyInfo.Gun)
|
|
|
|
// The key should not exist in the second keystore
|
|
_, _, err = cryptoService.keyStores[1].GetKey(privKey.ID())
|
|
require.Error(t, err)
|
|
_, err = cryptoService.keyStores[1].GetKeyInfo(privKey.ID())
|
|
require.Error(t, err)
|
|
|
|
// We should be able to successfully get the key from the cryptoservice level
|
|
retrievedKey, retrievedRole, err = cryptoService.GetPrivateKey(privKey.ID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, privKey.Private(), retrievedKey.Private())
|
|
require.Equal(t, data.CanonicalTargetsRole, retrievedRole)
|
|
retrievedKeyInfo, err = cryptoService.GetKeyInfo(privKey.ID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, data.CanonicalTargetsRole, retrievedKeyInfo.Role)
|
|
require.Equal(t, c.gun, retrievedKeyInfo.Gun)
|
|
|
|
// Add the same key to the targets role, since the info is the same we should have no error
|
|
require.NoError(t, cryptoService.AddKey(data.CanonicalTargetsRole, c.gun, privKey))
|
|
|
|
// Try to add the same key to the snapshot role, which should error due to the role mismatch
|
|
require.Error(t, cryptoService.AddKey(data.CanonicalSnapshotRole, c.gun, privKey))
|
|
}
|
|
|
|
// Prints out an error message with information about the key algorithm,
|
|
// role, and test name. Ideally we could generate different tests given
|
|
// data, without having to put for loops in one giant test function, but
|
|
// that involves a lot of boilerplate. So as a compromise, everything will
|
|
// still be run in for loops in one giant test function, but we can at
|
|
// least provide an error message stating what data/helper test function
|
|
// failed.
|
|
func (c CryptoServiceTester) errorMsg(message string, args ...interface{}) string {
|
|
pc := make([]uintptr, 10) // at least 1 entry needed
|
|
runtime.Callers(2, pc) // the caller of errorMsg
|
|
f := runtime.FuncForPC(pc[0])
|
|
return fmt.Sprintf("%s (role: %s, keyAlgo: %s): %s", f.Name(), c.role,
|
|
c.keyAlgo, fmt.Sprintf(message, args...))
|
|
}
|
|
|
|
func testCryptoService(t *testing.T, gun string) {
|
|
roles := []string{
|
|
data.CanonicalRootRole,
|
|
data.CanonicalTargetsRole,
|
|
data.CanonicalSnapshotRole,
|
|
data.CanonicalTimestampRole,
|
|
}
|
|
|
|
for _, role := range roles {
|
|
for algo := range algoToSigType {
|
|
cst := CryptoServiceTester{
|
|
role: role,
|
|
keyAlgo: algo,
|
|
gun: gun,
|
|
}
|
|
cst.TestAddKey(t)
|
|
cst.TestCreateAndGetKey(t)
|
|
cst.TestCreateAndGetWhenMultipleKeystores(t)
|
|
cst.TestGetNonexistentKey(t)
|
|
cst.TestSignWithKey(t)
|
|
cst.TestSignNoMatchingKeys(t)
|
|
cst.TestGetPrivateKeyMultipleKeystores(t)
|
|
cst.TestRemoveCreatedKey(t)
|
|
cst.TestRemoveFromMultipleKeystores(t)
|
|
cst.TestListFromMultipleKeystores(t)
|
|
cst.TestGetPrivateKeyPasswordInvalid(t)
|
|
cst.TestGetPrivateKeyAttemptsExceeded(t)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCryptoServiceWithNonEmptyGUN(t *testing.T) {
|
|
testCryptoService(t, "org/repo")
|
|
}
|
|
|
|
func TestCryptoServiceWithEmptyGUN(t *testing.T) {
|
|
testCryptoService(t, "")
|
|
}
|