diff --git a/client/backwards_compatibility_test.go b/client/backwards_compatibility_test.go index 09abd061e3..62b43b77a3 100644 --- a/client/backwards_compatibility_test.go +++ b/client/backwards_compatibility_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/store" "github.com/stretchr/testify/require" @@ -90,7 +91,7 @@ func Test0Dot1RepoFormat(t *testing.T) { defer ts.Close() repo, err := NewNotaryRepository(tmpDir, gun, ts.URL, http.DefaultTransport, - passphrase.ConstantRetriever(passwd)) + passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) // targets should have 1 target, and it should be readable offline @@ -152,7 +153,7 @@ func TestDownloading0Dot1RepoFormat(t *testing.T) { defer os.RemoveAll(repoDir) repo, err := NewNotaryRepository(repoDir, gun, ts.URL, http.DefaultTransport, - passphrase.ConstantRetriever(passwd)) + passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) err = repo.Update(true) diff --git a/client/client.go b/client/client.go index 8f434dc794..c52abd42ea 100644 --- a/client/client.go +++ b/client/client.go @@ -15,10 +15,10 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/notary" - "github.com/docker/notary/certs" "github.com/docker/notary/client/changelist" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf" tufclient "github.com/docker/notary/tuf/client" "github.com/docker/notary/tuf/data" @@ -88,13 +88,14 @@ type NotaryRepository struct { tufRepo *tuf.Repo roundTrip http.RoundTripper CertStore trustmanager.X509Store + trustPinning trustpinning.TrustPinConfig } // repositoryFromKeystores is a helper function for NewNotaryRepository that // takes some basic NotaryRepository parameters as well as keystores (in order // of usage preference), and returns a NotaryRepository. func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper, - keyStores []trustmanager.KeyStore) (*NotaryRepository, error) { + keyStores []trustmanager.KeyStore, trustPin trustpinning.TrustPinConfig) (*NotaryRepository, error) { certPath := filepath.Join(baseDir, notary.TrustedCertsDir) certStore, err := trustmanager.NewX509FilteredFileStore( @@ -115,6 +116,7 @@ func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper, CryptoService: cryptoService, roundTrip: rt, CertStore: certStore, + trustPinning: trustPin, } fileStore, err := store.NewFilesystemStore( @@ -873,7 +875,7 @@ func (r *NotaryRepository) validateRoot(rootJSON []byte) (*data.SignedRoot, erro return nil, err } - err = certs.ValidateRoot(r.CertStore, root, r.gun) + err = trustpinning.ValidateRoot(r.CertStore, root, r.gun, r.trustPinning) if err != nil { return nil, err } diff --git a/client/client_root_validation_test.go b/client/client_root_validation_test.go index 5f665f1394..4ecfbeb6f8 100644 --- a/client/client_root_validation_test.go +++ b/client/client_root_validation_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/Sirupsen/logrus" - "github.com/docker/notary/certs" + "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/stretchr/testify/require" ) @@ -72,7 +72,7 @@ func validateRootSuccessfully(t *testing.T, rootType string) { // in the store for the dnsName docker.com/notary, so TOFUS doesn't apply _, err = repo.ListTargets(data.CanonicalTargetsRole) require.Error(t, err, "An error was expected") - require.Equal(t, err, &certs.ErrValidationFail{ + require.Equal(t, err, &trustpinning.ErrValidationFail{ Reason: "failed to validate data with current trusted certificates", }) } diff --git a/client/client_test.go b/client/client_test.go index e779432525..21c7c7fda8 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -31,6 +31,7 @@ import ( "github.com/docker/notary/server" "github.com/docker/notary/server/storage" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/store" @@ -191,7 +192,7 @@ func createRepoAndKey(t *testing.T, rootType, tempBaseDir, gun, url string) ( rec := newRoleRecorder() repo, err := NewNotaryRepository( - tempBaseDir, gun, url, http.DefaultTransport, rec.retriever) + tempBaseDir, gun, url, http.DefaultTransport, rec.retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType) @@ -222,7 +223,7 @@ func newRepoToTestRepo(t *testing.T, existingRepo *NotaryRepository, newDir bool rec := newRoleRecorder() repo, err := NewNotaryRepository( repoDir, existingRepo.gun, existingRepo.baseURL, - http.DefaultTransport, rec.retriever) + http.DefaultTransport, rec.retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repository: %s", err) if err != nil && newDir { defer os.RemoveAll(repoDir) @@ -574,7 +575,7 @@ func testInitRepoAttemptsExceeded(t *testing.T, rootType string) { defer ts.Close() retriever := passphrase.ConstantRetriever("password") - repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever) + repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType) require.NoError(t, err, "error generating root key: %s", err) @@ -582,7 +583,7 @@ func testInitRepoAttemptsExceeded(t *testing.T, rootType string) { retriever = passphrase.ConstantRetriever("incorrect password") // repo.CryptoService’s FileKeyStore caches the unlocked private key, so to test // private key unlocking we need a new repo instance. - repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever) + repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) err = repo.Initialize(rootPubKey.ID()) require.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error()) @@ -612,14 +613,14 @@ func testInitRepoPasswordInvalid(t *testing.T, rootType string) { defer ts.Close() retriever := passphrase.ConstantRetriever("password") - repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever) + repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, retriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType) require.NoError(t, err, "error generating root key: %s", err) // repo.CryptoService’s FileKeyStore caches the unlocked private key, so to test // private key unlocking we need a new repo instance. - repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, giveUpPassphraseRetriever) + repo, err = NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, giveUpPassphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) err = repo.Initialize(rootPubKey.ID()) require.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error()) @@ -1641,7 +1642,7 @@ func TestPublishUninitializedRepo(t *testing.T) { defer os.RemoveAll(tempBaseDir) repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, - http.DefaultTransport, passphraseRetriever) + http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repository: %s", err) err = repo.Publish() require.Error(t, err) @@ -2005,7 +2006,7 @@ func TestPublishSnapshotLocalKeysCreatedFirst(t *testing.T) { defer ts.Close() repo, err := NewNotaryRepository( - tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever) + tempBaseDir, gun, ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) @@ -2863,7 +2864,7 @@ func TestRemoteServerUnavailableNoLocalCache(t *testing.T) { defer ts.Close() repo, err := NewNotaryRepository(tempBaseDir, "docker.com/notary", - ts.URL, http.DefaultTransport, passphraseRetriever) + ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) _, err = repo.ListTargets(data.CanonicalTargetsRole) @@ -3173,6 +3174,7 @@ func TestBootstrapClientBadURL(t *testing.T) { "http://localhost:9998", http.DefaultTransport, passphraseRetriever, + trustpinning.TrustPinConfig{}, ) require.NoError(t, err, "error creating repo: %s", err) @@ -3202,6 +3204,7 @@ func TestBootstrapClientInvalidURL(t *testing.T) { "#!*)&!)#*^%!#)%^!#", http.DefaultTransport, passphraseRetriever, + trustpinning.TrustPinConfig{}, ) require.NoError(t, err, "error creating repo: %s", err) diff --git a/client/client_update_test.go b/client/client_update_test.go index 426e4d00f8..01b4e37216 100644 --- a/client/client_update_test.go +++ b/client/client_update_test.go @@ -14,8 +14,8 @@ import ( "testing" "time" - "github.com/docker/notary/certs" "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/client" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" @@ -31,7 +31,7 @@ func newBlankRepo(t *testing.T, url string) *NotaryRepository { require.NoError(t, err, "failed to create a temporary directory: %s", err) repo, err := NewNotaryRepository(tempBaseDir, "docker.com/notary", url, - http.DefaultTransport, passphrase.ConstantRetriever("pass")) + http.DefaultTransport, passphrase.ConstantRetriever("pass"), trustpinning.TrustPinConfig{}) require.NoError(t, err) return repo } @@ -784,29 +784,29 @@ var waysToMessUpServer = []swizzleExpectations{ // for everything else, the errors come from tuf/signed {desc: "invalid SignedMeta Type", expectErrs: []interface{}{ - &certs.ErrValidationFail{}, signed.ErrWrongType, data.ErrInvalidMetadata{}}, + &trustpinning.ErrValidationFail{}, signed.ErrWrongType, data.ErrInvalidMetadata{}}, swizzle: (*testutils.MetadataSwizzler).SetInvalidMetadataType}, {desc: "invalid signatures", expectErrs: []interface{}{ - &certs.ErrValidationFail{}, signed.ErrRoleThreshold{}}, + &trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}}, swizzle: (*testutils.MetadataSwizzler).InvalidateMetadataSignatures}, {desc: "meta signed by wrong key", expectErrs: []interface{}{ - &certs.ErrValidationFail{}, signed.ErrRoleThreshold{}}, + &trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}}, swizzle: (*testutils.MetadataSwizzler).SignMetadataWithInvalidKey}, {desc: "expired metadata", expectErrs: []interface{}{ - &certs.ErrValidationFail{}, signed.ErrExpired{}}, + &trustpinning.ErrValidationFail{}, signed.ErrExpired{}}, swizzle: (*testutils.MetadataSwizzler).ExpireMetadata}, {desc: "lower metadata version", expectErrs: []interface{}{ - &certs.ErrValidationFail{}, signed.ErrLowVersion{}}, + &trustpinning.ErrValidationFail{}, signed.ErrLowVersion{}}, swizzle: func(s *testutils.MetadataSwizzler, role string) error { return s.OffsetMetadataVersion(role, -3) }}, {desc: "insufficient signatures", expectErrs: []interface{}{ - &certs.ErrValidationFail{}, signed.ErrRoleThreshold{}}, + &trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}}, swizzle: func(s *testutils.MetadataSwizzler, role string) error { return s.SetThreshold(role, 2) }}, @@ -824,7 +824,7 @@ func waysToMessUpServerRoot() []swizzleExpectations { swizzleExpectations{ desc: fmt.Sprintf("no %s keys", roleName), expectErrs: []interface{}{ - &certs.ErrValidationFail{}, signed.ErrRoleThreshold{}}, + &trustpinning.ErrValidationFail{}, signed.ErrRoleThreshold{}}, swizzle: func(s *testutils.MetadataSwizzler, role string) error { return s.MutateRoot(func(r *data.Root) { r.Roles[roleName].KeyIDs = []string{} diff --git a/client/repo.go b/client/repo.go index d9931891ff..1612611702 100644 --- a/client/repo.go +++ b/client/repo.go @@ -8,13 +8,14 @@ import ( "github.com/docker/notary/passphrase" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustpinning" ) // NewNotaryRepository is a helper method that returns a new notary repository. // It takes the base directory under where all the trust files will be stored // (usually ~/.docker/trust/). func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, - retriever passphrase.Retriever) ( + retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) ( *NotaryRepository, error) { fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) @@ -23,5 +24,5 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, } return repositoryFromKeystores(baseDir, gun, baseURL, rt, - []trustmanager.KeyStore{fileKeyStore}) + []trustmanager.KeyStore{fileKeyStore}, trustPinning) } diff --git a/client/repo_pkcs11.go b/client/repo_pkcs11.go index 60ae18f410..b21f5a8169 100644 --- a/client/repo_pkcs11.go +++ b/client/repo_pkcs11.go @@ -9,13 +9,14 @@ import ( "github.com/docker/notary/passphrase" "github.com/docker/notary/trustmanager" "github.com/docker/notary/trustmanager/yubikey" + "github.com/docker/notary/trustpinning" ) // NewNotaryRepository is a helper method that returns a new notary repository. // It takes the base directory under where all the trust files will be stored // (usually ~/.docker/trust/). func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, - retriever passphrase.Retriever) ( + retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) ( *NotaryRepository, error) { fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) @@ -29,5 +30,5 @@ func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, keyStores = []trustmanager.KeyStore{yubiKeyStore, fileKeyStore} } - return repositoryFromKeystores(baseDir, gun, baseURL, rt, keyStores) + return repositoryFromKeystores(baseDir, gun, baseURL, rt, keyStores, trustPinning) } diff --git a/cmd/notary/cert.go b/cmd/notary/cert.go index 2922850d4e..503ccdb398 100644 --- a/cmd/notary/cert.go +++ b/cmd/notary/cert.go @@ -138,8 +138,12 @@ func (c *certCommander) certRemove(cmd *cobra.Command, args []string) error { if removeTrustData { // Remove all TUF data, so call RemoveTrustData on a NotaryRepository with the GUN // no online operations are performed so the transport argument is nil + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } nRepo, err := notaryclient.NewNotaryRepository( - trustDir, c.certRemoveGUN, getRemoteTrustServer(config), nil, c.retriever) + trustDir, c.certRemoveGUN, getRemoteTrustServer(config), nil, c.retriever, trustPin) if err != nil { return fmt.Errorf("Could not establish trust data for GUN %s", c.certRemoveGUN) } diff --git a/cmd/notary/delegations.go b/cmd/notary/delegations.go index d92a3f8769..0730d84067 100644 --- a/cmd/notary/delegations.go +++ b/cmd/notary/delegations.go @@ -83,9 +83,14 @@ func (d *delegationCommander) delegationsList(cmd *cobra.Command, args []string) return err } + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + // initialize repo with transport to get latest state of the world before listing delegations nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, d.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, d.retriever, trustPin) if err != nil { return err } @@ -137,10 +142,15 @@ func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string d.paths = nil } + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + // no online operations are performed by add so the transport argument // should be nil nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, trustPin) if err != nil { return err } @@ -243,10 +253,15 @@ func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) e d.paths = []string{""} } + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + // no online operations are performed by add so the transport argument // should be nil nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, trustPin) if err != nil { return err } diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 56d3e54753..d57515958f 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -418,9 +418,14 @@ func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error { return err } + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + nRepo, err := notaryclient.NewNotaryRepository( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), - rt, k.getRetriever()) + rt, k.getRetriever(), trustPin) if err != nil { return err } diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index 20a3b80d41..372040258c 100644 --- a/cmd/notary/keys_test.go +++ b/cmd/notary/keys_test.go @@ -22,6 +22,7 @@ import ( "github.com/docker/notary/server" "github.com/docker/notary/server/storage" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -329,7 +330,7 @@ func setUpRepo(t *testing.T, tempBaseDir, gun string, ret passphrase.Retriever) ts := httptest.NewServer(server.RootHandler(nil, ctx, cryptoService, nil, nil)) repo, err := client.NewNotaryRepository( - tempBaseDir, gun, ts.URL, http.DefaultTransport, ret) + tempBaseDir, gun, ts.URL, http.DefaultTransport, ret, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", "", data.ECDSAKey) @@ -371,7 +372,7 @@ func TestRotateKeyRemoteServerManagesKey(t *testing.T) { } require.NoError(t, k.keysRotate(&cobra.Command{}, []string{gun, role, "-r"})) - repo, err := client.NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, ret) + repo, err := client.NewNotaryRepository(tempBaseDir, gun, ts.URL, http.DefaultTransport, ret, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) cl, err := repo.GetChangelist() @@ -395,7 +396,6 @@ func TestRotateKeyRemoteServerManagesKey(t *testing.T) { require.True(t, ok) } } - } } @@ -426,7 +426,7 @@ func TestRotateKeyBothKeys(t *testing.T) { require.NoError(t, k.keysRotate(&cobra.Command{}, []string{gun, data.CanonicalTargetsRole})) require.NoError(t, k.keysRotate(&cobra.Command{}, []string{gun, data.CanonicalSnapshotRole})) - repo, err := client.NewNotaryRepository(tempBaseDir, gun, ts.URL, nil, ret) + repo, err := client.NewNotaryRepository(tempBaseDir, gun, ts.URL, nil, ret, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) cl, err := repo.GetChangelist() diff --git a/cmd/notary/main_test.go b/cmd/notary/main_test.go index e1545075f8..ca3a787b2a 100644 --- a/cmd/notary/main_test.go +++ b/cmd/notary/main_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/docker/go-connections/tlsconfig" + "github.com/docker/notary" "github.com/docker/notary/passphrase" "github.com/docker/notary/server/storage" "github.com/stretchr/testify/require" @@ -369,3 +370,108 @@ func TestConfigFileOverridenByCmdLineFlags(t *testing.T) { require.Len(t, m.gotten, 1) require.Equal(t, m.gotten[0], "repo.root") } + +// the config can specify trust pinning settings for TOFUs, as well as pinned Certs or CA +func TestConfigFileTrustPinning(t *testing.T) { + var err error + + tempDir := tempDirWithConfig(t, `{ + "trust_pinning": { + "disable_tofu": false + } + }`) + defer os.RemoveAll(tempDir) + commander := ¬aryCommander{ + getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, + configFile: filepath.Join(tempDir, "config.json"), + } + + // Check that tofu was set correctly + config, err := commander.parseConfig() + require.NoError(t, err) + require.Equal(t, false, config.GetBool("trust_pinning.disable_tofu")) + trustPin, err := getTrustPinning(config) + require.NoError(t, err) + require.Equal(t, false, trustPin.DisableTOFU) + + tempDir = tempDirWithConfig(t, `{ + "remote_server": { + "url": "%s" + }, + "trust_pinning": { + "disable_tofu": true + } + }`) + defer os.RemoveAll(tempDir) + commander = ¬aryCommander{ + getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, + configFile: filepath.Join(tempDir, "config.json"), + } + + // Check that tofu was correctly disabled + config, err = commander.parseConfig() + require.NoError(t, err) + require.Equal(t, true, config.GetBool("trust_pinning.disable_tofu")) + trustPin, err = getTrustPinning(config) + require.NoError(t, err) + require.Equal(t, true, trustPin.DisableTOFU) + + tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ + "trust_pinning": { + "certs": { + "repo3": ["%s"] + } + } + }`, strings.Repeat("x", notary.Sha256HexSize))) + defer os.RemoveAll(tempDir) + commander = ¬aryCommander{ + getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, + configFile: filepath.Join(tempDir, "config.json"), + } + + config, err = commander.parseConfig() + require.NoError(t, err) + require.Equal(t, []interface{}{strings.Repeat("x", notary.Sha256HexSize)}, config.GetStringMap("trust_pinning.certs")["repo3"]) + trustPin, err = getTrustPinning(config) + require.NoError(t, err) + require.Equal(t, strings.Repeat("x", notary.Sha256HexSize), trustPin.Certs["repo3"][0]) + + // Check that an invalid cert ID pinning format fails + tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ + "trust_pinning": { + "certs": { + "repo3": "%s" + } + } + }`, strings.Repeat("x", notary.Sha256HexSize))) + defer os.RemoveAll(tempDir) + commander = ¬aryCommander{ + getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, + configFile: filepath.Join(tempDir, "config.json"), + } + + config, err = commander.parseConfig() + require.NoError(t, err) + trustPin, err = getTrustPinning(config) + require.Error(t, err) + + tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ + "trust_pinning": { + "ca": { + "repo4": "%s" + } + } + }`, "root-ca.crt")) + defer os.RemoveAll(tempDir) + commander = ¬aryCommander{ + getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, + configFile: filepath.Join(tempDir, "config.json"), + } + + config, err = commander.parseConfig() + require.NoError(t, err) + require.Equal(t, "root-ca.crt", config.GetStringMap("trust_pinning.ca")["repo4"]) + trustPin, err = getTrustPinning(config) + require.NoError(t, err) + require.Equal(t, "root-ca.crt", trustPin.CA["repo4"]) +} diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 397ea39cf3..783472f665 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -18,6 +18,7 @@ import ( "github.com/docker/go-connections/tlsconfig" notaryclient "github.com/docker/notary/client" "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/docker/notary/utils" "github.com/spf13/cobra" @@ -116,10 +117,15 @@ func (t *tufCommander) tufAdd(cmd *cobra.Command, args []string) error { targetName := args[1] targetPath := args[2] + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + // no online operations are performed by add so the transport argument // should be nil nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, trustPin) if err != nil { return err } @@ -155,8 +161,13 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { return err } + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -200,8 +211,13 @@ func (t *tufCommander) tufList(cmd *cobra.Command, args []string) error { return err } + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -235,8 +251,13 @@ func (t *tufCommander) tufLookup(cmd *cobra.Command, args []string) error { return err } + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -262,8 +283,13 @@ func (t *tufCommander) tufStatus(cmd *cobra.Command, args []string) error { } gun := args[0] + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, trustPin) if err != nil { return err } @@ -306,8 +332,13 @@ func (t *tufCommander) tufPublish(cmd *cobra.Command, args []string) error { return err } + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -330,10 +361,15 @@ func (t *tufCommander) tufRemove(cmd *cobra.Command, args []string) error { gun := args[0] targetName := args[1] + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + // no online operation are performed by remove so the transport argument // should be nil. repo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, trustPin) if err != nil { return err } @@ -371,8 +407,13 @@ func (t *tufCommander) tufVerify(cmd *cobra.Command, args []string) error { return err } + trustPin, err := getTrustPinning(config) + if err != nil { + return err + } + nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -546,3 +587,30 @@ func getRemoteTrustServer(config *viper.Viper) string { } return defaultServerURL } + +func getTrustPinning(config *viper.Viper) (trustpinning.TrustPinConfig, error) { + var ok bool + // Need to parse out Certs section from config + certMap := config.GetStringMap("trust_pinning.certs") + resultCertMap := make(map[string][]string) + for gun, certSlice := range certMap { + var castedCertSlice []interface{} + if castedCertSlice, ok = certSlice.([]interface{}); !ok { + return trustpinning.TrustPinConfig{}, fmt.Errorf("invalid format for trust_pinning.certs") + } + certsForGun := make([]string, len(castedCertSlice)) + for idx, certIDInterface := range castedCertSlice { + if certID, ok := certIDInterface.(string); ok { + certsForGun[idx] = certID + } else { + return trustpinning.TrustPinConfig{}, fmt.Errorf("invalid format for trust_pinning.certs") + } + } + resultCertMap[gun] = certsForGun + } + return trustpinning.TrustPinConfig{ + DisableTOFU: config.GetBool("trust_pinning.disable_tofu"), + CA: config.GetStringMapString("trust_pinning.ca"), + Certs: resultCertMap, + }, nil +} diff --git a/cryptoservice/certificate.go b/cryptoservice/certificate.go index 25183aea51..ff6f41b473 100644 --- a/cryptoservice/certificate.go +++ b/cryptoservice/certificate.go @@ -21,13 +21,6 @@ func GenerateCertificate(rootKey data.PrivateKey, gun string, startTime, endTime return generateCertificate(signer, gun, startTime, endTime) } -// GenerateTestingCertificate generates a non-expired X509 Certificate from a template, given a GUN. -// Good enough for tests where expiration does not really matter; do not use if you care about the policy. -func GenerateTestingCertificate(signer crypto.Signer, gun string) (*x509.Certificate, error) { - startTime := time.Now() - return generateCertificate(signer, gun, startTime, startTime.AddDate(10, 0, 0)) -} - func generateCertificate(signer crypto.Signer, gun string, startTime, endTime time.Time) (*x509.Certificate, error) { template, err := trustmanager.NewCertificate(gun, startTime, endTime) if err != nil { diff --git a/docs/images/trust-pinning-flow.png b/docs/images/trust-pinning-flow.png new file mode 100644 index 0000000000..6d88335890 Binary files /dev/null and b/docs/images/trust-pinning-flow.png differ diff --git a/docs/reference/client-config.md b/docs/reference/client-config.md index c92d855c21..e646fe8058 100644 --- a/docs/reference/client-config.md +++ b/docs/reference/client-config.md @@ -36,6 +36,11 @@ JSON keys to learn more about the configuration section corresponding to that ke "tls_client_cert": "./fixtures/secure.example.com.crt", "tls_client_key": "./fixtures/secure.example.com.crt" } + "trust_pinning": { + "certs": { + "docker.com/notary": ["49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292"] + } + } } @@ -113,6 +118,56 @@ Remote server example: +## trust_pinning section (optional) + +The `trust_pinning` specifies how to bootstrap trust for the root of a +Notary client's trusted collection. + +This section is optional, Notary will use TOFU over HTTPS by default and +trust certificates in the downloaded root file. + +In this section, one can provide specific certificates to pin to, or a CA +to pin to as a root of trust for a GUN. Multiple sections can be specified, +but the pinned certificates will take highest priority for validation, followed +by the pinned CA, followed by TOFUS (TOFU over HTTPS). The diagram below +describes this validation flow: + +
Trust pinning flow
+ +Only one trust pinning option will be used to validate a GUN even if multiple +sections are specified, and any validation failure will result in a failed +bootstrapping of the repo. + + + + + + + + + + + + + + + + + + + + + + +
ParameterRequiredDescription
certsno

Mapping of GUN to certificate IDs to pin to. + Both are strings in the JSON object.

cano

Mapping of GUN prefixes to filepaths containing + the root CA file with which to verify the certificates in the root file. + This file can contain multiple root certificates, bundled in separate + PEM blocks. + The path is relative to the directory of the configuration file.

disable_tofuno

Boolean value determining whether to use trust + on first use when bootstrapping validation on a collection's + root file. This keeps TOFUs on by default.

+ ## Environment variables (optional) The following environment variables containing signing key passphrases can diff --git a/trustmanager/x509filestore.go b/trustmanager/x509filestore.go index 8417805583..1d3b1213de 100644 --- a/trustmanager/x509filestore.go +++ b/trustmanager/x509filestore.go @@ -246,7 +246,7 @@ func (s *X509FileStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, err // as part of the roots list. This never allows the use of system roots, returning // an error if there are no root CAs. func (s *X509FileStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) { - // If we have no Certificates loaded return error (we don't want to rever to using + // If we have no Certificates loaded return error (we don't want to revert to using // system CAs). if len(s.fingerprintMap) == 0 { return x509.VerifyOptions{}, errors.New("no root CAs available") diff --git a/trustmanager/x509memstore.go b/trustmanager/x509memstore.go index 55666c0976..0d321938d5 100644 --- a/trustmanager/x509memstore.go +++ b/trustmanager/x509memstore.go @@ -188,7 +188,7 @@ func (s *X509MemStore) GetCertificatesByCN(cn string) ([]*x509.Certificate, erro // as part of the roots list. This never allows the use of system roots, returning // an error if there are no root CAs. func (s *X509MemStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, error) { - // If we have no Certificates loaded return error (we don't want to rever to using + // If we have no Certificates loaded return error (we don't want to revert to using // system CAs). if len(s.fingerprintMap) == 0 { return x509.VerifyOptions{}, errors.New("no root CAs available") diff --git a/trustmanager/x509utils.go b/trustmanager/x509utils.go index e2ce8addd3..72a36b4716 100644 --- a/trustmanager/x509utils.go +++ b/trustmanager/x509utils.go @@ -1,6 +1,7 @@ package trustmanager import ( + "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -57,13 +58,24 @@ func GetCertFromURL(urlStr string) (*x509.Certificate, error) { return cert, nil } -// CertToPEM is an utility function returns a PEM encoded x509 Certificate +// CertToPEM is a utility function returns a PEM encoded x509 Certificate func CertToPEM(cert *x509.Certificate) []byte { pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) return pemCert } +// CertChainToPEM is a utility function returns a PEM encoded chain of x509 Certificates, in the order they are passed +func CertChainToPEM(certChain []*x509.Certificate) ([]byte, error) { + var pemBytes bytes.Buffer + for _, cert := range certChain { + if err := pem.Encode(&pemBytes, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil { + return nil, err + } + } + return pemBytes.Bytes(), nil +} + // LoadCertFromPEM returns the first certificate found in a bunch of bytes or error // if nothing is found. Taken from https://golang.org/src/crypto/x509/cert_pool.go#L85. func LoadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { @@ -544,12 +556,34 @@ func CertToKey(cert *x509.Certificate) data.PublicKey { } } -// CertsToKeys transforms each of the input certificates into it's corresponding +// CertsToKeys transforms each of the input certificate chains into its corresponding // PublicKey -func CertsToKeys(certs []*x509.Certificate) map[string]data.PublicKey { +func CertsToKeys(leafCerts []*x509.Certificate, intCerts map[string][]*x509.Certificate) map[string]data.PublicKey { keys := make(map[string]data.PublicKey) - for _, cert := range certs { - newKey := CertToKey(cert) + for _, leafCert := range leafCerts { + certBundle := []*x509.Certificate{leafCert} + certID, err := FingerprintCert(leafCert) + if err != nil { + continue + } + if intCertsForLeafs, ok := intCerts[certID]; ok { + certBundle = append(certBundle, intCertsForLeafs...) + } + certChainPEM, err := CertChainToPEM(certBundle) + if err != nil { + continue + } + var newKey data.PublicKey + // Use the leaf cert's public key algorithm for typing + switch leafCert.PublicKeyAlgorithm { + case x509.RSA: + newKey = data.NewRSAx509PublicKey(certChainPEM) + case x509.ECDSA: + newKey = data.NewECDSAx509PublicKey(certChainPEM) + default: + logrus.Debugf("Unknown key type parsed from certificate: %v", leafCert.PublicKeyAlgorithm) + continue + } keys[newKey.ID()] = newKey } return keys @@ -581,6 +615,7 @@ func NewCertificate(gun string, startTime, endTime time.Time) (*x509.Certificate // X509PublicKeyID returns a public key ID as a string, given a // data.PublicKey that contains an X509 Certificate func X509PublicKeyID(certPubKey data.PublicKey) (string, error) { + // Note that this only loads the first certificate from the public key cert, err := LoadCertFromPEM(certPubKey.Public()) if err != nil { return "", err diff --git a/trustmanager/x509utils_test.go b/trustmanager/x509utils_test.go index f7fb7e12b5..693dd938a8 100644 --- a/trustmanager/x509utils_test.go +++ b/trustmanager/x509utils_test.go @@ -30,8 +30,8 @@ func TestCertsToKeys(t *testing.T) { // Get our certList with Leaf Cert and Intermediate certList := []*x509.Certificate{leafCert, intermediateCA, rootCA} - // Call CertsToKEys - keys := CertsToKeys(certList) + // Call CertsToKeys + keys := CertsToKeys(certList, make(map[string][]*x509.Certificate)) require.NotNil(t, keys) require.Len(t, keys, 3) @@ -44,6 +44,13 @@ func TestCertsToKeys(t *testing.T) { newKeys = GetIntermediateCerts(certList) require.NotNil(t, newKeys) require.Len(t, newKeys, 2) + + // Try calling CertToKeys on a junk leaf cert that won't fingerprint + emptyCert := x509.Certificate{} + // Also try changing the pre-existing leaf cert into an invalid algorithm + leafCert.PublicKeyAlgorithm = x509.DSA + keys = CertsToKeys([]*x509.Certificate{&emptyCert, leafCert}, make(map[string][]*x509.Certificate)) + require.Empty(t, keys) } func TestNewCertificate(t *testing.T) { diff --git a/certs/certs.go b/trustpinning/certs.go similarity index 80% rename from certs/certs.go rename to trustpinning/certs.go index 8d3aba2d3b..4ad275efde 100644 --- a/certs/certs.go +++ b/trustpinning/certs.go @@ -1,4 +1,4 @@ -package certs +package trustpinning import ( "crypto/x509" @@ -58,33 +58,44 @@ that list is non-empty means that we've already seen this repository before, and have a list of trusted certificates for it. In this case, we use this list of certificates to attempt to validate this root file. -If the previous validation succeeds, or in the case where we found no trusted -certificates for this particular GUN, we check the integrity of the root by +If the previous validation succeeds, we check the integrity of the root by making sure that it is validated by itself. This means that we will attempt to validate the root data with the certificates that are included in the root keys themselves. -If this last steps succeeds, we attempt to do root rotation, by ensuring that -we only trust the certificates that are present in the new root. +However, if we do not have any current trusted certificates for this GUN, we +check if there are any pinned certificates specified in the trust_pinning section +of the notary client config. If this section specifies a Certs section with this +GUN, we attempt to validate that the certificates present in the downloaded root +file match the pinned ID. -This mechanism of operation is essentially Trust On First Use (TOFU): if we -have never seen a certificate for a particular CN, we trust it. If later we see -a different certificate for that certificate, we return an ErrValidationFailed error. +If the Certs section is empty for this GUN, we check if the trust_pinning +section specifies a CA section specified in the config for this GUN. If so, we check +that the specified CA is valid and has signed a certificate included in the downloaded +root file. The specified CA can be a prefix for this GUN. + +If both the Certs and CA configs do not match this GUN, we fall back to the TOFU +section in the config: if true, we trust certificates specified in the root for +this GUN. If later we see a different certificate for that certificate, we return +an ErrValidationFailed error. Note that since we only allow trust data to be downloaded over an HTTPS channel we are using the current public PKI to validate the first download of the certificate adding an extra layer of security over the normal (SSH style) trust model. We shall call this: TOFUS. + +Validation failure at any step will result in an ErrValidationFailed error. */ -func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun string) error { +func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun string, trustPinning TrustPinConfig) error { logrus.Debugf("entered ValidateRoot with dns: %s", gun) signedRoot, err := data.RootFromSigned(root) if err != nil { return err } - // Retrieve all the leaf certificates in root for which the CN matches the GUN - certsFromRoot, err := validRootLeafCerts(signedRoot, gun) + // Retrieve all the leaf and intermediate certificates in root for which the CN matches the GUN + allLeafCerts, allIntCerts := parseAllCerts(signedRoot) + certsFromRoot, err := validRootLeafCerts(allLeafCerts, gun) if err != nil { logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err) return &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"} @@ -101,23 +112,44 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin return &ErrValidationFail{Reason: "unable to retrieve trusted certificates"} } } - // If we have certificates that match this specific GUN, let's make sure to // use them first to validate that this new root is valid. if len(trustedCerts) != 0 { logrus.Debugf("found %d valid root certificates for %s: %s", len(trustedCerts), gun, prettyFormatCertIDs(trustedCerts)) - err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(trustedCerts)) + err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(trustedCerts, allIntCerts)) if err != nil { logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) return &ErrValidationFail{Reason: "failed to validate data with current trusted certificates"} } } else { - logrus.Debugf("found no currently valid root certificates for %s", gun) + logrus.Debugf("found no currently valid root certificates for %s, using trust_pinning config to bootstrap trust", gun) + trustPinCheckFunc, err := NewTrustPinChecker(trustPinning, gun) + if err != nil { + return &ErrValidationFail{Reason: err.Error()} + } + + validPinnedCerts := []*x509.Certificate{} + for _, cert := range certsFromRoot { + certID, err := trustmanager.FingerprintCert(cert) + if err != nil { + continue + } + if ok := trustPinCheckFunc(cert, allIntCerts[certID]); !ok { + continue + } + validPinnedCerts = append(validPinnedCerts, cert) + } + if len(validPinnedCerts) == 0 { + return &ErrValidationFail{Reason: "unable to match any certificates to trust_pinning config"} + } + certsFromRoot = validPinnedCerts } // Validate the integrity of the new root (does it have valid signatures) - err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(certsFromRoot)) + // Note that certsFromRoot is guaranteed to be unchanged only if we had prior cert data for this GUN or enabled TOFUS + // If we attempted to pin a certain certificate or CA, certsFromRoot could have been pruned accordingly + err = signed.VerifyRoot(root, 0, trustmanager.CertsToKeys(certsFromRoot, allIntCerts)) if err != nil { logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) return &ErrValidationFail{Reason: "failed to validate integrity of roots"} @@ -165,9 +197,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin // validRootLeafCerts returns a list of non-expired, non-sha1 certificates // found in root whose Common-Names match the provided GUN. Note that this // "validity" alone does not imply any measure of trust. -func validRootLeafCerts(root *data.SignedRoot, gun string) ([]*x509.Certificate, error) { - // Get a list of all of the leaf certificates present in root - allLeafCerts, _ := parseAllCerts(root) +func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string) ([]*x509.Certificate, error) { var validLeafCerts []*x509.Certificate // Go through every leaf certificate and check that the CN matches the gun @@ -245,6 +275,11 @@ func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, m logrus.Debugf("invalid chain due to leaf certificate missing or too many leaf certificates for keyID: %s", keyID) continue } + // If we found a leaf certificate, assert that the cert bundle started with a leaf + if decodedCerts[0].IsCA { + logrus.Debugf("invalid chain due to leaf certificate not being first certificate for keyID: %s", keyID) + continue + } // Get the ID of the leaf certificate leafCert := leafCertList[0] diff --git a/certs/certs_test.go b/trustpinning/certs_test.go similarity index 60% rename from certs/certs_test.go rename to trustpinning/certs_test.go index 84f1ec26d9..4b64778931 100644 --- a/certs/certs_test.go +++ b/trustpinning/certs_test.go @@ -1,15 +1,23 @@ -package certs +package trustpinning import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "crypto/x509" + "crypto/x509/pkix" "encoding/json" + "encoding/pem" "io/ioutil" + "math/big" "os" "path/filepath" "testing" "text/template" + "time" + "github.com/docker/notary" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/trustmanager" @@ -32,6 +40,10 @@ const validIntermediateAndCertRSA = `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUl const signedRSARootTemplate = `{"signed":{"_type":"Root","consistent_snapshot":false,"expires":"2016-07-16T23:34:13.389129622-07:00","keys":{"1fc4fdc38f66558658c5c59b67f1716bdc6a74ef138b023ae5931db69f51d670":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8nIgzLigo5D47dWQe1IUjzHXxvyx0j/OL16VQymuloWsgVDxxT6+mH3CeviMAs+/McnEPE9exnm6SQGR5x3XMw=="}},"23c29cc372109c819e081bc953b7657d05e3f968f03c21d0d75ea457590f3d14":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEClUFVWkc85OQScfTQRS02VaLIEaeCmxdwYS/hcTLVoTxlFfRfs7HyalTwXGAGO79XZZS+koE6s8D0xGcCJQkLQ=="}},"49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292":{"keytype":"rsa-x509","keyval":{"private":null,"public":"{{.RootPem}}"}},"e3a5a4fdaf11ea1ec58f5efed6f3639b39cd4cfa1418c8b55c9a8c2447ace5d9":{"keytype":"ecdsa","keyval":{"private":null,"public":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgl3rzMPMEKhS1k/AX16MM4PdidpjJr+z4pj0Td+30QnpbOIARgpyR1PiFztU8BZlqG3cUazvFclr2q/xHvfrqw=="}}},"roles":{"root":{"keyids":["49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292"],"threshold":1},"snapshot":{"keyids":["23c29cc372109c819e081bc953b7657d05e3f968f03c21d0d75ea457590f3d14"],"threshold":1},"targets":{"keyids":["1fc4fdc38f66558658c5c59b67f1716bdc6a74ef138b023ae5931db69f51d670"],"threshold":1},"timestamp":{"keyids":["e3a5a4fdaf11ea1ec58f5efed6f3639b39cd4cfa1418c8b55c9a8c2447ace5d9"],"threshold":1}},"version":2},"signatures":[{"keyid":"49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292","method":"rsapss","sig":"YlZwtCj028Xc23+KHfj6govFEY6hMbBXO5HT20F0I5ZeIPb1l7OmkjEiwp9ZHusClY+QeqiP1CFh\n/AfCbv4tLanqMkXPtm8UJJ1hMZVq86coieB32PQDj9k6x1hErHzvPUbOzTRW2BQkFFMZFkLDAd06\npH8lmxyPLOhdkVE8qIT7sBCy/4bQIGfvEX6yCDz84MZdcLNX5B9mzGi9A7gDloh9IEZxA8UgoI18\nSYpv/fYeSZSqM/ws2G+kiELGgTWhcZ+gOlF7ArM/DOlcC/NYqcvY1ugE6Gn7G8opre6NOofdRp3w\n603A2rMMvYTwqKLY6oX/d+07A2+WGHXPUy5otCAybWOw2hIZ35Jjmh12g6Dc6Qk4K2zXwAgvWwBU\nWlT8MlP1Tf7f80jnGjh0aARlHI4LCxlYU5L/pCaYuHgynujvLuzoOuiiPfJv7sYvKoQ8UieE1w//\nHc8E6tWtV5G2FguKLurMoKZ9FBWcanDO0fg5AWuG3qcgUJdvh9acQ33EKer1fqBxs6LSAUWo8rDt\nQkg+b55AW0YBukAW9IAfMySQGAS2e3mHZ8nK/ijaygCRu7/P+NgKY9/zpmfL2xgcNslLcANcSOOt\nhiJS6yqYM9i9G0af0yw/TxAT4ntwjVm8u52UyR/hXIiUc/mjZcYRbSmJOHws902+i+Z/qv72knk="}]}` +const rootPubKeyID = "49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292" + +const targetsPubKeyID = "1fc4fdc38f66558658c5c59b67f1716bdc6a74ef138b023ae5931db69f51d670" + func TestCertsToRemove(t *testing.T) { // Get a few certificates to test with cert1, err := trustmanager.LoadCertFromFile("../fixtures/secure.example.com.crt") @@ -138,12 +150,12 @@ func TestValidateRoot(t *testing.T) { // This call to ValidateRoot will succeed since we are using a valid PEM // encoded certificate, and have no other certificates for this CN - err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary") + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{}) require.NoError(t, err) // This call to ValidateRoot will fail since we are passing in a dnsName that // doesn't match the CN of the certificate. - err = ValidateRoot(certStore, &testSignedRoot, "diogomonica.com/notary") + err = ValidateRoot(certStore, &testSignedRoot, "diogomonica.com/notary", TrustPinConfig{}) require.Error(t, err, "An error was expected") require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) @@ -157,7 +169,7 @@ func TestValidateRoot(t *testing.T) { // Unmarshal our signedroot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) - err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary") + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{}) require.Error(t, err, "illegal base64 data at input byte") // @@ -170,7 +182,7 @@ func TestValidateRoot(t *testing.T) { // Unmarshal our signedroot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) - err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary") + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{}) require.Error(t, err, "An error was expected") require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) @@ -185,15 +197,16 @@ func TestValidateRoot(t *testing.T) { // Unmarshal our signedroot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) - err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary") + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{}) require.Error(t, err, "An error was expected") require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) // - // This call to ValidateRoot will succeed in getting to the TUF validation, since + // This call to ValidateRoot could succeed in getting to the TUF validation, since // we are using a valid PEM encoded certificate chain of intermediate + leaf cert // that are signed by a trusted root authority and the leaf cert has a correct CN. - // It will, however, fail to validate, because it has an invalid TUF signature + // It will, however, fail to validate, because the leaf cert does not precede the + // intermediate in the certificate bundle // // Execute our template deleting the old buffer first signedRootBytes.Reset() @@ -203,9 +216,445 @@ func TestValidateRoot(t *testing.T) { // Unmarshal our signedroot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) - err = ValidateRoot(certStore, &testSignedRoot, "secure.example.com") + err = ValidateRoot(certStore, &testSignedRoot, "secure.example.com", TrustPinConfig{}) require.Error(t, err, "An error was expected") - require.Equal(t, err, &ErrValidationFail{Reason: "failed to validate integrity of roots"}) + require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) +} + +func TestValidateRootWithoutTOFUS(t *testing.T) { + var testSignedRoot data.Signed + var signedRootBytes bytes.Buffer + + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) + require.NoError(t, err, "failed to create a temporary directory: %s", err) + + // Create a X509Store + trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir) + certStore, err := trustmanager.NewX509FilteredFileStore( + trustPath, + trustmanager.FilterCertsExpiredSha1, + ) + require.NoError(t, err) + + // Execute our template + templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) + templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot}) + + // Unmarshal our signedroot + json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) + + // This call to ValidateRoot will fail since we are explicitly disabling TOFU and have no local certs + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{DisableTOFU: true}) + require.Error(t, err) +} + +func TestValidateRootWithPinnedCert(t *testing.T) { + var testSignedRoot data.Signed + var signedRootBytes bytes.Buffer + + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) + require.NoError(t, err, "failed to create a temporary directory: %s", err) + + // Create a X509Store + trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir) + certStore, err := trustmanager.NewX509FilteredFileStore( + trustPath, + trustmanager.FilterCertsExpiredSha1, + ) + require.NoError(t, err) + + // Execute our template + templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) + templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot}) + + // Unmarshal our signedroot + json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) + + // This call to ValidateRoot should succeed with the correct Cert ID (same as root public key ID) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {rootPubKeyID}}, DisableTOFU: true}) + require.NoError(t, err) + + // purge the cert store to check another valid certs trust pinning + certStore.RemoveAll() + + // This call to ValidateRoot should also succeed with the correct Cert ID (same as root public key ID), even though we passed an extra bad one + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {rootPubKeyID, "invalidID"}}, DisableTOFU: true}) + require.NoError(t, err) +} + +func TestValidateRootWithPinnerCertAndIntermediates(t *testing.T) { + now := time.Now() + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + + pass := func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) { + return "password", false, nil + } + memStore := trustmanager.NewKeyMemoryStore(pass) + cs := cryptoservice.NewCryptoService(memStore) + + // generate CA cert + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(t, err) + caTmpl := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "notary testing CA", + }, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 3, + } + caPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + _, err = x509.CreateCertificate( + rand.Reader, + &caTmpl, + &caTmpl, + caPrivKey.Public(), + caPrivKey, + ) + + // generate intermediate + intTmpl := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "notary testing intermediate", + }, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 2, + } + intPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + intCert, err := x509.CreateCertificate( + rand.Reader, + &intTmpl, + &caTmpl, + intPrivKey.Public(), + caPrivKey, + ) + require.NoError(t, err) + + // generate leaf + serialNumber, err = rand.Int(rand.Reader, serialNumberLimit) + require.NoError(t, err) + leafTmpl := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "docker.io/notary/test", + }, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(time.Hour), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + BasicConstraintsValid: true, + } + + leafPubKey, err := cs.Create("root", "docker.io/notary/test", data.ECDSAKey) + require.NoError(t, err) + leafPrivKey, _, err := cs.GetPrivateKey(leafPubKey.ID()) + require.NoError(t, err) + signer := leafPrivKey.CryptoSigner() + leafCert, err := x509.CreateCertificate( + rand.Reader, + &leafTmpl, + &intTmpl, + signer.Public(), + intPrivKey, + ) + + rootBundleWriter := bytes.NewBuffer(nil) + pem.Encode( + rootBundleWriter, + &pem.Block{ + Type: "CERTIFICATE", + Bytes: leafCert, + }, + ) + pem.Encode( + rootBundleWriter, + &pem.Block{ + Type: "CERTIFICATE", + Bytes: intCert, + }, + ) + + rootBundle := rootBundleWriter.Bytes() + + ecdsax509Key := data.NewECDSAx509PublicKey(rootBundle) + + otherKey, err := cs.Create("targets", "docker.io/notary/test", data.ED25519Key) + require.NoError(t, err) + + root := data.SignedRoot{ + Signatures: make([]data.Signature, 0), + Signed: data.Root{ + SignedCommon: data.SignedCommon{ + Type: "Root", + Expires: now.Add(time.Hour), + Version: 1, + }, + Keys: map[string]data.PublicKey{ + ecdsax509Key.ID(): ecdsax509Key, + otherKey.ID(): otherKey, + }, + Roles: map[string]*data.RootRole{ + "root": { + KeyIDs: []string{ecdsax509Key.ID()}, + Threshold: 1, + }, + "targets": { + KeyIDs: []string{otherKey.ID()}, + Threshold: 1, + }, + "snapshot": { + KeyIDs: []string{otherKey.ID()}, + Threshold: 1, + }, + "timestamp": { + KeyIDs: []string{otherKey.ID()}, + Threshold: 1, + }, + }, + }, + Dirty: true, + } + + signedRoot, err := root.ToSigned() + require.NoError(t, err) + err = signed.Sign(cs, signedRoot, []data.PublicKey{ecdsax509Key}, 1, nil) + require.NoError(t, err) + + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) + require.NoError(t, err, "failed to create a temporary directory: %s", err) + // Create a X509Store + trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir) + certStore, err := trustmanager.NewX509FilteredFileStore( + trustPath, + trustmanager.FilterCertsExpiredSha1, + ) + require.NoError(t, err) + + err = ValidateRoot( + certStore, + signedRoot, + "docker.io/notary/test", + TrustPinConfig{ + Certs: map[string][]string{ + "docker.io/notary/test": {ecdsax509Key.ID()}, + }, + DisableTOFU: true, + }, + ) + require.NoError(t, err, "failed to validate certID with intermediate") +} + +func TestValidateRootFailuresWithPinnedCert(t *testing.T) { + var testSignedRoot data.Signed + var signedRootBytes bytes.Buffer + + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) + require.NoError(t, err, "failed to create a temporary directory: %s", err) + + // Create a X509Store + trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir) + certStore, err := trustmanager.NewX509FilteredFileStore( + trustPath, + trustmanager.FilterCertsExpiredSha1, + ) + require.NoError(t, err) + + // Execute our template + templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) + templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot}) + + // Unmarshal our signedroot + json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) + + // This call to ValidateRoot should fail due to an incorrect cert ID + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {"ABSOLUTELY NOT A CERT ID"}}, DisableTOFU: true}) + require.Error(t, err) + + // This call to ValidateRoot should fail due to an empty cert ID + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {""}}, DisableTOFU: true}) + require.Error(t, err) + + // This call to ValidateRoot should fail due to an invalid GUN (even though the cert ID is correct), and TOFUS is set to false + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"not_a_gun": {rootPubKeyID}}, DisableTOFU: true}) + require.Error(t, err) + + // This call to ValidateRoot should fail due to an invalid cert ID, even though it's a valid key ID for targets + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {targetsPubKeyID}}, DisableTOFU: true}) + require.Error(t, err) + + // This call to ValidateRoot should succeed because we fall through to TOFUS because we have no matching GUNs under Certs + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"not_a_gun": {rootPubKeyID}}, DisableTOFU: false}) + require.NoError(t, err) +} + +func TestValidateRootWithPinnedCA(t *testing.T) { + var testSignedRoot data.Signed + var signedRootBytes bytes.Buffer + + // Temporary directory where test files will be created + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) + require.NoError(t, err, "failed to create a temporary directory: %s", err) + + // Create a X509Store + trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir) + certStore, err := trustmanager.NewX509FilteredFileStore( + trustPath, + trustmanager.FilterCertsExpiredSha1, + ) + require.NoError(t, err) + + templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) + templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot}) + // Unmarshal our signedRoot + json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) + + // This call to ValidateRoot will fail because we have an invalid path for the CA + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}}) + require.Error(t, err) + + // This call to ValidateRoot will fail because we have no valid GUNs to use, and TOFUS is disabled + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, DisableTOFU: true}) + require.Error(t, err) + + // This call to ValidateRoot will succeed because we have no valid GUNs to use and we fall back to enabled TOFUS + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, DisableTOFU: false}) + require.NoError(t, err) + require.NoError(t, certStore.RemoveAll()) + + // Write an invalid CA cert (not even a PEM) to the tempDir and ensure validation fails when using it + invalidCAFilepath := filepath.Join(tempBaseDir, "invalid.ca") + require.NoError(t, ioutil.WriteFile(invalidCAFilepath, []byte("ABSOLUTELY NOT A PEM"), 0644)) + + // Using this invalid CA cert should fail on ValidateRoot + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{CA: map[string]string{"docker.com/notary": invalidCAFilepath}, DisableTOFU: true}) + require.Error(t, err) + + validCAFilepath := "../fixtures/root-ca.crt" + + // If we pass an invalid Certs entry in addition to this valid CA entry, since Certs has priority for pinning we will fail + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {"invalidID"}}, CA: map[string]string{"docker.com/notary": validCAFilepath}, DisableTOFU: true}) + require.Error(t, err) + + // Now construct a new root with a valid cert chain, such that signatures are correct over the 'notary-signer' GUN. Pin the root-ca and validate + leafCert, err := trustmanager.LoadCertFromFile("../fixtures/notary-signer.crt") + require.NoError(t, err) + + intermediateCert, err := trustmanager.LoadCertFromFile("../fixtures/intermediate-ca.crt") + require.NoError(t, err) + + pemChainBytes, err := trustmanager.CertChainToPEM([]*x509.Certificate{leafCert, intermediateCert}) + require.NoError(t, err) + + newRootKey := data.NewPublicKey(data.RSAx509Key, pemChainBytes) + + rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{newRootKey.ID()}, nil) + require.NoError(t, err) + + testRoot, err := data.NewRoot( + map[string]data.PublicKey{newRootKey.ID(): newRootKey}, + map[string]*data.RootRole{ + data.CanonicalRootRole: &rootRole.RootRole, + data.CanonicalTimestampRole: &rootRole.RootRole, + data.CanonicalTargetsRole: &rootRole.RootRole, + data.CanonicalSnapshotRole: &rootRole.RootRole}, + false, + ) + require.NoError(t, err, "Failed to create new root") + + keyReader, err := os.Open("../fixtures/notary-signer.key") + require.NoError(t, err, "could not open key file") + pemBytes, err := ioutil.ReadAll(keyReader) + require.NoError(t, err, "could not read key file") + privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "") + require.NoError(t, err) + + store, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever) + require.NoError(t, err) + cs := cryptoservice.NewCryptoService(store) + + err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: "notary-signer"}, privKey) + require.NoError(t, err) + + newTestSignedRoot, err := testRoot.ToSigned() + require.NoError(t, err) + + err = signed.Sign(cs, newTestSignedRoot, []data.PublicKey{newRootKey}, 1, nil) + require.NoError(t, err) + + // Check that we validate correctly against a pinned CA and provided bundle + err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", TrustPinConfig{CA: map[string]string{"notary-signer": validCAFilepath}, DisableTOFU: true}) + require.NoError(t, err) + + // Add an expired CA for the same gun to our previous pinned bundle, ensure that we still validate correctly + certStore.RemoveAll() + goodRootCABundle, err := trustmanager.LoadCertBundleFromFile(validCAFilepath) + require.NoError(t, err) + memKeyStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) + cryptoService := cryptoservice.NewCryptoService(memKeyStore) + testPubKey, err := cryptoService.Create("root", "notary-signer", data.ECDSAKey) + require.NoError(t, err) + testPrivKey, _, err := memKeyStore.GetKey(testPubKey.ID()) + require.NoError(t, err) + expiredCert, err := generateExpiredTestingCertificate(testPrivKey, "notary-signer") + require.NoError(t, err) + bundleWithExpiredCert, err := trustmanager.CertChainToPEM(append(goodRootCABundle, expiredCert)) + require.NoError(t, err) + bundleWithExpiredCertPath := filepath.Join(tempBaseDir, "bundle_with_expired_cert.pem") + require.NoError(t, ioutil.WriteFile(bundleWithExpiredCertPath, bundleWithExpiredCert, 0644)) + + // Check that we validate correctly against a pinned CA and provided bundle + err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", TrustPinConfig{CA: map[string]string{"notary-signer": bundleWithExpiredCertPath}, DisableTOFU: true}) + require.NoError(t, err) + + certStore.RemoveAll() + testPubKey2, err := cryptoService.Create("root", "notary-signer", data.ECDSAKey) + require.NoError(t, err) + testPrivKey2, _, err := memKeyStore.GetKey(testPubKey2.ID()) + require.NoError(t, err) + expiredCert2, err := generateExpiredTestingCertificate(testPrivKey2, "notary-signer") + require.NoError(t, err) + allExpiredCertBundle, err := trustmanager.CertChainToPEM([]*x509.Certificate{expiredCert, expiredCert2}) + require.NoError(t, err) + allExpiredCertPath := filepath.Join(tempBaseDir, "all_expired_cert.pem") + require.NoError(t, ioutil.WriteFile(allExpiredCertPath, allExpiredCertBundle, 0644)) + // Now only use expired certs in the bundle, we should fail + err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", TrustPinConfig{CA: map[string]string{"notary-signer": allExpiredCertPath}, DisableTOFU: true}) + require.Error(t, err) + + // Add a CA cert for a that won't validate against the root leaf certificate + certStore.RemoveAll() + testPubKey3, err := cryptoService.Create("root", "notary-signer", data.ECDSAKey) + require.NoError(t, err) + testPrivKey3, _, err := memKeyStore.GetKey(testPubKey3.ID()) + require.NoError(t, err) + validCert, err := cryptoservice.GenerateCertificate(testPrivKey3, "notary-signer", time.Now(), time.Now().AddDate(1, 0, 0)) + require.NoError(t, err) + bundleWithWrongCert, err := trustmanager.CertChainToPEM([]*x509.Certificate{validCert}) + require.NoError(t, err) + bundleWithWrongCertPath := filepath.Join(tempBaseDir, "bundle_with_expired_cert.pem") + require.NoError(t, ioutil.WriteFile(bundleWithWrongCertPath, bundleWithWrongCert, 0644)) + err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", TrustPinConfig{CA: map[string]string{"notary-signer": bundleWithWrongCertPath}, DisableTOFU: true}) + require.Error(t, err) } // TestValidateSuccessfulRootRotation runs through a full root certificate rotation @@ -246,7 +695,7 @@ func filestoreWithTwoCerts(t *testing.T, gun, keyAlg string) ( key, _, err := fileKeyStore.GetKey(pubKey.ID()) require.NoError(t, err) - cert, err := cryptoservice.GenerateTestingCertificate(key.CryptoSigner(), gun) + cert, err := generateTestingCertificate(key, gun) require.NoError(t, err) certificates[i] = cert @@ -296,7 +745,7 @@ func testValidateSuccessfulRootRotation(t *testing.T, keyAlg, rootKeyType string // This call to ValidateRoot will succeed since we are using a valid PEM // encoded certificate, and have no other certificates for this CN - err = ValidateRoot(certStore, signedTestRoot, gun) + err = ValidateRoot(certStore, signedTestRoot, gun, TrustPinConfig{}) require.NoError(t, err) // Finally, validate the only trusted certificate that exists is the new one @@ -357,7 +806,7 @@ func testValidateRootRotationMissingOrigSig(t *testing.T, keyAlg, rootKeyType st // This call to ValidateRoot will succeed since we are using a valid PEM // encoded certificate, and have no other certificates for this CN - err = ValidateRoot(certStore, signedTestRoot, gun) + err = ValidateRoot(certStore, signedTestRoot, gun, TrustPinConfig{}) require.Error(t, err, "insuficient signatures on root") // Finally, validate the only trusted certificate that exists is still @@ -421,7 +870,7 @@ func testValidateRootRotationMissingNewSig(t *testing.T, keyAlg, rootKeyType str // This call to ValidateRoot will succeed since we are using a valid PEM // encoded certificate, and have no other certificates for this CN - err = ValidateRoot(certStore, signedTestRoot, gun) + err = ValidateRoot(certStore, signedTestRoot, gun, TrustPinConfig{}) require.Error(t, err, "insuficient signatures on root") // Finally, validate the only trusted certificate that exists is still @@ -430,3 +879,13 @@ func testValidateRootRotationMissingNewSig(t *testing.T, keyAlg, rootKeyType str require.Len(t, certificates, 1) require.Equal(t, certificates[0], origRootCert) } + +func generateTestingCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) { + startTime := time.Now() + return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.AddDate(10, 0, 0)) +} + +func generateExpiredTestingCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) { + startTime := time.Now().AddDate(-10, 0, 0) + return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.AddDate(1, 0, 0)) +} diff --git a/trustpinning/trustpin.go b/trustpinning/trustpin.go new file mode 100644 index 0000000000..7af34f2a31 --- /dev/null +++ b/trustpinning/trustpin.go @@ -0,0 +1,118 @@ +package trustpinning + +import ( + "crypto/x509" + "fmt" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/utils" + "strings" +) + +// TrustPinConfig represents the configuration under the trust_pinning section of the config file +// This struct represents the preferred way to bootstrap trust for this repository +type TrustPinConfig struct { + CA map[string]string + Certs map[string][]string + DisableTOFU bool +} + +type trustPinChecker struct { + gun string + config TrustPinConfig + pinnedCAPool *x509.CertPool + pinnedCertIDs []string +} + +// CertChecker is a function type that will be used to check leaf certs against pinned trust +type CertChecker func(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool + +// NewTrustPinChecker returns a new certChecker function from a TrustPinConfig for a GUN +func NewTrustPinChecker(trustPinConfig TrustPinConfig, gun string) (CertChecker, error) { + t := trustPinChecker{gun: gun, config: trustPinConfig} + // Determine the mode, and if it's even valid + if pinnedCerts, ok := trustPinConfig.Certs[gun]; ok { + t.pinnedCertIDs = pinnedCerts + return t.certsCheck, nil + } + + if caFilepath, err := getPinnedCAFilepathByPrefix(gun, trustPinConfig); err == nil { + // Try to add the CA certs from its bundle file to our certificate store, + // and use it to validate certs in the root.json later + caCerts, err := trustmanager.LoadCertBundleFromFile(caFilepath) + if err != nil { + return nil, fmt.Errorf("could not load root cert from CA path") + } + // Now only consider certificates that are direct children from this CA cert chain + caRootPool := x509.NewCertPool() + for _, caCert := range caCerts { + if err = trustmanager.ValidateCertificate(caCert); err != nil { + continue + } + caRootPool.AddCert(caCert) + } + // If we didn't have any valid CA certs, error out + if len(caRootPool.Subjects()) == 0 { + return nil, fmt.Errorf("invalid CA certs provided") + } + t.pinnedCAPool = caRootPool + return t.caCheck, nil + } + + if !trustPinConfig.DisableTOFU { + return t.tofusCheck, nil + } + return nil, fmt.Errorf("invalid trust pinning specified") +} + +func (t trustPinChecker) certsCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { + // reconstruct the leaf + intermediate cert chain, which is bundled as {leaf, intermediates...}, + // in order to get the matching id in the root file + leafCertID, err := trustmanager.FingerprintCert(leafCert) + if err != nil { + return false + } + rootKeys := trustmanager.CertsToKeys([]*x509.Certificate{leafCert}, map[string][]*x509.Certificate{leafCertID: intCerts}) + for keyID := range rootKeys { + if utils.StrSliceContains(t.pinnedCertIDs, keyID) { + return true + } + } + return false +} + +func (t trustPinChecker) caCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { + // Use intermediate certificates included in the root TUF metadata for our validation + caIntPool := x509.NewCertPool() + for _, intCert := range intCerts { + caIntPool.AddCert(intCert) + } + // Attempt to find a valid certificate chain from the leaf cert to CA root + // Use this certificate if such a valid chain exists (possibly using intermediates) + if _, err := leafCert.Verify(x509.VerifyOptions{Roots: t.pinnedCAPool, Intermediates: caIntPool}); err == nil { + return true + } + return false +} + +func (t trustPinChecker) tofusCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { + return true +} + +// Will return the CA filepath corresponding to the most specific (longest) entry in the map that is still a prefix +// of the provided gun. Returns an error if no entry matches this GUN as a prefix. +func getPinnedCAFilepathByPrefix(gun string, t TrustPinConfig) (string, error) { + specificGUN := "" + specificCAFilepath := "" + foundCA := false + for gunPrefix, caFilepath := range t.CA { + if strings.HasPrefix(gun, gunPrefix) && len(gunPrefix) >= len(specificGUN) { + specificGUN = gunPrefix + specificCAFilepath = caFilepath + foundCA = true + } + } + if !foundCA { + return "", fmt.Errorf("could not find pinned CA for GUN: %s\n", gun) + } + return specificCAFilepath, nil +} diff --git a/tuf/signed/sign_test.go b/tuf/signed/sign_test.go index f59cf57b8d..022c5c40c3 100644 --- a/tuf/signed/sign_test.go +++ b/tuf/signed/sign_test.go @@ -6,6 +6,7 @@ import ( "encoding/pem" "io" "testing" + "time" "github.com/docker/go/canonical/json" "github.com/docker/notary/cryptoservice" @@ -211,7 +212,7 @@ func TestSignWithX509(t *testing.T) { require.NoError(t, err) // make a RSA x509 key - cert, err := cryptoservice.GenerateTestingCertificate(privKey.CryptoSigner(), "test") + cert, err := cryptoservice.GenerateCertificate(privKey, "test", time.Now(), time.Now().AddDate(10, 0, 0)) require.NoError(t, err) tufRSAx509Key := trustmanager.CertToKey(cert) diff --git a/tuf/signed/verify.go b/tuf/signed/verify.go index 304de4c616..2fca4d783d 100644 --- a/tuf/signed/verify.go +++ b/tuf/signed/verify.go @@ -36,7 +36,6 @@ func VerifyRoot(s *data.Signed, minVersion int, keys map[string]data.PublicKey) if err != nil { return err } - for _, sig := range s.Signatures { // method lookup is consistent due to Unmarshal JSON doing lower case for us. method := sig.Method @@ -45,7 +44,6 @@ func VerifyRoot(s *data.Signed, minVersion int, keys map[string]data.PublicKey) logrus.Debugf("continuing b/c signing method is not supported for verify root: %s\n", sig.Method) continue } - key, ok := keys[sig.KeyID] if !ok { logrus.Debugf("continuing b/c signing key isn't present in keys: %s\n", sig.KeyID) diff --git a/tuf/utils/util.go b/tuf/utils/util.go index fe5d559897..a3836f6802 100644 --- a/tuf/utils/util.go +++ b/tuf/utils/util.go @@ -98,7 +98,7 @@ func HashedPaths(path string, hashes data.Hashes) []string { // CanonicalKeyID returns the ID of the public bytes version of a TUF key. // On regular RSA/ECDSA TUF keys, this is just the key ID. On X509 RSA/ECDSA -// TUF keys, this is the key ID of the public key part of the key. +// TUF keys, this is the key ID of the public key part of the key in the leaf cert func CanonicalKeyID(k data.PublicKey) (string, error) { switch k.Algorithm() { case data.ECDSAx509Key, data.RSAx509Key: