diff --git a/certs/certs_test.go b/certs/certs_test.go index a1c63bd797..f1a6456a91 100644 --- a/certs/certs_test.go +++ b/certs/certs_test.go @@ -219,7 +219,7 @@ func TestValidateRootWithoutTOFUS(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) - assert.NoError(t, err, "failed to create a temporary directory: %s", err) + require.NoError(t, err, "failed to create a temporary directory: %s", err) // Create a X509Store trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir) @@ -227,7 +227,7 @@ func TestValidateRootWithoutTOFUS(t *testing.T) { trustPath, trustmanager.FilterCertsExpiredSha1, ) - assert.NoError(t, err) + require.NoError(t, err) // Execute our template templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) @@ -238,7 +238,7 @@ func TestValidateRootWithoutTOFUS(t *testing.T) { // This call to ValidateRoot will fail since we are explicitly disabling TOFU and have no local certs err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{TOFU: false}) - assert.Error(t, err) + require.Error(t, err) } func TestValidateRootWithPinnedCert(t *testing.T) { @@ -248,7 +248,7 @@ func TestValidateRootWithPinnedCert(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) - assert.NoError(t, err, "failed to create a temporary directory: %s", err) + require.NoError(t, err, "failed to create a temporary directory: %s", err) // Create a X509Store trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir) @@ -256,7 +256,7 @@ func TestValidateRootWithPinnedCert(t *testing.T) { trustPath, trustmanager.FilterCertsExpiredSha1, ) - assert.NoError(t, err) + require.NoError(t, err) // Execute our template templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) @@ -267,7 +267,7 @@ func TestValidateRootWithPinnedCert(t *testing.T) { // This call to ValidateRoot should succeed with the correct Cert ID (same as root public key ID) err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": rootPubKeyID}}) - assert.NoError(t, err) + require.NoError(t, err) } func TestValidateRootFailuresWithPinnedCert(t *testing.T) { @@ -277,7 +277,7 @@ func TestValidateRootFailuresWithPinnedCert(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) - assert.NoError(t, err, "failed to create a temporary directory: %s", err) + require.NoError(t, err, "failed to create a temporary directory: %s", err) // Create a X509Store trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir) @@ -285,7 +285,7 @@ func TestValidateRootFailuresWithPinnedCert(t *testing.T) { trustPath, trustmanager.FilterCertsExpiredSha1, ) - assert.NoError(t, err) + require.NoError(t, err) // Execute our template templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) @@ -296,23 +296,23 @@ func TestValidateRootFailuresWithPinnedCert(t *testing.T) { // This call to ValidateRoot should fail due to an incorrect cert ID err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": "ABSOLUTELY NOT A CERT ID"}}) - assert.Error(t, err) + require.Error(t, err) // This call to ValidateRoot should fail due to an empty cert ID err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": ""}}) - assert.Error(t, err) + require.Error(t, err) // This call to ValidateRoot should fail due to an invalid GUN (even though the cert ID is correct), and TOFUS defaults to false err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"not_a_gun": rootPubKeyID}, TOFU: false}) - assert.Error(t, err) + 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", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": targetsPubKeyID}}) - assert.Error(t, err) + require.Error(t, err) // This call to ValidateRoot should succeed because we fall through to TOFUS even though we have no matching GUNs under Certs err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string]string{"not_a_gun": rootPubKeyID}, TOFU: true}) - assert.NoError(t, err) + require.NoError(t, err) } func TestValidateRootWithPinnedCA(t *testing.T) { @@ -322,7 +322,7 @@ func TestValidateRootWithPinnedCA(t *testing.T) { // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) - assert.NoError(t, err, "failed to create a temporary directory: %s", err) + require.NoError(t, err, "failed to create a temporary directory: %s", err) // Create a X509Store trustPath := filepath.Join(tempBaseDir, notary.TrustedCertsDir) @@ -330,7 +330,7 @@ func TestValidateRootWithPinnedCA(t *testing.T) { trustPath, trustmanager.FilterCertsExpiredSha1, ) - assert.NoError(t, err) + require.NoError(t, err) templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot}) @@ -339,42 +339,42 @@ func TestValidateRootWithPinnedCA(t *testing.T) { // This call to ValidateRoot will fail because we have an invalid path for the CA err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}}) - assert.Error(t, err) + 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", notary.TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, TOFU: false}) - assert.Error(t, err) + require.Error(t, err) // 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") - assert.NoError(t, ioutil.WriteFile(invalidCAFilepath, []byte("ABSOLUTELY NOT A PEM"), 0644)) + 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", notary.TrustPinConfig{CA: map[string]string{"docker.com/notary": invalidCAFilepath}, TOFU: false}) - assert.Error(t, err) + 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", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": "invalidID"}, CA: map[string]string{"docker.com/notary": validCAFilepath}, TOFU: false}) - assert.Error(t, err) + 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") - assert.NoError(t, err) + require.NoError(t, err) pemLeafBytes := trustmanager.CertToPEM(leafCert) newRootLeafKey := data.NewPublicKey(data.RSAx509Key, pemLeafBytes) intermediateCert, err := trustmanager.LoadCertFromFile("../fixtures/intermediate-ca.crt") - assert.NoError(t, err) + require.NoError(t, err) pemChainBytes, err := trustmanager.CertChainToPEM([]*x509.Certificate{leafCert, intermediateCert}) - assert.NoError(t, err) + require.NoError(t, err) newRootKey := data.NewPublicKey(data.RSAx509Key, pemChainBytes) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{newRootKey.ID()}, nil) - assert.NoError(t, err) + require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{newRootKey.ID(): newRootKey}, @@ -385,31 +385,31 @@ func TestValidateRootWithPinnedCA(t *testing.T) { data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) - assert.NoError(t, err, "Failed to create new root") + require.NoError(t, err, "Failed to create new root") keyReader, err := os.Open("../fixtures/notary-signer.key") - assert.NoError(t, err, "could not open key file") + require.NoError(t, err, "could not open key file") pemBytes, err := ioutil.ReadAll(keyReader) - assert.NoError(t, err, "could not read key file") + require.NoError(t, err, "could not read key file") privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "") - assert.NoError(t, err) + require.NoError(t, err) store, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever) - assert.NoError(t, err) + require.NoError(t, err) cs := cryptoservice.NewCryptoService(store) err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: "notary-signer"}, privKey) - assert.NoError(t, err) + require.NoError(t, err) newTestSignedRoot, err := testRoot.ToSigned() - assert.NoError(t, err) + require.NoError(t, err) err = signed.Sign(cs, newTestSignedRoot, newRootLeafKey) - assert.NoError(t, err) + require.NoError(t, err) // Check that we validate correctly against a pinned CA and provided bundle err = ValidateRoot(certStore, newTestSignedRoot, "notary-signer", notary.TrustPinConfig{CA: map[string]string{"notary-signer": validCAFilepath}, TOFU: false}) - assert.NoError(t, err) + require.NoError(t, err) } // TestValidateSuccessfulRootRotation runs through a full root certificate rotation diff --git a/certs/trustpin.go b/certs/trustpin.go index 59a0bc46de..e68713f8c5 100644 --- a/certs/trustpin.go +++ b/certs/trustpin.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/docker/notary" "github.com/docker/notary/trustmanager" - "github.com/docker/notary/tuf/utils" "strings" ) @@ -32,36 +31,35 @@ func NewTrustPinChecker(trustPinConfig notary.TrustPinConfig, gun string) (Trust // Determine the mode, and if it's even valid if _, ok := trustPinConfig.Certs[gun]; ok { trustPinChecker.mode = certs - } else if utils.ContainsKeyPrefix(trustPinConfig.CA, gun) { - trustPinChecker.mode = ca - for caGunPrefix, caFilepath := range trustPinConfig.CA { - if strings.HasPrefix(gun, caGunPrefix) { - // Try to add the CA cert to our certificate store, - // and use it to validate certs in the root.json later - caCert, err := trustmanager.LoadCertFromFile(caFilepath) - if err != nil { - return TrustPinChecker{}, fmt.Errorf("could not load root cert from CA path") - } - if err = trustmanager.ValidateCertificate(caCert); err != nil { - return TrustPinChecker{}, fmt.Errorf("invalid CA cert provided") - } - // Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts - caRootPool := x509.NewCertPool() - caRootPool.AddCert(caCert) - if err != nil { - return TrustPinChecker{}, fmt.Errorf("unable to initialize CA cert pool") - } - trustPinChecker.pinnedCAPool = caRootPool - return trustPinChecker, nil - } - } - - } else if trustPinConfig.TOFU { - trustPinChecker.mode = tofus - } else { - return TrustPinChecker{}, fmt.Errorf("no trust pinning specified") + return trustPinChecker, nil } - return trustPinChecker, nil + + if caFilepath, err := trustPinChecker.getCAFilepathByPrefix(gun); err == nil { + trustPinChecker.mode = ca + // 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 TrustPinChecker{}, fmt.Errorf("could not load root cert from CA path") + } + if err = trustmanager.ValidateCertificate(caCerts); err != nil { + return TrustPinChecker{}, fmt.Errorf("invalid CA cert provided") + } + // Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts + caRootPool := x509.NewCertPool() + caRootPool.AddCert(caCerts) + if err != nil { + return TrustPinChecker{}, fmt.Errorf("unable to initialize CA cert pool") + } + trustPinChecker.pinnedCAPool = caRootPool + return trustPinChecker, nil + } + + if trustPinConfig.TOFU { + trustPinChecker.mode = tofus + return trustPinChecker, nil + } + return TrustPinChecker{}, fmt.Errorf("invalid trust pinning specified") } func (t TrustPinChecker) checkCert(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { @@ -88,3 +86,22 @@ func (t TrustPinChecker) checkCert(leafCert *x509.Certificate, intCerts []*x509. } return false } + +// 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 (t TrustPinChecker) getCAFilepathByPrefix(gun string) (string, error) { + specificGUN := "" + specificCAFilepath := "" + foundCA := false + for gunPrefix, caFilepath := range t.config.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/cmd/notary/main_test.go b/cmd/notary/main_test.go index ea97953097..52a521be62 100644 --- a/cmd/notary/main_test.go +++ b/cmd/notary/main_test.go @@ -384,7 +384,7 @@ func TestConfigFileTrustPinning(t *testing.T) { CAFile: "../../fixtures/root-ca.crt", ClientAuth: tls.RequireAndVerifyClientCert, }) - assert.NoError(t, err) + require.NoError(t, err) s.StartTLS() defer s.Close() @@ -406,7 +406,7 @@ func TestConfigFileTrustPinning(t *testing.T) { // set a config file, so it doesn't check ~/.notary/config.json by default, // and execute a random command so that the flags are parsed cwd, err := os.Getwd() - assert.NoError(t, err) + require.NoError(t, err) cmd := commander.GetCommand() cmd.SetArgs([]string{ @@ -416,17 +416,17 @@ func TestConfigFileTrustPinning(t *testing.T) { "--tlskey", "../../fixtures/notary-server.key"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output err = cmd.Execute() - assert.Error(t, err, "there was no repository, so list should have failed") - assert.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") + require.Error(t, err, "there was no repository, so list should have failed") + require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") // Check that tofu was set correctly config, err := commander.parseConfig() - assert.NoError(t, err) - assert.Equal(t, true, config.GetBool("trust_pinning.tofu")) + require.NoError(t, err) + require.Equal(t, true, config.GetBool("trust_pinning.tofu")) // validate that we actually managed to connect and attempted to download the root though - assert.Len(t, m.gotten, 1) - assert.Equal(t, m.gotten[0], "repo.root") + require.Len(t, m.gotten, 1) + require.Equal(t, m.gotten[0], "repo.root") tempDir2 := tempDirWithConfig(t, fmt.Sprintf(`{ "remote_server": { @@ -447,15 +447,15 @@ func TestConfigFileTrustPinning(t *testing.T) { "--tlskey", "../../fixtures/notary-server.key"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output err = cmd.Execute() - assert.Error(t, err, "there was no repository, so list should have failed") - assert.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") + require.Error(t, err, "there was no repository, so list should have failed") + require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") config, err = commander.parseConfig() - assert.NoError(t, err) - assert.Equal(t, false, config.GetBool("trust_pinning.tofu")) + require.NoError(t, err) + require.Equal(t, false, config.GetBool("trust_pinning.tofu")) // Even though TOFUS will fail, we'll have successfully parsed the config and attempted to retrieve the root - assert.Len(t, m.gotten, 2) + require.Len(t, m.gotten, 2) tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ "remote_server": { @@ -478,15 +478,15 @@ func TestConfigFileTrustPinning(t *testing.T) { "--tlskey", "../../fixtures/notary-server.key"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output err = cmd.Execute() - assert.Error(t, err, "there was no repository, so list should have failed") - assert.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") + require.Error(t, err, "there was no repository, so list should have failed") + require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") config, err = commander.parseConfig() - assert.NoError(t, err) - assert.Equal(t, strings.Repeat("x", notary.Sha256HexSize), config.GetStringMap("trust_pinning.certs")["repo3"]) + require.NoError(t, err) + require.Equal(t, strings.Repeat("x", notary.Sha256HexSize), config.GetStringMap("trust_pinning.certs")["repo3"]) // Even though specified certs will fail, we'll have successfully parsed the config and attempted to retrieve the root - assert.Len(t, m.gotten, 3) + require.Len(t, m.gotten, 3) tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ "remote_server": { @@ -509,13 +509,13 @@ func TestConfigFileTrustPinning(t *testing.T) { "--tlskey", "../../fixtures/notary-server.key"}) cmd.SetOutput(new(bytes.Buffer)) // eat the output err = cmd.Execute() - assert.Error(t, err, "there was no repository, so list should have failed") - assert.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") + require.Error(t, err, "there was no repository, so list should have failed") + require.NotContains(t, err.Error(), "TLS", "there was no TLS error though!") config, err = commander.parseConfig() - assert.NoError(t, err) - assert.Equal(t, "root-ca.crt", config.GetStringMap("trust_pinning.ca")["repo4"]) + require.NoError(t, err) + require.Equal(t, "root-ca.crt", config.GetStringMap("trust_pinning.ca")["repo4"]) // Even though specified ca will fail, we'll have successfully parsed the config and attempted to retrieve the root - assert.Len(t, m.gotten, 4) + require.Len(t, m.gotten, 4) } diff --git a/docs/reference/client-config.md b/docs/reference/client-config.md index 30c3417a0e..3100cb9cac 100644 --- a/docs/reference/client-config.md +++ b/docs/reference/client-config.md @@ -154,7 +154,9 @@ bootstrapping of the repo. ca no

Mapping of GUN prefixes to filepaths containing - the root CA with which to verify the certificates in the root file. + 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.

diff --git a/tuf/utils/utils.go b/tuf/utils/utils.go index 4a8639da23..8de72b6797 100644 --- a/tuf/utils/utils.go +++ b/tuf/utils/utils.go @@ -67,16 +67,6 @@ func StrSliceContainsI(ss []string, s string) bool { return false } -// ContainsKeyPrefix checks if the given string map contains a key that prefixes the desired key -func ContainsKeyPrefix(smap map[string]string, s string) bool { - for k := range smap { - if strings.HasPrefix(s, k) { - return true - } - } - return false -} - // FileExists returns true if a file (or dir) exists at the given path, // false otherwise func FileExists(path string) bool {