From 7d6fdc08cdb49d3a0e979efd40433bc2142f709a Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Fri, 11 Mar 2016 11:01:03 -0800 Subject: [PATCH 01/14] Update function description for expected behavior, groundwork for config in validation Signed-off-by: Riyaz Faizullabhoy --- certs/certs.go | 92 ++++++++++++-- certs/certs_test.go | 163 +++++++++++++++++++++++-- client/backwards_compatibility_test.go | 5 +- client/client.go | 6 +- client/client_test.go | 20 +-- client/client_update_test.go | 3 +- client/repo.go | 5 +- client/repo_pkcs11.go | 5 +- cmd/notary/cert.go | 2 +- cmd/notary/delegations.go | 7 +- cmd/notary/delegations_test.go | 2 + cmd/notary/keys.go | 2 +- cmd/notary/keys_test.go | 27 ++-- cmd/notary/main.go | 9 ++ cmd/notary/tuf.go | 17 +-- const.go | 8 ++ tuf/utils/utils.go | 10 ++ 17 files changed, 327 insertions(+), 56 deletions(-) diff --git a/certs/certs.go b/certs/certs.go index 8d3aba2d3b..2b53b90f8b 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -8,9 +8,11 @@ import ( "time" "github.com/Sirupsen/logrus" + "github.com/docker/notary" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/utils" ) // ErrValidationFail is returned when there is no valid trusted certificates @@ -58,25 +60,35 @@ 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 are 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. + +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 notary.TrustPinConfig) error { logrus.Debugf("entered ValidateRoot with dns: %s", gun) signedRoot, err := data.RootFromSigned(root) if err != nil { @@ -101,7 +113,6 @@ 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 { @@ -114,9 +125,72 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin } } else { logrus.Debugf("found no currently valid root certificates for %s", gun) + logrus.Debugf("using trust_pinning config to bootstrap trust: %v", trustPinning) + // First, check if the Certs section is specified for our GUN. + // If so, we try to find a matching Cert that is pinned to bootstrap trust from + if pinnedID, ok := trustPinning.Certs[gun]; ok { + foundCertIDMatch := false + for _, cert := range certsFromRoot { + // Try to match by CertID or public key ID + certID, err := trustmanager.FingerprintCert(cert) + if err != nil { + continue + } + if certID == pinnedID { + // If we found our pinned cert, only use that one certificate as allValidCerts for verification + certsFromRoot = []*x509.Certificate{cert} + foundCertIDMatch = true + break + } + } + // If we didn't find any entries under our GUN in Certs with a matching certificate ID, fail validation + if !foundCertIDMatch { + return &ErrValidationFail{Reason: "failed to find matching certificate ID "} + } + } else if utils.ContainsKeyPrefix(trustPinning.CA, gun) { + // Next, check if the CA section is specified with a GUN that prefixes our GUN. If so, we use this CA to bootstrap trust: + // We attempt to add the CA PEM if it's valid, and all certs to our certStore that are from this CA + if len(trustPinning.CA) > 0 { + for caGunPrefix, caFilepath := range trustPinning.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 &ErrValidationFail{Reason: "failed to load specified CA trust pin"} + } + if err = trustmanager.ValidateCertificate(caCert); err != nil { + return &ErrValidationFail{Reason: "failed to validate specified CA trust pin"} + } + if err = certStore.AddCertFromFile(caFilepath); err != nil { + return &ErrValidationFail{Reason: "failed to add CA trust pin to certificate store"} + } + opts, err := certStore.GetVerifyOptions(gun) + if err != nil { + return &ErrValidationFail{Reason: "failed to add CA trust pin to certificate store for verification"} + } + // Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts + validCertsForCA := []*x509.Certificate{} + for _, cert := range certsFromRoot { + if _, err := cert.Verify(opts); err == nil { + validCertsForCA = append(validCertsForCA, cert) + } + } + certsFromRoot = validCertsForCA + } + } + } + } else if !trustPinning.TOFU { + // If we reach this if-case, it means that we didn't find any local certs/CAs for this GUN, + // nor did we specify any specifications for how to bootstrap trust in trust_pinning using TOFU + // If TOFU is true, we fall through and consider all certificates + return &ErrValidationFail{Reason: "could not bootstrap trust without trust_pinning configuration"} + } } // Validate the integrity of the new root (does it have valid signatures) + // 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)) if err != nil { logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) diff --git a/certs/certs_test.go b/certs/certs_test.go index 84f1ec26d9..1996aa83e3 100644 --- a/certs/certs_test.go +++ b/certs/certs_test.go @@ -32,6 +32,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 +142,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", notary.TrustPinConfig{TOFU: true}) 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", notary.TrustPinConfig{TOFU: true}) require.Error(t, err, "An error was expected") require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) @@ -157,7 +161,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", notary.TrustPinConfig{TOFU: true}) require.Error(t, err, "illegal base64 data at input byte") // @@ -170,7 +174,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", notary.TrustPinConfig{TOFU: true}) require.Error(t, err, "An error was expected") require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) @@ -185,7 +189,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", notary.TrustPinConfig{TOFU: true}) require.Error(t, err, "An error was expected") require.Equal(t, err, &ErrValidationFail{Reason: "unable to retrieve valid leaf certificates"}) @@ -203,11 +207,152 @@ 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", notary.TrustPinConfig{TOFU: true}) require.Error(t, err, "An error was expected") require.Equal(t, err, &ErrValidationFail{Reason: "failed to validate integrity of roots"}) } +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) + assert.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, + ) + assert.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", notary.TrustPinConfig{TOFU: false}) + assert.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) + assert.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, + ) + assert.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", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": rootPubKeyID}}) + assert.NoError(t, err) +} + +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) + assert.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, + ) + assert.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", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": "ABSOLUTELY NOT A CERT ID"}}) + assert.Error(t, err) + certStore.RemoveAll() + + // 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) + certStore.RemoveAll() + + // 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) + certStore.RemoveAll() + + // 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) + certStore.RemoveAll() + + // 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) + certStore.RemoveAll() +} + +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) + assert.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, + ) + assert.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 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) + + // This call to ValidateRoot will fail because we have no valid GUNs to use + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}}) + assert.Error(t, err) +} + // TestValidateSuccessfulRootRotation runs through a full root certificate rotation // We test this with both an RSA and ECDSA root certificate func TestValidateSuccessfulRootRotation(t *testing.T) { @@ -296,7 +441,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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err) // Finally, validate the only trusted certificate that exists is the new one @@ -357,7 +502,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, notary.TrustPinConfig{TOFU: true}) require.Error(t, err, "insuficient signatures on root") // Finally, validate the only trusted certificate that exists is still @@ -421,7 +566,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, notary.TrustPinConfig{TOFU: true}) require.Error(t, err, "insuficient signatures on root") // Finally, validate the only trusted certificate that exists is still diff --git a/client/backwards_compatibility_test.go b/client/backwards_compatibility_test.go index 09abd061e3..f69639609c 100644 --- a/client/backwards_compatibility_test.go +++ b/client/backwards_compatibility_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/docker/notary" "github.com/docker/notary/passphrase" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/store" @@ -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), notary.TrustPinConfig{TOFU: true}) 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), notary.TrustPinConfig{TOFU: true}) 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..eb6f923ab9 100644 --- a/client/client.go +++ b/client/client.go @@ -88,13 +88,14 @@ type NotaryRepository struct { tufRepo *tuf.Repo roundTrip http.RoundTripper CertStore trustmanager.X509Store + trustPinning notary.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 notary.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 = certs.ValidateRoot(r.CertStore, root, r.gun, r.trustPinning) if err != nil { return nil, err } diff --git a/client/client_test.go b/client/client_test.go index e779432525..84dced87f7 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -191,7 +191,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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", repo.gun, rootType) @@ -222,7 +222,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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repository: %s", err) if err != nil && newDir { defer os.RemoveAll(repoDir) @@ -574,7 +574,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, notary.TrustPinConfig{TOFU: true}) 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 +582,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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repo: %s", err) err = repo.Initialize(rootPubKey.ID()) require.EqualError(t, err, trustmanager.ErrAttemptsExceeded{}.Error()) @@ -612,14 +612,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, notary.TrustPinConfig{TOFU: true}) 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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repo: %s", err) err = repo.Initialize(rootPubKey.ID()) require.EqualError(t, err, trustmanager.ErrPasswordInvalid{}.Error()) @@ -1641,7 +1641,7 @@ func TestPublishUninitializedRepo(t *testing.T) { defer os.RemoveAll(tempBaseDir) repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL, - http.DefaultTransport, passphraseRetriever) + http.DefaultTransport, passphraseRetriever, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repository: %s", err) err = repo.Publish() require.Error(t, err) @@ -2005,7 +2005,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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repo: %s", err) cs := cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(passphraseRetriever)) @@ -2863,7 +2863,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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repo: %s", err) _, err = repo.ListTargets(data.CanonicalTargetsRole) @@ -3173,6 +3173,7 @@ func TestBootstrapClientBadURL(t *testing.T) { "http://localhost:9998", http.DefaultTransport, passphraseRetriever, + notary.TrustPinConfig{TOFU: true}, ) require.NoError(t, err, "error creating repo: %s", err) @@ -3202,6 +3203,7 @@ func TestBootstrapClientInvalidURL(t *testing.T) { "#!*)&!)#*^%!#)%^!#", http.DefaultTransport, passphraseRetriever, + notary.TrustPinConfig{TOFU: true}, ) 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..df66a82a56 100644 --- a/client/client_update_test.go +++ b/client/client_update_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/docker/notary" "github.com/docker/notary/certs" "github.com/docker/notary/passphrase" "github.com/docker/notary/tuf/client" @@ -31,7 +32,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"), notary.TrustPinConfig{TOFU: true}) require.NoError(t, err) return repo } diff --git a/client/repo.go b/client/repo.go index d9931891ff..6e5df7bad8 100644 --- a/client/repo.go +++ b/client/repo.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" + "github.com/docker/notary" "github.com/docker/notary/passphrase" "github.com/docker/notary/trustmanager" ) @@ -14,7 +15,7 @@ import ( // 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 notary.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..78dd88888a 100644 --- a/client/repo_pkcs11.go +++ b/client/repo_pkcs11.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" + "github.com/docker/notary" "github.com/docker/notary/passphrase" "github.com/docker/notary/trustmanager" "github.com/docker/notary/trustmanager/yubikey" @@ -15,7 +16,7 @@ import ( // 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 notary.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..a840ef4bf5 100644 --- a/cmd/notary/cert.go +++ b/cmd/notary/cert.go @@ -139,7 +139,7 @@ func (c *certCommander) certRemove(cmd *cobra.Command, args []string) error { // Remove all TUF data, so call RemoveTrustData on a NotaryRepository with the GUN // no online operations are performed so the transport argument is nil nRepo, err := notaryclient.NewNotaryRepository( - trustDir, c.certRemoveGUN, getRemoteTrustServer(config), nil, c.retriever) + trustDir, c.certRemoveGUN, getRemoteTrustServer(config), nil, c.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) 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..26373d46fe 100644 --- a/cmd/notary/delegations.go +++ b/cmd/notary/delegations.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" + "github.com/docker/notary" notaryclient "github.com/docker/notary/client" "github.com/docker/notary/passphrase" "github.com/docker/notary/trustmanager" @@ -85,7 +86,7 @@ func (d *delegationCommander) delegationsList(cmd *cobra.Command, args []string) // 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, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } @@ -140,7 +141,7 @@ func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string // 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, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } @@ -246,7 +247,7 @@ func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) e // 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, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } diff --git a/cmd/notary/delegations_test.go b/cmd/notary/delegations_test.go index e80f5ca460..1ac9e021ad 100644 --- a/cmd/notary/delegations_test.go +++ b/cmd/notary/delegations_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/docker/notary" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/trustmanager" "github.com/spf13/viper" @@ -21,6 +22,7 @@ func setup() *delegationCommander { configGetter: func() (*viper.Viper, error) { mainViper := viper.New() mainViper.Set("trust_dir", testTrustDir) + mainViper.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) return mainViper, nil }, retriever: nil, diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 56d3e54753..ab8fc4cd9b 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -420,7 +420,7 @@ func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error { nRepo, err := notaryclient.NewNotaryRepository( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), - rt, k.getRetriever()) + rt, k.getRetriever(), config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index 20a3b80d41..f821dc098f 100644 --- a/cmd/notary/keys_test.go +++ b/cmd/notary/keys_test.go @@ -249,7 +249,11 @@ func TestRotateKeyInvalidRoles(t *testing.T) { for _, role := range invalids { for _, serverManaged := range []bool{true, false} { k := &keyCommander{ - configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, + configGetter: func() (*viper.Viper, error) { + v := viper.New() + v.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) + return v, nil + }, getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: role, rotateKeyServerManaged: serverManaged, @@ -270,7 +274,11 @@ func TestRotateKeyInvalidRoles(t *testing.T) { func TestRotateKeyTargetCannotBeServerManaged(t *testing.T) { setUp(t) k := &keyCommander{ - configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, + configGetter: func() (*viper.Viper, error) { + v := viper.New() + v.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) + return v, nil + }, getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: data.CanonicalTargetsRole, rotateKeyServerManaged: true, @@ -284,7 +292,11 @@ func TestRotateKeyTargetCannotBeServerManaged(t *testing.T) { func TestRotateKeyTimestampCannotBeLocallyManaged(t *testing.T) { setUp(t) k := &keyCommander{ - configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, + configGetter: func() (*viper.Viper, error) { + v := viper.New() + v.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) + return v, nil + }, getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: data.CanonicalTimestampRole, rotateKeyServerManaged: false, @@ -329,7 +341,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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repo: %s", err) rootPubKey, err := repo.CryptoService.Create("root", "", data.ECDSAKey) @@ -364,6 +376,7 @@ func TestRotateKeyRemoteServerManagesKey(t *testing.T) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) + v.SetDefault("trust_pinning", notary.TrustPinConfig{TOFU: true}) return v, nil }, getRetriever: func() passphrase.Retriever { return ret }, @@ -371,7 +384,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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repo: %s", err) cl, err := repo.GetChangelist() @@ -395,7 +408,6 @@ func TestRotateKeyRemoteServerManagesKey(t *testing.T) { require.True(t, ok) } } - } } @@ -419,6 +431,7 @@ func TestRotateKeyBothKeys(t *testing.T) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) + v.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) return v, nil }, getRetriever: func() passphrase.Retriever { return ret }, @@ -426,7 +439,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, notary.TrustPinConfig{TOFU: true}) require.NoError(t, err, "error creating repo: %s", err) cl, err := repo.GetChangelist() diff --git a/cmd/notary/main.go b/cmd/notary/main.go index 6fa2960064..fbb90394cc 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/Sirupsen/logrus" + "github.com/docker/notary" "github.com/docker/notary/passphrase" "github.com/docker/notary/tuf/data" "github.com/docker/notary/version" @@ -97,6 +98,7 @@ func (n *notaryCommander) parseConfig() (*viper.Viper, error) { // Setup the configuration details into viper config.SetDefault("trust_dir", defaultTrustDir) config.SetDefault("remote_server", map[string]string{"url": defaultServerURL}) + config.SetDefault("trust_pinning", notary.TrustPinConfig{TOFU: true}) // Find and read the config file if err := config.ReadInConfig(); err != nil { @@ -109,6 +111,13 @@ func (n *notaryCommander) parseConfig() (*viper.Viper, error) { } } + // Check that the trust_pinning section was a valid notary.TrustPinConfig object + // We parse this here so that parsing and usage are separated in code, we then use type assertions later + if _, ok := config.Get("trust_pinning").(notary.TrustPinConfig); !ok { + // Return a noisy error if the user passed an invalid trust_pinning section instead of sticking to default TOFUS + return nil, fmt.Errorf("error parsing trust_pinning section of config") + } + // At this point we either have the default value or the one set by the config. // Either way, some command-line flags have precedence and overwrites the value if n.trustDir != "" { diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 397ea39cf3..019d54755d 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -16,6 +16,7 @@ import ( "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/pkg/term" "github.com/docker/go-connections/tlsconfig" + "github.com/docker/notary" notaryclient "github.com/docker/notary/client" "github.com/docker/notary/passphrase" "github.com/docker/notary/tuf/data" @@ -119,7 +120,7 @@ func (t *tufCommander) tufAdd(cmd *cobra.Command, args []string) error { // 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, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } @@ -156,7 +157,7 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } @@ -201,7 +202,7 @@ func (t *tufCommander) tufList(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } @@ -236,7 +237,7 @@ func (t *tufCommander) tufLookup(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } @@ -263,7 +264,7 @@ func (t *tufCommander) tufStatus(cmd *cobra.Command, args []string) error { gun := args[0] nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } @@ -307,7 +308,7 @@ func (t *tufCommander) tufPublish(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } @@ -333,7 +334,7 @@ func (t *tufCommander) tufRemove(cmd *cobra.Command, args []string) error { // 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, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } @@ -372,7 +373,7 @@ func (t *tufCommander) tufVerify(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) if err != nil { return err } diff --git a/const.go b/const.go index f3d834ad16..3724bb99b5 100644 --- a/const.go +++ b/const.go @@ -64,3 +64,11 @@ var NotaryDefaultExpiries = map[string]time.Duration{ "snapshot": NotarySnapshotExpiry, "timestamp": NotaryTimestampExpiry, } + +// 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 + TOFU bool +} diff --git a/tuf/utils/utils.go b/tuf/utils/utils.go index 8de72b6797..4a8639da23 100644 --- a/tuf/utils/utils.go +++ b/tuf/utils/utils.go @@ -67,6 +67,16 @@ 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 { From 043f84aa625dd1bd6d48c2552adebb261599b2c6 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Tue, 15 Mar 2016 17:16:36 -0700 Subject: [PATCH 02/14] Documentation for config, clean up tests Signed-off-by: Riyaz Faizullabhoy --- certs/certs_test.go | 9 ++----- docs/reference/client-config.md | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/certs/certs_test.go b/certs/certs_test.go index 1996aa83e3..901d2ac496 100644 --- a/certs/certs_test.go +++ b/certs/certs_test.go @@ -297,27 +297,22 @@ 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) - certStore.RemoveAll() // 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) - certStore.RemoveAll() // 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) - certStore.RemoveAll() // 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) - certStore.RemoveAll() // 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) - certStore.RemoveAll() } func TestValidateRootWithPinnedCA(t *testing.T) { @@ -348,8 +343,8 @@ func TestValidateRootWithPinnedCA(t *testing.T) { err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}}) assert.Error(t, err) - // This call to ValidateRoot will fail because we have no valid GUNs to use - err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}}) + // 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) } diff --git a/docs/reference/client-config.md b/docs/reference/client-config.md index c92d855c21..b9ddc04aec 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": { + "GUN": "49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292" + } + } } @@ -113,6 +118,48 @@ 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. + +Only one trust pinning option can be used per GUN, 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 with which to verify the certificates in the root file. + The path is relative to the directory of the configuration file.

TOFUno

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

+ ## Environment variables (optional) The following environment variables containing signing key passphrases can From 2d065f241d7adc96a66eeef45455f6575f8ab1df Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Tue, 15 Mar 2016 18:46:30 -0700 Subject: [PATCH 03/14] Change config parsing, add helpers to clean up config usage Signed-off-by: Riyaz Faizullabhoy --- cmd/notary/cert.go | 2 +- cmd/notary/delegations.go | 7 +++---- cmd/notary/delegations_test.go | 3 +-- cmd/notary/keys.go | 2 +- cmd/notary/keys_test.go | 11 ++++++----- cmd/notary/main.go | 10 +--------- cmd/notary/tuf.go | 24 ++++++++++++++++-------- docs/reference/client-config.md | 10 +++++----- 8 files changed, 34 insertions(+), 35 deletions(-) diff --git a/cmd/notary/cert.go b/cmd/notary/cert.go index a840ef4bf5..9cf935e3cc 100644 --- a/cmd/notary/cert.go +++ b/cmd/notary/cert.go @@ -139,7 +139,7 @@ func (c *certCommander) certRemove(cmd *cobra.Command, args []string) error { // Remove all TUF data, so call RemoveTrustData on a NotaryRepository with the GUN // no online operations are performed so the transport argument is nil nRepo, err := notaryclient.NewNotaryRepository( - trustDir, c.certRemoveGUN, getRemoteTrustServer(config), nil, c.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) + trustDir, c.certRemoveGUN, getRemoteTrustServer(config), nil, c.retriever, getTrustPinning(config)) 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 26373d46fe..4784bc4b79 100644 --- a/cmd/notary/delegations.go +++ b/cmd/notary/delegations.go @@ -4,7 +4,6 @@ import ( "fmt" "io/ioutil" - "github.com/docker/notary" notaryclient "github.com/docker/notary/client" "github.com/docker/notary/passphrase" "github.com/docker/notary/trustmanager" @@ -86,7 +85,7 @@ func (d *delegationCommander) delegationsList(cmd *cobra.Command, args []string) // 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.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, d.retriever, getTrustPinning(config)) if err != nil { return err } @@ -141,7 +140,7 @@ func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string // 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.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, getTrustPinning(config)) if err != nil { return err } @@ -247,7 +246,7 @@ func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) e // 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.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, getTrustPinning(config)) if err != nil { return err } diff --git a/cmd/notary/delegations_test.go b/cmd/notary/delegations_test.go index 1ac9e021ad..ee9b464825 100644 --- a/cmd/notary/delegations_test.go +++ b/cmd/notary/delegations_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/docker/notary" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/trustmanager" "github.com/spf13/viper" @@ -22,7 +21,7 @@ func setup() *delegationCommander { configGetter: func() (*viper.Viper, error) { mainViper := viper.New() mainViper.Set("trust_dir", testTrustDir) - mainViper.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) + mainViper.Set("trust_pinning.tofu", true) return mainViper, nil }, retriever: nil, diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index ab8fc4cd9b..81f36686ee 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -420,7 +420,7 @@ func (k *keyCommander) keysRotate(cmd *cobra.Command, args []string) error { nRepo, err := notaryclient.NewNotaryRepository( config.GetString("trust_dir"), gun, getRemoteTrustServer(config), - rt, k.getRetriever(), config.Get("trust_pinning").(notary.TrustPinConfig)) + rt, k.getRetriever(), getTrustPinning(config)) if err != nil { return err } diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index f821dc098f..29ec8533b0 100644 --- a/cmd/notary/keys_test.go +++ b/cmd/notary/keys_test.go @@ -251,7 +251,7 @@ func TestRotateKeyInvalidRoles(t *testing.T) { k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() - v.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) + v.Set("trust_pinning.tofu", true) return v, nil }, getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, @@ -276,7 +276,7 @@ func TestRotateKeyTargetCannotBeServerManaged(t *testing.T) { k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() - v.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) + v.Set("trust_pinning.tofu", true) return v, nil }, getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, @@ -294,7 +294,7 @@ func TestRotateKeyTimestampCannotBeLocallyManaged(t *testing.T) { k := &keyCommander{ configGetter: func() (*viper.Viper, error) { v := viper.New() - v.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) + v.Set("trust_pinning.tofu", true) return v, nil }, getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, @@ -376,7 +376,7 @@ func TestRotateKeyRemoteServerManagesKey(t *testing.T) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) - v.SetDefault("trust_pinning", notary.TrustPinConfig{TOFU: true}) + v.SetDefault("trust_pinning.tofu", true) return v, nil }, getRetriever: func() passphrase.Retriever { return ret }, @@ -431,7 +431,8 @@ func TestRotateKeyBothKeys(t *testing.T) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) - v.Set("trust_pinning", notary.TrustPinConfig{TOFU: true}) + v.Set("trust_pinning.tofu", true) + // won't need a remote server URL, since we are creating local keys return v, nil }, getRetriever: func() passphrase.Retriever { return ret }, diff --git a/cmd/notary/main.go b/cmd/notary/main.go index fbb90394cc..8396048479 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/Sirupsen/logrus" - "github.com/docker/notary" "github.com/docker/notary/passphrase" "github.com/docker/notary/tuf/data" "github.com/docker/notary/version" @@ -98,7 +97,7 @@ func (n *notaryCommander) parseConfig() (*viper.Viper, error) { // Setup the configuration details into viper config.SetDefault("trust_dir", defaultTrustDir) config.SetDefault("remote_server", map[string]string{"url": defaultServerURL}) - config.SetDefault("trust_pinning", notary.TrustPinConfig{TOFU: true}) + config.SetDefault("trust_pinning.tofu", true) // Find and read the config file if err := config.ReadInConfig(); err != nil { @@ -111,13 +110,6 @@ func (n *notaryCommander) parseConfig() (*viper.Viper, error) { } } - // Check that the trust_pinning section was a valid notary.TrustPinConfig object - // We parse this here so that parsing and usage are separated in code, we then use type assertions later - if _, ok := config.Get("trust_pinning").(notary.TrustPinConfig); !ok { - // Return a noisy error if the user passed an invalid trust_pinning section instead of sticking to default TOFUS - return nil, fmt.Errorf("error parsing trust_pinning section of config") - } - // At this point we either have the default value or the one set by the config. // Either way, some command-line flags have precedence and overwrites the value if n.trustDir != "" { diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 019d54755d..e4485a2fd1 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -120,7 +120,7 @@ func (t *tufCommander) tufAdd(cmd *cobra.Command, args []string) error { // 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.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, getTrustPinning(config)) if err != nil { return err } @@ -157,7 +157,7 @@ func (t *tufCommander) tufInit(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, getTrustPinning(config)) if err != nil { return err } @@ -202,7 +202,7 @@ func (t *tufCommander) tufList(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, getTrustPinning(config)) if err != nil { return err } @@ -237,7 +237,7 @@ func (t *tufCommander) tufLookup(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, getTrustPinning(config)) if err != nil { return err } @@ -264,7 +264,7 @@ func (t *tufCommander) tufStatus(cmd *cobra.Command, args []string) error { gun := args[0] nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, getTrustPinning(config)) if err != nil { return err } @@ -308,7 +308,7 @@ func (t *tufCommander) tufPublish(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, getTrustPinning(config)) if err != nil { return err } @@ -334,7 +334,7 @@ func (t *tufCommander) tufRemove(cmd *cobra.Command, args []string) error { // 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.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, getTrustPinning(config)) if err != nil { return err } @@ -373,7 +373,7 @@ func (t *tufCommander) tufVerify(cmd *cobra.Command, args []string) error { } nRepo, err := notaryclient.NewNotaryRepository( - config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, config.Get("trust_pinning").(notary.TrustPinConfig)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, getTrustPinning(config)) if err != nil { return err } @@ -547,3 +547,11 @@ func getRemoteTrustServer(config *viper.Viper) string { } return defaultServerURL } + +func getTrustPinning(config *viper.Viper) notary.TrustPinConfig { + return notary.TrustPinConfig{ + TOFU: config.GetBool("trust_pinning.tofu"), + CA: config.GetStringMapString("trust_pinning.ca"), + Certs: config.GetStringMapString("trust_pinning.certs"), + } +} diff --git a/docs/reference/client-config.md b/docs/reference/client-config.md index b9ddc04aec..8d99683ba7 100644 --- a/docs/reference/client-config.md +++ b/docs/reference/client-config.md @@ -37,8 +37,8 @@ JSON keys to learn more about the configuration section corresponding to that ke "tls_client_key": "./fixtures/secure.example.com.crt" } "trust_pinning": { - "Certs": { - "GUN": "49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292" + "certs": { + "docker.com/notary": "49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292" } } } @@ -139,20 +139,20 @@ will result in a failed bootstrapping of the repo. Description - Certs + certs no

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

- CA + ca no

Mapping of GUN prefixes to filepaths containing the root CA with which to verify the certificates in the root file. The path is relative to the directory of the configuration file.

- TOFU + tofu no

Boolean value determining whether to use trust on first use when bootstrapping validation on a collection's From c2f57536305ed0c1e6d53c1d7929233b5dc21a6f Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Wed, 16 Mar 2016 14:56:57 -0700 Subject: [PATCH 04/14] Update CA pinning logic to include intermediates, add positive test case Signed-off-by: Riyaz Faizullabhoy --- certs/certs.go | 33 +++++--- certs/certs_test.go | 70 +++++++++++++++- cmd/notary/keys_test.go | 1 - cmd/notary/main.go | 5 +- cmd/notary/main_test.go | 150 ++++++++++++++++++++++++++++++++++ trustmanager/x509filestore.go | 2 +- trustmanager/x509memstore.go | 2 +- trustmanager/x509utils.go | 14 +++- 8 files changed, 258 insertions(+), 19 deletions(-) diff --git a/certs/certs.go b/certs/certs.go index 2b53b90f8b..ba74c9f41e 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -71,10 +71,10 @@ of the notary client config. If this section specifies a Certs section with thi GUN, we attempt to validate that the certificates present in the downloaded root file match the pinned ID. -If the Certs section is empty are empty for this GUN, we check if the trust_pinning +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. +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 @@ -149,7 +149,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin } } else if utils.ContainsKeyPrefix(trustPinning.CA, gun) { // Next, check if the CA section is specified with a GUN that prefixes our GUN. If so, we use this CA to bootstrap trust: - // We attempt to add the CA PEM if it's valid, and all certs to our certStore that are from this CA + // We attempt to use the CA PEM if it's valid to add all certs to our certStore that are signed from this cert if len(trustPinning.CA) > 0 { for caGunPrefix, caFilepath := range trustPinning.CA { if strings.HasPrefix(gun, caGunPrefix) { @@ -162,17 +162,28 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin if err = trustmanager.ValidateCertificate(caCert); err != nil { return &ErrValidationFail{Reason: "failed to validate specified CA trust pin"} } - if err = certStore.AddCertFromFile(caFilepath); err != nil { - return &ErrValidationFail{Reason: "failed to add CA trust pin to certificate store"} - } - opts, err := certStore.GetVerifyOptions(gun) - if err != nil { - return &ErrValidationFail{Reason: "failed to add CA trust pin to certificate store for verification"} - } // Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts + caRootPool := x509.NewCertPool() + caRootPool.AddCert(caCert) validCertsForCA := []*x509.Certificate{} + for _, cert := range certsFromRoot { - if _, err := cert.Verify(opts); err == nil { + certID, err := trustmanager.FingerprintCert(cert) + if err != nil { + logrus.Debugf("error while fingerprinting certificate with keyID: %v", err) + continue + } + // Use intermediate certificates included in the root TUF metadata for our validation + caIntPool := x509.NewCertPool() + _, intermediateCerts := parseAllCerts(signedRoot) + if intermediateCertList, ok := intermediateCerts[certID]; ok { + for _, intCert := range intermediateCertList { + 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 = cert.Verify(x509.VerifyOptions{Roots: caRootPool, Intermediates: caIntPool}); err == nil { validCertsForCA = append(validCertsForCA, cert) } } diff --git a/certs/certs_test.go b/certs/certs_test.go index 901d2ac496..a1c63bd797 100644 --- a/certs/certs_test.go +++ b/certs/certs_test.go @@ -332,11 +332,9 @@ func TestValidateRootWithPinnedCA(t *testing.T) { ) assert.NoError(t, err) - // Execute our template templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot}) - - // Unmarshal our signedroot + // Unmarshal our signedRoot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) // This call to ValidateRoot will fail because we have an invalid path for the CA @@ -346,6 +344,72 @@ func TestValidateRootWithPinnedCA(t *testing.T) { // 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) + + // 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)) + + // 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) + + 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) + + // 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) + pemLeafBytes := trustmanager.CertToPEM(leafCert) + newRootLeafKey := data.NewPublicKey(data.RSAx509Key, pemLeafBytes) + + intermediateCert, err := trustmanager.LoadCertFromFile("../fixtures/intermediate-ca.crt") + assert.NoError(t, err) + + pemChainBytes, err := trustmanager.CertChainToPEM([]*x509.Certificate{leafCert, intermediateCert}) + assert.NoError(t, err) + + newRootKey := data.NewPublicKey(data.RSAx509Key, pemChainBytes) + + rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{newRootKey.ID()}, nil) + assert.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, + ) + assert.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") + pemBytes, err := ioutil.ReadAll(keyReader) + assert.NoError(t, err, "could not read key file") + privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "") + assert.NoError(t, err) + + store, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever) + assert.NoError(t, err) + cs := cryptoservice.NewCryptoService(store) + + err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: "notary-signer"}, privKey) + assert.NoError(t, err) + + newTestSignedRoot, err := testRoot.ToSigned() + assert.NoError(t, err) + + err = signed.Sign(cs, newTestSignedRoot, newRootLeafKey) + assert.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) } // TestValidateSuccessfulRootRotation runs through a full root certificate rotation diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index 29ec8533b0..fc54e16f97 100644 --- a/cmd/notary/keys_test.go +++ b/cmd/notary/keys_test.go @@ -432,7 +432,6 @@ func TestRotateKeyBothKeys(t *testing.T) { v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) v.Set("trust_pinning.tofu", true) - // won't need a remote server URL, since we are creating local keys return v, nil }, getRetriever: func() passphrase.Retriever { return ret }, diff --git a/cmd/notary/main.go b/cmd/notary/main.go index 8396048479..29fd684bfd 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -97,7 +97,6 @@ func (n *notaryCommander) parseConfig() (*viper.Viper, error) { // Setup the configuration details into viper config.SetDefault("trust_dir", defaultTrustDir) config.SetDefault("remote_server", map[string]string{"url": defaultServerURL}) - config.SetDefault("trust_pinning.tofu", true) // Find and read the config file if err := config.ReadInConfig(); err != nil { @@ -110,6 +109,10 @@ func (n *notaryCommander) parseConfig() (*viper.Viper, error) { } } + // If the user didn't pass in a value for trust_pinning.tofu, enable TOFUs by default + if config.Get("trust_pinning.tofu") == nil { + config.SetDefault("trust_pinning.tofu", true) + } // At this point we either have the default value or the one set by the config. // Either way, some command-line flags have precedence and overwrites the value if n.trustDir != "" { diff --git a/cmd/notary/main_test.go b/cmd/notary/main_test.go index e1545075f8..ea97953097 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,152 @@ 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) { + // Set up server that with a self signed cert + var err error + // add a handler for getting the root + m := &recordingMetaStore{MemStorage: *storage.NewMemStorage()} + s := httptest.NewUnstartedServer(setupServerHandler(m)) + s.TLS, err = tlsconfig.Server(tlsconfig.Options{ + CertFile: "../../fixtures/notary-server.crt", + KeyFile: "../../fixtures/notary-server.key", + CAFile: "../../fixtures/root-ca.crt", + ClientAuth: tls.RequireAndVerifyClientCert, + }) + assert.NoError(t, err) + s.StartTLS() + defer s.Close() + + commander := ¬aryCommander{ + getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, + } + + tempDir := tempDirWithConfig(t, fmt.Sprintf(`{ + "remote_server": { + "url": "%s" + }, + "trust_pinning": { + "tofu": true + } + }`, s.URL)) + defer os.RemoveAll(tempDir) + configFile := filepath.Join(tempDir, "config.json") + + // 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) + + cmd := commander.GetCommand() + cmd.SetArgs([]string{ + "-c", configFile, "-d", tempDir, "list", "repo", + "--tlscacert", "../../fixtures/root-ca.crt", + "--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")), + "--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!") + + // Check that tofu was set correctly + config, err := commander.parseConfig() + assert.NoError(t, err) + assert.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") + + tempDir2 := tempDirWithConfig(t, fmt.Sprintf(`{ + "remote_server": { + "url": "%s" + }, + "trust_pinning": { + "tofu": false + } + }`, s.URL)) + defer os.RemoveAll(tempDir) + configFile = filepath.Join(tempDir2, "config.json") + + cmd = commander.GetCommand() + cmd.SetArgs([]string{ + "-c", configFile, "-d", tempDir2, "list", "repo2", + "--tlscacert", "../../fixtures/root-ca.crt", + "--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")), + "--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!") + + config, err = commander.parseConfig() + assert.NoError(t, err) + assert.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) + + tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ + "remote_server": { + "url": "%s" + }, + "trust_pinning": { + "certs": { + "repo3": "%s" + } + } + }`, s.URL, strings.Repeat("x", notary.Sha256HexSize))) + defer os.RemoveAll(tempDir) + configFile = filepath.Join(tempDir, "config.json") + + cmd = commander.GetCommand() + cmd.SetArgs([]string{ + "-c", configFile, "-d", tempDir, "list", "repo3", + "--tlscacert", "../../fixtures/root-ca.crt", + "--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")), + "--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!") + + config, err = commander.parseConfig() + assert.NoError(t, err) + assert.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) + + tempDir = tempDirWithConfig(t, fmt.Sprintf(`{ + "remote_server": { + "url": "%s" + }, + "trust_pinning": { + "ca": { + "repo4": "%s" + } + } + }`, s.URL, "root-ca.crt")) + defer os.RemoveAll(tempDir) + configFile = filepath.Join(tempDir, "config.json") + + cmd = commander.GetCommand() + cmd.SetArgs([]string{ + "-c", configFile, "-d", tempDir, "list", "repo4", + "--tlscacert", "../../fixtures/root-ca.crt", + "--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")), + "--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!") + + config, err = commander.parseConfig() + assert.NoError(t, err) + assert.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) +} 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..f7033718bc 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) { From df3bd7dba6556e6381fed8bbbdcc0c73fb0a66d7 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Fri, 1 Apr 2016 16:05:42 -0700 Subject: [PATCH 05/14] Refactor out trust pinning check Signed-off-by: Riyaz Faizullabhoy --- certs/certs.go | 90 +++++++++++------------------------------------ certs/trustpin.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 70 deletions(-) create mode 100644 certs/trustpin.go diff --git a/certs/certs.go b/certs/certs.go index ba74c9f41e..2b46d72b69 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -12,7 +12,6 @@ import ( "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" - "github.com/docker/notary/tuf/utils" ) // ErrValidationFail is returned when there is no valid trusted certificates @@ -126,77 +125,28 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin } else { logrus.Debugf("found no currently valid root certificates for %s", gun) logrus.Debugf("using trust_pinning config to bootstrap trust: %v", trustPinning) - // First, check if the Certs section is specified for our GUN. - // If so, we try to find a matching Cert that is pinned to bootstrap trust from - if pinnedID, ok := trustPinning.Certs[gun]; ok { - foundCertIDMatch := false - for _, cert := range certsFromRoot { - // Try to match by CertID or public key ID - certID, err := trustmanager.FingerprintCert(cert) - if err != nil { - continue - } - if certID == pinnedID { - // If we found our pinned cert, only use that one certificate as allValidCerts for verification - certsFromRoot = []*x509.Certificate{cert} - foundCertIDMatch = true - break - } - } - // If we didn't find any entries under our GUN in Certs with a matching certificate ID, fail validation - if !foundCertIDMatch { - return &ErrValidationFail{Reason: "failed to find matching certificate ID "} - } - } else if utils.ContainsKeyPrefix(trustPinning.CA, gun) { - // Next, check if the CA section is specified with a GUN that prefixes our GUN. If so, we use this CA to bootstrap trust: - // We attempt to use the CA PEM if it's valid to add all certs to our certStore that are signed from this cert - if len(trustPinning.CA) > 0 { - for caGunPrefix, caFilepath := range trustPinning.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 &ErrValidationFail{Reason: "failed to load specified CA trust pin"} - } - if err = trustmanager.ValidateCertificate(caCert); err != nil { - return &ErrValidationFail{Reason: "failed to validate specified CA trust pin"} - } - // Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts - caRootPool := x509.NewCertPool() - caRootPool.AddCert(caCert) - validCertsForCA := []*x509.Certificate{} - for _, cert := range certsFromRoot { - certID, err := trustmanager.FingerprintCert(cert) - if err != nil { - logrus.Debugf("error while fingerprinting certificate with keyID: %v", err) - continue - } - // Use intermediate certificates included in the root TUF metadata for our validation - caIntPool := x509.NewCertPool() - _, intermediateCerts := parseAllCerts(signedRoot) - if intermediateCertList, ok := intermediateCerts[certID]; ok { - for _, intCert := range intermediateCertList { - 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 = cert.Verify(x509.VerifyOptions{Roots: caRootPool, Intermediates: caIntPool}); err == nil { - validCertsForCA = append(validCertsForCA, cert) - } - } - certsFromRoot = validCertsForCA - } - } - } - } else if !trustPinning.TOFU { - // If we reach this if-case, it means that we didn't find any local certs/CAs for this GUN, - // nor did we specify any specifications for how to bootstrap trust in trust_pinning using TOFU - // If TOFU is true, we fall through and consider all certificates - return &ErrValidationFail{Reason: "could not bootstrap trust without trust_pinning configuration"} + trustPinChecker, err := NewTrustPinChecker(trustPinning, gun) + if err != nil { + return &ErrValidationFail{Reason: err.Error()} } + + validPinnedCerts := []*x509.Certificate{} + for _, cert := range certsFromRoot { + _, intermediateCerts := parseAllCerts(signedRoot) + certID, err := trustmanager.FingerprintCert(cert) + if err != nil { + continue + } + if ok := trustPinChecker.checkCert(cert, intermediateCerts[certID]); !ok { + continue + } + validPinnedCerts = append(validPinnedCerts, cert) + } + if len(validPinnedCerts) == 0 { + return &ErrValidationFail{Reason: "unable to match any certificates to trust_pinning config"} + } + allValidCerts = validPinnedCerts } // Validate the integrity of the new root (does it have valid signatures) diff --git a/certs/trustpin.go b/certs/trustpin.go new file mode 100644 index 0000000000..e678bdc6ab --- /dev/null +++ b/certs/trustpin.go @@ -0,0 +1,89 @@ +package certs + +import ( + "crypto/x509" + "fmt" + "github.com/Sirupsen/logrus" + "github.com/docker/notary" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/utils" + "strings" +) + +// TrustPinChecker handles logic for trust pinning for a gun, given a TrustPinConfig +type TrustPinChecker struct { + mode TrustPinMode + gun string + config notary.TrustPinConfig +} + +// TrustPinMode is a type to distinguish possible trust pinning modes +type TrustPinMode string + +const ( + tofus TrustPinMode = "tofu" + ca TrustPinMode = "ca" + certs TrustPinMode = "certs" +) + +// NewTrustPinChecker returns a new TrustPinChecker from a TrustPinConfig for a GUN +func NewTrustPinChecker(trustPinConfig notary.TrustPinConfig, gun string) (TrustPinChecker, error) { + trustPinChecker := TrustPinChecker{config: trustPinConfig, gun: gun} + // Determine the mode, and if it's even valid + if trustPinConfig.TOFU { + trustPinChecker.mode = tofus + } else if _, ok := trustPinConfig.Certs[gun]; ok { + trustPinChecker.mode = certs + } else if utils.ContainsKeyPrefix(trustPinConfig.CA, gun) { + trustPinChecker.mode = ca + } else { + return TrustPinChecker{}, fmt.Errorf("no trust pinning specified") + } + return trustPinChecker, nil +} + +func (t TrustPinChecker) checkCert(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { + switch t.mode { + case tofus: + return true + case certs: + certID, err := trustmanager.FingerprintCert(leafCert) + if err != nil { + return false + } + return t.config.Certs[t.gun] == certID + case ca: + for caGunPrefix, caFilepath := range t.config.CA { + if strings.HasPrefix(t.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 false + } + if err = trustmanager.ValidateCertificate(caCert); err != nil { + return false + } + // Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts + caRootPool := x509.NewCertPool() + caRootPool.AddCert(caCert) + if err != nil { + logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", t.gun, err) + return false + } + + // 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: caRootPool, Intermediates: caIntPool}); err == nil { + return true + } + } + } + } + return false +} From b5534779dff8d507b63cd544d84a5b3ddd64fb25 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Mon, 4 Apr 2016 16:00:46 -0700 Subject: [PATCH 06/14] Include trust pinning flow diagram Signed-off-by: Riyaz Faizullabhoy --- certs/certs.go | 6 ++---- docs/images/trust-pinning-flow.png | Bin 0 -> 63615 bytes docs/reference/client-config.md | 12 +++++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 docs/images/trust-pinning-flow.png diff --git a/certs/certs.go b/certs/certs.go index 2b46d72b69..2007de3878 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -123,9 +123,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin 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("using trust_pinning config to bootstrap trust: %v", trustPinning) - + logrus.Debugf("found no currently valid root certificates for %s, using trust_pinning config to bootstrap trust:", gun, trustPinning) trustPinChecker, err := NewTrustPinChecker(trustPinning, gun) if err != nil { return &ErrValidationFail{Reason: err.Error()} @@ -146,7 +144,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin if len(validPinnedCerts) == 0 { return &ErrValidationFail{Reason: "unable to match any certificates to trust_pinning config"} } - allValidCerts = validPinnedCerts + certsFromRoot = validPinnedCerts } // Validate the integrity of the new root (does it have valid signatures) diff --git a/docs/images/trust-pinning-flow.png b/docs/images/trust-pinning-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..6d883358901f9d7c21f3481de6fa82362f4410aa GIT binary patch literal 63615 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 E2X-#t`~Uy| literal 0 HcmV?d00001 diff --git a/docs/reference/client-config.md b/docs/reference/client-config.md index 8d99683ba7..30c3417a0e 100644 --- a/docs/reference/client-config.md +++ b/docs/reference/client-config.md @@ -127,10 +127,16 @@ 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. +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: -Only one trust pinning option can be used per GUN, and any validation failure -will result in a failed bootstrapping of the repo. +
    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. From 86d9e081fbbd63b44a1ac69ff3480e145b2d1a8c Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Mon, 4 Apr 2016 16:56:42 -0700 Subject: [PATCH 07/14] Refactor initialization for trust pinning, intermediate cert handling Signed-off-by: Riyaz Faizullabhoy --- certs/certs.go | 14 ++++----- certs/trustpin.go | 79 ++++++++++++++++++++++++----------------------- 2 files changed, 47 insertions(+), 46 deletions(-) diff --git a/certs/certs.go b/certs/certs.go index 2007de3878..3443f20a90 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -94,8 +94,9 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin 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"} @@ -131,12 +132,11 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin validPinnedCerts := []*x509.Certificate{} for _, cert := range certsFromRoot { - _, intermediateCerts := parseAllCerts(signedRoot) certID, err := trustmanager.FingerprintCert(cert) if err != nil { continue } - if ok := trustPinChecker.checkCert(cert, intermediateCerts[certID]); !ok { + if ok := trustPinChecker.checkCert(cert, allIntCerts[certID]); !ok { continue } validPinnedCerts = append(validPinnedCerts, cert) @@ -198,9 +198,9 @@ 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) +// validRootLeafCerts returns a list of non-expired, non-sha1 certificates whose +// Common-Names match the provided GUN +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 diff --git a/certs/trustpin.go b/certs/trustpin.go index e678bdc6ab..59a0bc46de 100644 --- a/certs/trustpin.go +++ b/certs/trustpin.go @@ -3,7 +3,6 @@ package certs import ( "crypto/x509" "fmt" - "github.com/Sirupsen/logrus" "github.com/docker/notary" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/utils" @@ -12,9 +11,10 @@ import ( // TrustPinChecker handles logic for trust pinning for a gun, given a TrustPinConfig type TrustPinChecker struct { - mode TrustPinMode - gun string - config notary.TrustPinConfig + mode TrustPinMode + gun string + config notary.TrustPinConfig + pinnedCAPool *x509.CertPool } // TrustPinMode is a type to distinguish possible trust pinning modes @@ -30,12 +30,34 @@ const ( func NewTrustPinChecker(trustPinConfig notary.TrustPinConfig, gun string) (TrustPinChecker, error) { trustPinChecker := TrustPinChecker{config: trustPinConfig, gun: gun} // Determine the mode, and if it's even valid - if trustPinConfig.TOFU { - trustPinChecker.mode = tofus - } else if _, ok := trustPinConfig.Certs[gun]; ok { + 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") } @@ -44,8 +66,6 @@ func NewTrustPinChecker(trustPinConfig notary.TrustPinConfig, gun string) (Trust func (t TrustPinChecker) checkCert(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { switch t.mode { - case tofus: - return true case certs: certID, err := trustmanager.FingerprintCert(leafCert) if err != nil { @@ -53,37 +73,18 @@ func (t TrustPinChecker) checkCert(leafCert *x509.Certificate, intCerts []*x509. } return t.config.Certs[t.gun] == certID case ca: - for caGunPrefix, caFilepath := range t.config.CA { - if strings.HasPrefix(t.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 false - } - if err = trustmanager.ValidateCertificate(caCert); err != nil { - return false - } - // Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts - caRootPool := x509.NewCertPool() - caRootPool.AddCert(caCert) - if err != nil { - logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", t.gun, err) - return false - } - - // 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: caRootPool, Intermediates: caIntPool}); err == nil { - return true - } - } + // 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 + } + case tofus: + return true } return false } From 60faca0661a0151c97c729e6efc083d86b0d0b52 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Wed, 6 Apr 2016 15:03:09 -0700 Subject: [PATCH 08/14] Use most specific CA prefix for pinning Signed-off-by: Riyaz Faizullabhoy --- certs/certs_test.go | 66 ++++++++++++++-------------- certs/trustpin.go | 77 ++++++++++++++++++++------------- cmd/notary/main_test.go | 46 ++++++++++---------- docs/reference/client-config.md | 4 +- tuf/utils/utils.go | 10 ----- 5 files changed, 106 insertions(+), 97 deletions(-) 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. 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 { From bbf51108c0a6d5361dd00344aafc0111d614a072 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Wed, 6 Apr 2016 17:38:11 -0700 Subject: [PATCH 09/14] Rename certs package to trustpinning Signed-off-by: Riyaz Faizullabhoy --- client/client.go | 4 ++-- client/client_root_validation_test.go | 4 ++-- client/client_update_test.go | 16 ++++++++-------- {certs => trustpinning}/certs.go | 2 +- {certs => trustpinning}/certs_test.go | 2 +- {certs => trustpinning}/trustpin.go | 13 ++++++------- 6 files changed, 20 insertions(+), 21 deletions(-) rename {certs => trustpinning}/certs.go (99%) rename {certs => trustpinning}/certs_test.go (99%) rename {certs => trustpinning}/trustpin.go (92%) diff --git a/client/client.go b/client/client.go index eb6f923ab9..37d581261f 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" @@ -875,7 +875,7 @@ func (r *NotaryRepository) validateRoot(rootJSON []byte) (*data.SignedRoot, erro return nil, err } - err = certs.ValidateRoot(r.CertStore, root, r.gun, r.trustPinning) + 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_update_test.go b/client/client_update_test.go index df66a82a56..da2234a176 100644 --- a/client/client_update_test.go +++ b/client/client_update_test.go @@ -15,8 +15,8 @@ import ( "time" "github.com/docker/notary" - "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" @@ -785,29 +785,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) }}, @@ -825,7 +825,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/certs/certs.go b/trustpinning/certs.go similarity index 99% rename from certs/certs.go rename to trustpinning/certs.go index 3443f20a90..e01041bbac 100644 --- a/certs/certs.go +++ b/trustpinning/certs.go @@ -1,4 +1,4 @@ -package certs +package trustpinning import ( "crypto/x509" diff --git a/certs/certs_test.go b/trustpinning/certs_test.go similarity index 99% rename from certs/certs_test.go rename to trustpinning/certs_test.go index f1a6456a91..a92c36cc87 100644 --- a/certs/certs_test.go +++ b/trustpinning/certs_test.go @@ -1,4 +1,4 @@ -package certs +package trustpinning import ( "bytes" diff --git a/certs/trustpin.go b/trustpinning/trustpin.go similarity index 92% rename from certs/trustpin.go rename to trustpinning/trustpin.go index e68713f8c5..1c56c6f590 100644 --- a/certs/trustpin.go +++ b/trustpinning/trustpin.go @@ -1,4 +1,4 @@ -package certs +package trustpinning import ( "crypto/x509" @@ -42,14 +42,13 @@ func NewTrustPinChecker(trustPinConfig notary.TrustPinConfig, gun string) (Trust 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") + for _, caCert := range caCerts { + if err = trustmanager.ValidateCertificate(caCert); err != nil { + return TrustPinChecker{}, fmt.Errorf("invalid CA cert provided") + } + caRootPool.AddCert(caCert) } trustPinChecker.pinnedCAPool = caRootPool return trustPinChecker, nil From 8c9424f051909fb4f27d85a481d701b73d27c104 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Fri, 8 Apr 2016 13:47:23 -0700 Subject: [PATCH 10/14] Allow for multiple pinned certs per gun Signed-off-by: Riyaz Faizullabhoy --- cmd/notary/main_test.go | 4 ++-- cmd/notary/tuf.go | 10 +++++++++- const.go | 2 +- trustpinning/certs_test.go | 21 ++++++++++++++------- trustpinning/trustpin.go | 9 +++++++-- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/cmd/notary/main_test.go b/cmd/notary/main_test.go index 52a521be62..8fbc99ed30 100644 --- a/cmd/notary/main_test.go +++ b/cmd/notary/main_test.go @@ -463,7 +463,7 @@ func TestConfigFileTrustPinning(t *testing.T) { }, "trust_pinning": { "certs": { - "repo3": "%s" + "repo3": ["%s"] } } }`, s.URL, strings.Repeat("x", notary.Sha256HexSize))) @@ -483,7 +483,7 @@ func TestConfigFileTrustPinning(t *testing.T) { config, err = commander.parseConfig() require.NoError(t, err) - require.Equal(t, strings.Repeat("x", notary.Sha256HexSize), config.GetStringMap("trust_pinning.certs")["repo3"]) + require.Equal(t, []interface{}{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 require.Len(t, m.gotten, 3) diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index e4485a2fd1..608893b583 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -549,9 +549,17 @@ func getRemoteTrustServer(config *viper.Viper) string { } func getTrustPinning(config *viper.Viper) notary.TrustPinConfig { + // Need to parse out Certs section from config + certMap := config.GetStringMap("trust_pinning.certs") + resultCertMap := make(map[string][]string) + for gun, certSlice := range certMap { + if castedCertSlice, ok := certSlice.([]string); ok { + resultCertMap[gun] = castedCertSlice + } + } return notary.TrustPinConfig{ TOFU: config.GetBool("trust_pinning.tofu"), CA: config.GetStringMapString("trust_pinning.ca"), - Certs: config.GetStringMapString("trust_pinning.certs"), + Certs: resultCertMap, } } diff --git a/const.go b/const.go index 3724bb99b5..b7151be55b 100644 --- a/const.go +++ b/const.go @@ -69,6 +69,6 @@ var NotaryDefaultExpiries = map[string]time.Duration{ // This struct represents the preferred way to bootstrap trust for this repository type TrustPinConfig struct { CA map[string]string - Certs map[string]string + Certs map[string][]string TOFU bool } diff --git a/trustpinning/certs_test.go b/trustpinning/certs_test.go index a92c36cc87..8b261b52fa 100644 --- a/trustpinning/certs_test.go +++ b/trustpinning/certs_test.go @@ -266,7 +266,14 @@ func TestValidateRootWithPinnedCert(t *testing.T) { 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", notary.TrustPinConfig{Certs: map[string]string{"docker.com/notary": rootPubKeyID}}) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {rootPubKeyID}}}) + 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", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {rootPubKeyID, "invalidID"}}}) require.NoError(t, err) } @@ -295,23 +302,23 @@ func TestValidateRootFailuresWithPinnedCert(t *testing.T) { json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) // 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"}}) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {"ABSOLUTELY NOT A CERT ID"}}}) 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": ""}}) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {""}}}) 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}) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string][]string{"not_a_gun": {rootPubKeyID}}, TOFU: false}) 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}}) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {targetsPubKeyID}}}) 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}) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string][]string{"not_a_gun": {rootPubKeyID}}, TOFU: true}) require.NoError(t, err) } @@ -356,7 +363,7 @@ func TestValidateRootWithPinnedCA(t *testing.T) { 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}) + 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}) 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 diff --git a/trustpinning/trustpin.go b/trustpinning/trustpin.go index 1c56c6f590..abde5e9068 100644 --- a/trustpinning/trustpin.go +++ b/trustpinning/trustpin.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/docker/notary" "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/utils" "strings" ) @@ -46,10 +47,14 @@ func NewTrustPinChecker(trustPinConfig notary.TrustPinConfig, gun string) (Trust caRootPool := x509.NewCertPool() for _, caCert := range caCerts { if err = trustmanager.ValidateCertificate(caCert); err != nil { - return TrustPinChecker{}, fmt.Errorf("invalid CA cert provided") + continue } caRootPool.AddCert(caCert) } + // If we didn't have any valid CA certs, error out + if len(caRootPool.Subjects()) == 0 { + return TrustPinChecker{}, fmt.Errorf("invalid CA certs provided") + } trustPinChecker.pinnedCAPool = caRootPool return trustPinChecker, nil } @@ -68,7 +73,7 @@ func (t TrustPinChecker) checkCert(leafCert *x509.Certificate, intCerts []*x509. if err != nil { return false } - return t.config.Certs[t.gun] == certID + return utils.StrSliceContains(t.config.Certs[t.gun], certID) case ca: // Use intermediate certificates included in the root TUF metadata for our validation caIntPool := x509.NewCertPool() From 94a2e3a741123994a987108f78356d6bf26edc4e Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Mon, 11 Apr 2016 09:56:48 -0700 Subject: [PATCH 11/14] Change config to disable_tofu instead of tofu Signed-off-by: Riyaz Faizullabhoy --- client/backwards_compatibility_test.go | 6 +- client/client.go | 4 +- client/client_test.go | 23 ++-- client/client_update_test.go | 3 +- client/repo.go | 4 +- client/repo_pkcs11.go | 4 +- cmd/notary/cert.go | 6 +- cmd/notary/delegations.go | 21 +++- cmd/notary/delegations_test.go | 1 - cmd/notary/keys.go | 7 +- cmd/notary/keys_test.go | 27 ++--- cmd/notary/main.go | 4 - cmd/notary/main_test.go | 160 +++++++++---------------- cmd/notary/tuf.go | 85 ++++++++++--- const.go | 8 -- cryptoservice/certificate.go | 7 -- docs/reference/client-config.md | 6 +- trustpinning/certs.go | 9 +- trustpinning/certs_test.go | 119 ++++++++++++++---- trustpinning/trustpin.go | 105 ++++++++-------- tuf/signed/sign_test.go | 3 +- 21 files changed, 337 insertions(+), 275 deletions(-) diff --git a/client/backwards_compatibility_test.go b/client/backwards_compatibility_test.go index f69639609c..62b43b77a3 100644 --- a/client/backwards_compatibility_test.go +++ b/client/backwards_compatibility_test.go @@ -12,8 +12,8 @@ import ( "testing" "time" - "github.com/docker/notary" "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" @@ -91,7 +91,7 @@ func Test0Dot1RepoFormat(t *testing.T) { defer ts.Close() repo, err := NewNotaryRepository(tmpDir, gun, ts.URL, http.DefaultTransport, - passphrase.ConstantRetriever(passwd), notary.TrustPinConfig{TOFU: true}) + 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 @@ -153,7 +153,7 @@ func TestDownloading0Dot1RepoFormat(t *testing.T) { defer os.RemoveAll(repoDir) repo, err := NewNotaryRepository(repoDir, gun, ts.URL, http.DefaultTransport, - passphrase.ConstantRetriever(passwd), notary.TrustPinConfig{TOFU: true}) + 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 37d581261f..c52abd42ea 100644 --- a/client/client.go +++ b/client/client.go @@ -88,14 +88,14 @@ type NotaryRepository struct { tufRepo *tuf.Repo roundTrip http.RoundTripper CertStore trustmanager.X509Store - trustPinning notary.TrustPinConfig + 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, trustPin notary.TrustPinConfig) (*NotaryRepository, error) { + keyStores []trustmanager.KeyStore, trustPin trustpinning.TrustPinConfig) (*NotaryRepository, error) { certPath := filepath.Join(baseDir, notary.TrustedCertsDir) certStore, err := trustmanager.NewX509FilteredFileStore( diff --git a/client/client_test.go b/client/client_test.go index 84dced87f7..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, notary.TrustPinConfig{TOFU: true}) + 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, notary.TrustPinConfig{TOFU: true}) + 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, notary.TrustPinConfig{TOFU: true}) + 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, notary.TrustPinConfig{TOFU: true}) + 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, notary.TrustPinConfig{TOFU: true}) + 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, notary.TrustPinConfig{TOFU: true}) + 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, notary.TrustPinConfig{TOFU: true}) + 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, notary.TrustPinConfig{TOFU: true}) + 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, notary.TrustPinConfig{TOFU: true}) + ts.URL, http.DefaultTransport, passphraseRetriever, trustpinning.TrustPinConfig{}) require.NoError(t, err, "error creating repo: %s", err) _, err = repo.ListTargets(data.CanonicalTargetsRole) @@ -3173,7 +3174,7 @@ func TestBootstrapClientBadURL(t *testing.T) { "http://localhost:9998", http.DefaultTransport, passphraseRetriever, - notary.TrustPinConfig{TOFU: true}, + trustpinning.TrustPinConfig{}, ) require.NoError(t, err, "error creating repo: %s", err) @@ -3203,7 +3204,7 @@ func TestBootstrapClientInvalidURL(t *testing.T) { "#!*)&!)#*^%!#)%^!#", http.DefaultTransport, passphraseRetriever, - notary.TrustPinConfig{TOFU: true}, + 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 da2234a176..01b4e37216 100644 --- a/client/client_update_test.go +++ b/client/client_update_test.go @@ -14,7 +14,6 @@ import ( "testing" "time" - "github.com/docker/notary" "github.com/docker/notary/passphrase" "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/client" @@ -32,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"), notary.TrustPinConfig{TOFU: true}) + http.DefaultTransport, passphrase.ConstantRetriever("pass"), trustpinning.TrustPinConfig{}) require.NoError(t, err) return repo } diff --git a/client/repo.go b/client/repo.go index 6e5df7bad8..1612611702 100644 --- a/client/repo.go +++ b/client/repo.go @@ -6,16 +6,16 @@ import ( "fmt" "net/http" - "github.com/docker/notary" "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, trustPinning notary.TrustPinConfig) ( + retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) ( *NotaryRepository, error) { fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) diff --git a/client/repo_pkcs11.go b/client/repo_pkcs11.go index 78dd88888a..b21f5a8169 100644 --- a/client/repo_pkcs11.go +++ b/client/repo_pkcs11.go @@ -6,17 +6,17 @@ import ( "fmt" "net/http" - "github.com/docker/notary" "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, trustPinning notary.TrustPinConfig) ( + retriever passphrase.Retriever, trustPinning trustpinning.TrustPinConfig) ( *NotaryRepository, error) { fileKeyStore, err := trustmanager.NewKeyFileStore(baseDir, retriever) diff --git a/cmd/notary/cert.go b/cmd/notary/cert.go index 9cf935e3cc..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, getTrustPinning(config)) + 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 4784bc4b79..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, getTrustPinning(config)) + 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, getTrustPinning(config)) + 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, getTrustPinning(config)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, d.retriever, trustPin) if err != nil { return err } diff --git a/cmd/notary/delegations_test.go b/cmd/notary/delegations_test.go index ee9b464825..e80f5ca460 100644 --- a/cmd/notary/delegations_test.go +++ b/cmd/notary/delegations_test.go @@ -21,7 +21,6 @@ func setup() *delegationCommander { configGetter: func() (*viper.Viper, error) { mainViper := viper.New() mainViper.Set("trust_dir", testTrustDir) - mainViper.Set("trust_pinning.tofu", true) return mainViper, nil }, retriever: nil, diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 81f36686ee..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(), getTrustPinning(config)) + rt, k.getRetriever(), trustPin) if err != nil { return err } diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index fc54e16f97..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" @@ -249,11 +250,7 @@ func TestRotateKeyInvalidRoles(t *testing.T) { for _, role := range invalids { for _, serverManaged := range []bool{true, false} { k := &keyCommander{ - configGetter: func() (*viper.Viper, error) { - v := viper.New() - v.Set("trust_pinning.tofu", true) - return v, nil - }, + configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: role, rotateKeyServerManaged: serverManaged, @@ -274,11 +271,7 @@ func TestRotateKeyInvalidRoles(t *testing.T) { func TestRotateKeyTargetCannotBeServerManaged(t *testing.T) { setUp(t) k := &keyCommander{ - configGetter: func() (*viper.Viper, error) { - v := viper.New() - v.Set("trust_pinning.tofu", true) - return v, nil - }, + configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: data.CanonicalTargetsRole, rotateKeyServerManaged: true, @@ -292,11 +285,7 @@ func TestRotateKeyTargetCannotBeServerManaged(t *testing.T) { func TestRotateKeyTimestampCannotBeLocallyManaged(t *testing.T) { setUp(t) k := &keyCommander{ - configGetter: func() (*viper.Viper, error) { - v := viper.New() - v.Set("trust_pinning.tofu", true) - return v, nil - }, + configGetter: func() (*viper.Viper, error) { return viper.New(), nil }, getRetriever: func() passphrase.Retriever { return passphrase.ConstantRetriever("pass") }, rotateKeyRole: data.CanonicalTimestampRole, rotateKeyServerManaged: false, @@ -341,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, notary.TrustPinConfig{TOFU: true}) + 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) @@ -376,7 +365,6 @@ func TestRotateKeyRemoteServerManagesKey(t *testing.T) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) - v.SetDefault("trust_pinning.tofu", true) return v, nil }, getRetriever: func() passphrase.Retriever { return ret }, @@ -384,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, notary.TrustPinConfig{TOFU: true}) + 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() @@ -431,7 +419,6 @@ func TestRotateKeyBothKeys(t *testing.T) { v := viper.New() v.SetDefault("trust_dir", tempBaseDir) v.SetDefault("remote_server.url", ts.URL) - v.Set("trust_pinning.tofu", true) return v, nil }, getRetriever: func() passphrase.Retriever { return ret }, @@ -439,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, notary.TrustPinConfig{TOFU: true}) + 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.go b/cmd/notary/main.go index 29fd684bfd..6fa2960064 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -109,10 +109,6 @@ func (n *notaryCommander) parseConfig() (*viper.Viper, error) { } } - // If the user didn't pass in a value for trust_pinning.tofu, enable TOFUs by default - if config.Get("trust_pinning.tofu") == nil { - config.SetDefault("trust_pinning.tofu", true) - } // At this point we either have the default value or the one set by the config. // Either way, some command-line flags have precedence and overwrites the value if n.trustDir != "" { diff --git a/cmd/notary/main_test.go b/cmd/notary/main_test.go index 8fbc99ed30..ca3a787b2a 100644 --- a/cmd/notary/main_test.go +++ b/cmd/notary/main_test.go @@ -373,149 +373,105 @@ func TestConfigFileOverridenByCmdLineFlags(t *testing.T) { // the config can specify trust pinning settings for TOFUs, as well as pinned Certs or CA func TestConfigFileTrustPinning(t *testing.T) { - // Set up server that with a self signed cert var err error - // add a handler for getting the root - m := &recordingMetaStore{MemStorage: *storage.NewMemStorage()} - s := httptest.NewUnstartedServer(setupServerHandler(m)) - s.TLS, err = tlsconfig.Server(tlsconfig.Options{ - CertFile: "../../fixtures/notary-server.crt", - KeyFile: "../../fixtures/notary-server.key", - CAFile: "../../fixtures/root-ca.crt", - ClientAuth: tls.RequireAndVerifyClientCert, - }) - require.NoError(t, err) - s.StartTLS() - defer s.Close() + 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"), } - tempDir := tempDirWithConfig(t, fmt.Sprintf(`{ - "remote_server": { - "url": "%s" - }, - "trust_pinning": { - "tofu": true - } - }`, s.URL)) - defer os.RemoveAll(tempDir) - configFile := filepath.Join(tempDir, "config.json") - - // 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() - require.NoError(t, err) - - cmd := commander.GetCommand() - cmd.SetArgs([]string{ - "-c", configFile, "-d", tempDir, "list", "repo", - "--tlscacert", "../../fixtures/root-ca.crt", - "--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")), - "--tlskey", "../../fixtures/notary-server.key"}) - cmd.SetOutput(new(bytes.Buffer)) // eat the output - err = cmd.Execute() - 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() require.NoError(t, err) - require.Equal(t, true, config.GetBool("trust_pinning.tofu")) + require.Equal(t, false, config.GetBool("trust_pinning.disable_tofu")) + trustPin, err := getTrustPinning(config) + require.NoError(t, err) + require.Equal(t, false, trustPin.DisableTOFU) - // validate that we actually managed to connect and attempted to download the root though - require.Len(t, m.gotten, 1) - require.Equal(t, m.gotten[0], "repo.root") - - tempDir2 := tempDirWithConfig(t, fmt.Sprintf(`{ + tempDir = tempDirWithConfig(t, `{ "remote_server": { "url": "%s" }, "trust_pinning": { - "tofu": false - } - }`, s.URL)) + "disable_tofu": true + } + }`) defer os.RemoveAll(tempDir) - configFile = filepath.Join(tempDir2, "config.json") - - cmd = commander.GetCommand() - cmd.SetArgs([]string{ - "-c", configFile, "-d", tempDir2, "list", "repo2", - "--tlscacert", "../../fixtures/root-ca.crt", - "--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")), - "--tlskey", "../../fixtures/notary-server.key"}) - cmd.SetOutput(new(bytes.Buffer)) // eat the output - err = cmd.Execute() - 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!") + 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, false, config.GetBool("trust_pinning.tofu")) - - // Even though TOFUS will fail, we'll have successfully parsed the config and attempted to retrieve the root - require.Len(t, m.gotten, 2) + 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(`{ - "remote_server": { - "url": "%s" - }, "trust_pinning": { "certs": { "repo3": ["%s"] } } - }`, s.URL, strings.Repeat("x", notary.Sha256HexSize))) + }`, strings.Repeat("x", notary.Sha256HexSize))) defer os.RemoveAll(tempDir) - configFile = filepath.Join(tempDir, "config.json") - - cmd = commander.GetCommand() - cmd.SetArgs([]string{ - "-c", configFile, "-d", tempDir, "list", "repo3", - "--tlscacert", "../../fixtures/root-ca.crt", - "--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")), - "--tlskey", "../../fixtures/notary-server.key"}) - cmd.SetOutput(new(bytes.Buffer)) // eat the output - err = cmd.Execute() - 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!") + 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]) - // Even though specified certs will fail, we'll have successfully parsed the config and attempted to retrieve the root - require.Len(t, m.gotten, 3) + // 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(`{ - "remote_server": { - "url": "%s" - }, "trust_pinning": { "ca": { "repo4": "%s" } } - }`, s.URL, "root-ca.crt")) + }`, "root-ca.crt")) defer os.RemoveAll(tempDir) - configFile = filepath.Join(tempDir, "config.json") - - cmd = commander.GetCommand() - cmd.SetArgs([]string{ - "-c", configFile, "-d", tempDir, "list", "repo4", - "--tlscacert", "../../fixtures/root-ca.crt", - "--tlscert", filepath.Clean(filepath.Join(cwd, "../../fixtures/notary-server.crt")), - "--tlskey", "../../fixtures/notary-server.key"}) - cmd.SetOutput(new(bytes.Buffer)) // eat the output - err = cmd.Execute() - 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!") + 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"]) - - // Even though specified ca will fail, we'll have successfully parsed the config and attempted to retrieve the root - require.Len(t, m.gotten, 4) + 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 608893b583..783472f665 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -16,9 +16,9 @@ import ( "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/pkg/term" "github.com/docker/go-connections/tlsconfig" - "github.com/docker/notary" 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" @@ -117,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, getTrustPinning(config)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, trustPin) if err != nil { return err } @@ -156,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, getTrustPinning(config)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -201,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, getTrustPinning(config)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -236,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, getTrustPinning(config)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -263,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, getTrustPinning(config)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, trustPin) if err != nil { return err } @@ -307,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, getTrustPinning(config)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -331,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, getTrustPinning(config)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, t.retriever, trustPin) if err != nil { return err } @@ -372,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, getTrustPinning(config)) + config.GetString("trust_dir"), gun, getRemoteTrustServer(config), rt, t.retriever, trustPin) if err != nil { return err } @@ -548,18 +588,29 @@ func getRemoteTrustServer(config *viper.Viper) string { return defaultServerURL } -func getTrustPinning(config *viper.Viper) notary.TrustPinConfig { +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 { - if castedCertSlice, ok := certSlice.([]string); ok { - resultCertMap[gun] = castedCertSlice + 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 notary.TrustPinConfig{ - TOFU: config.GetBool("trust_pinning.tofu"), - CA: config.GetStringMapString("trust_pinning.ca"), - Certs: resultCertMap, - } + return trustpinning.TrustPinConfig{ + DisableTOFU: config.GetBool("trust_pinning.disable_tofu"), + CA: config.GetStringMapString("trust_pinning.ca"), + Certs: resultCertMap, + }, nil } diff --git a/const.go b/const.go index b7151be55b..f3d834ad16 100644 --- a/const.go +++ b/const.go @@ -64,11 +64,3 @@ var NotaryDefaultExpiries = map[string]time.Duration{ "snapshot": NotarySnapshotExpiry, "timestamp": NotaryTimestampExpiry, } - -// 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 - TOFU bool -} 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/reference/client-config.md b/docs/reference/client-config.md index 3100cb9cac..e646fe8058 100644 --- a/docs/reference/client-config.md +++ b/docs/reference/client-config.md @@ -38,7 +38,7 @@ JSON keys to learn more about the configuration section corresponding to that ke } "trust_pinning": { "certs": { - "docker.com/notary": "49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292" + "docker.com/notary": ["49cf5c6404a35fa41d5a5aa2ce539dfee0d7a2176d0da488914a38603b1f4292"] } } } @@ -160,11 +160,11 @@ bootstrapping of the repo. The path is relative to the directory of the configuration file.

    - + + root file. This keeps TOFUs on by default.

    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.

    tofudisable_tofu no

    Boolean value determining whether to use trust on first use when bootstrapping validation on a collection's - root file. This is true by default.

    diff --git a/trustpinning/certs.go b/trustpinning/certs.go index e01041bbac..3f20832735 100644 --- a/trustpinning/certs.go +++ b/trustpinning/certs.go @@ -8,7 +8,6 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/docker/notary" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" @@ -87,7 +86,7 @@ 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, trustPinning notary.TrustPinConfig) 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 { @@ -125,7 +124,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin } } else { logrus.Debugf("found no currently valid root certificates for %s, using trust_pinning config to bootstrap trust:", gun, trustPinning) - trustPinChecker, err := NewTrustPinChecker(trustPinning, gun) + trustPinCheckFunc, err := NewTrustPinChecker(trustPinning, gun) if err != nil { return &ErrValidationFail{Reason: err.Error()} } @@ -136,7 +135,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin if err != nil { continue } - if ok := trustPinChecker.checkCert(cert, allIntCerts[certID]); !ok { + if ok := trustPinCheckFunc(cert, allIntCerts[certID]); !ok { continue } validPinnedCerts = append(validPinnedCerts, cert) @@ -198,8 +197,6 @@ 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. -// validRootLeafCerts returns a list of non-expired, non-sha1 certificates whose -// Common-Names match the provided GUN func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string) ([]*x509.Certificate, error) { var validLeafCerts []*x509.Certificate diff --git a/trustpinning/certs_test.go b/trustpinning/certs_test.go index 8b261b52fa..7302267c5d 100644 --- a/trustpinning/certs_test.go +++ b/trustpinning/certs_test.go @@ -16,6 +16,7 @@ import ( "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" "github.com/stretchr/testify/require" + "time" ) type SignedRSARootTemplate struct { @@ -142,12 +143,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", notary.TrustPinConfig{TOFU: true}) + 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", notary.TrustPinConfig{TOFU: true}) + 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"}) @@ -161,7 +162,7 @@ func TestValidateRoot(t *testing.T) { // Unmarshal our signedroot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) - err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{TOFU: true}) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{}) require.Error(t, err, "illegal base64 data at input byte") // @@ -174,7 +175,7 @@ func TestValidateRoot(t *testing.T) { // Unmarshal our signedroot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) - err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{TOFU: true}) + 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"}) @@ -189,7 +190,7 @@ func TestValidateRoot(t *testing.T) { // Unmarshal our signedroot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) - err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{TOFU: true}) + 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"}) @@ -207,7 +208,7 @@ func TestValidateRoot(t *testing.T) { // Unmarshal our signedroot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) - err = ValidateRoot(certStore, &testSignedRoot, "secure.example.com", notary.TrustPinConfig{TOFU: true}) + 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"}) } @@ -237,7 +238,7 @@ func TestValidateRootWithoutTOFUS(t *testing.T) { 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", notary.TrustPinConfig{TOFU: false}) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{DisableTOFU: true}) require.Error(t, err) } @@ -266,14 +267,14 @@ func TestValidateRootWithPinnedCert(t *testing.T) { 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", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {rootPubKeyID}}}) + 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", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {rootPubKeyID, "invalidID"}}}) + err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {rootPubKeyID, "invalidID"}}, DisableTOFU: true}) require.NoError(t, err) } @@ -302,23 +303,23 @@ func TestValidateRootFailuresWithPinnedCert(t *testing.T) { json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) // 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"}}}) + 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", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {""}}}) + 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 defaults to false - err = ValidateRoot(certStore, &testSignedRoot, "docker.com/notary", notary.TrustPinConfig{Certs: map[string][]string{"not_a_gun": {rootPubKeyID}}, TOFU: false}) + // 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", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {targetsPubKeyID}}}) + 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 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}) + // 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) } @@ -345,25 +346,30 @@ func TestValidateRootWithPinnedCA(t *testing.T) { 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", notary.TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}}) + 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", notary.TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, TOFU: false}) + 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", notary.TrustPinConfig{CA: map[string]string{"docker.com/notary": invalidCAFilepath}, TOFU: false}) + 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", notary.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {"invalidID"}}, CA: map[string]string{"docker.com/notary": validCAFilepath}, TOFU: false}) + 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 @@ -411,12 +417,63 @@ func TestValidateRootWithPinnedCA(t *testing.T) { newTestSignedRoot, err := testRoot.ToSigned() require.NoError(t, err) - err = signed.Sign(cs, newTestSignedRoot, newRootLeafKey) + err = signed.Sign(cs, newTestSignedRoot, []data.PublicKey{newRootLeafKey}, 1, nil) 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}) + 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 @@ -457,7 +514,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 @@ -507,7 +564,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, notary.TrustPinConfig{TOFU: true}) + err = ValidateRoot(certStore, signedTestRoot, gun, TrustPinConfig{}) require.NoError(t, err) // Finally, validate the only trusted certificate that exists is the new one @@ -568,7 +625,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, notary.TrustPinConfig{TOFU: true}) + err = ValidateRoot(certStore, signedTestRoot, gun, TrustPinConfig{}) require.Error(t, err, "insuficient signatures on root") // Finally, validate the only trusted certificate that exists is still @@ -632,7 +689,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, notary.TrustPinConfig{TOFU: true}) + err = ValidateRoot(certStore, signedTestRoot, gun, TrustPinConfig{}) require.Error(t, err, "insuficient signatures on root") // Finally, validate the only trusted certificate that exists is still @@ -641,3 +698,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 index abde5e9068..e72f8017a2 100644 --- a/trustpinning/trustpin.go +++ b/trustpinning/trustpin.go @@ -3,47 +3,46 @@ package trustpinning import ( "crypto/x509" "fmt" - "github.com/docker/notary" "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/utils" "strings" ) -// TrustPinChecker handles logic for trust pinning for a gun, given a TrustPinConfig -type TrustPinChecker struct { - mode TrustPinMode - gun string - config notary.TrustPinConfig - pinnedCAPool *x509.CertPool +// 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 } -// TrustPinMode is a type to distinguish possible trust pinning modes -type TrustPinMode string +type trustPinChecker struct { + gun string + config TrustPinConfig + pinnedCAPool *x509.CertPool + pinnedCertIDs []string +} -const ( - tofus TrustPinMode = "tofu" - ca TrustPinMode = "ca" - certs TrustPinMode = "certs" -) +// 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 TrustPinChecker from a TrustPinConfig for a GUN -func NewTrustPinChecker(trustPinConfig notary.TrustPinConfig, gun string) (TrustPinChecker, error) { - trustPinChecker := TrustPinChecker{config: trustPinConfig, gun: gun} +// 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 _, ok := trustPinConfig.Certs[gun]; ok { - trustPinChecker.mode = certs - return trustPinChecker, nil + if pinnedCerts, ok := trustPinConfig.Certs[gun]; ok { + t.pinnedCertIDs = pinnedCerts + return t.certsCheck, nil } - if caFilepath, err := trustPinChecker.getCAFilepathByPrefix(gun); err == nil { - trustPinChecker.mode = ca + 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 TrustPinChecker{}, fmt.Errorf("could not load root cert from CA path") + return nil, fmt.Errorf("could not load root cert from CA path") } - // Now only consider certificates that are direct children from this CA cert, overwriting allValidCerts + // 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 { @@ -53,51 +52,51 @@ func NewTrustPinChecker(trustPinConfig notary.TrustPinConfig, gun string) (Trust } // If we didn't have any valid CA certs, error out if len(caRootPool.Subjects()) == 0 { - return TrustPinChecker{}, fmt.Errorf("invalid CA certs provided") + return nil, fmt.Errorf("invalid CA certs provided") } - trustPinChecker.pinnedCAPool = caRootPool - return trustPinChecker, nil + t.pinnedCAPool = caRootPool + return t.caCheck, nil } - if trustPinConfig.TOFU { - trustPinChecker.mode = tofus - return trustPinChecker, nil + if !trustPinConfig.DisableTOFU { + return t.tofusCheck, nil } - return TrustPinChecker{}, fmt.Errorf("invalid trust pinning specified") + return nil, fmt.Errorf("invalid trust pinning specified") } -func (t TrustPinChecker) checkCert(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { - switch t.mode { - case certs: - certID, err := trustmanager.FingerprintCert(leafCert) - if err != nil { - return false - } - return utils.StrSliceContains(t.config.Certs[t.gun], certID) - case ca: - // 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 - } - case tofus: +func (t trustPinChecker) certsCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { + certID, err := trustmanager.FingerprintCert(leafCert) + if err != nil { + return false + } + return utils.StrSliceContains(t.pinnedCertIDs, certID) +} + +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 (t TrustPinChecker) getCAFilepathByPrefix(gun string) (string, error) { +func getPinnedCAFilepathByPrefix(gun string, t TrustPinConfig) (string, error) { specificGUN := "" specificCAFilepath := "" foundCA := false - for gunPrefix, caFilepath := range t.config.CA { + for gunPrefix, caFilepath := range t.CA { if strings.HasPrefix(gun, gunPrefix) && len(gunPrefix) >= len(specificGUN) { specificGUN = gunPrefix specificCAFilepath = caFilepath 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) From 47767002152f0c162d4874f53e58c24babeaf1bb Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Mon, 18 Apr 2016 18:20:56 -0700 Subject: [PATCH 12/14] add test for using a pinned cert ID when the root Public field has a bundle with intermediates Signed-off-by: David Lawrence (github: endophage) --- trustpinning/certs_test.go | 184 ++++++++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 1 deletion(-) diff --git a/trustpinning/certs_test.go b/trustpinning/certs_test.go index 7302267c5d..9f1cc5fdb8 100644 --- a/trustpinning/certs_test.go +++ b/trustpinning/certs_test.go @@ -2,21 +2,28 @@ 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" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" "github.com/stretchr/testify/require" - "time" ) type SignedRSARootTemplate struct { @@ -278,6 +285,181 @@ func TestValidateRootWithPinnedCert(t *testing.T) { 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": &data.RootRole{ + KeyIDs: []string{ecdsax509Key.ID()}, + Threshold: 1, + }, + "targets": &data.RootRole{ + KeyIDs: []string{otherKey.ID()}, + Threshold: 1, + }, + "snapshot": &data.RootRole{ + KeyIDs: []string{otherKey.ID()}, + Threshold: 1, + }, + "timestamp": &data.RootRole{ + 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 From 26a95ef5a33f63b61246d2de0393584f48db73e4 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Tue, 19 Apr 2016 00:41:16 -0700 Subject: [PATCH 13/14] Handle cert bundles as key IDs Signed-off-by: Riyaz Faizullabhoy --- trustmanager/x509utils.go | 31 +++++++++++++++++++++++++++---- trustmanager/x509utils_test.go | 11 +++++++++-- trustpinning/certs.go | 4 ++-- trustpinning/certs_test.go | 12 +++++------- trustpinning/trustpin.go | 12 ++++++++++-- tuf/signed/verify.go | 2 -- tuf/utils/util.go | 2 +- 7 files changed, 54 insertions(+), 20 deletions(-) diff --git a/trustmanager/x509utils.go b/trustmanager/x509utils.go index f7033718bc..72a36b4716 100644 --- a/trustmanager/x509utils.go +++ b/trustmanager/x509utils.go @@ -556,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 @@ -593,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/trustpinning/certs.go b/trustpinning/certs.go index 3f20832735..1187c40e7e 100644 --- a/trustpinning/certs.go +++ b/trustpinning/certs.go @@ -117,7 +117,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin 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"} @@ -149,7 +149,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin // Validate the integrity of the new root (does it have valid signatures) // 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)) + 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"} diff --git a/trustpinning/certs_test.go b/trustpinning/certs_test.go index 9f1cc5fdb8..e1644709c0 100644 --- a/trustpinning/certs_test.go +++ b/trustpinning/certs_test.go @@ -409,19 +409,19 @@ func TestValidateRootWithPinnerCertAndIntermediates(t *testing.T) { otherKey.ID(): otherKey, }, Roles: map[string]*data.RootRole{ - "root": &data.RootRole{ + "root": { KeyIDs: []string{ecdsax509Key.ID()}, Threshold: 1, }, - "targets": &data.RootRole{ + "targets": { KeyIDs: []string{otherKey.ID()}, Threshold: 1, }, - "snapshot": &data.RootRole{ + "snapshot": { KeyIDs: []string{otherKey.ID()}, Threshold: 1, }, - "timestamp": &data.RootRole{ + "timestamp": { KeyIDs: []string{otherKey.ID()}, Threshold: 1, }, @@ -557,8 +557,6 @@ func TestValidateRootWithPinnedCA(t *testing.T) { // 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) - pemLeafBytes := trustmanager.CertToPEM(leafCert) - newRootLeafKey := data.NewPublicKey(data.RSAx509Key, pemLeafBytes) intermediateCert, err := trustmanager.LoadCertFromFile("../fixtures/intermediate-ca.crt") require.NoError(t, err) @@ -599,7 +597,7 @@ func TestValidateRootWithPinnedCA(t *testing.T) { newTestSignedRoot, err := testRoot.ToSigned() require.NoError(t, err) - err = signed.Sign(cs, newTestSignedRoot, []data.PublicKey{newRootLeafKey}, 1, nil) + 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 diff --git a/trustpinning/trustpin.go b/trustpinning/trustpin.go index e72f8017a2..7af34f2a31 100644 --- a/trustpinning/trustpin.go +++ b/trustpinning/trustpin.go @@ -65,11 +65,19 @@ func NewTrustPinChecker(trustPinConfig TrustPinConfig, gun string) (CertChecker, } func (t trustPinChecker) certsCheck(leafCert *x509.Certificate, intCerts []*x509.Certificate) bool { - certID, err := trustmanager.FingerprintCert(leafCert) + // 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 } - return utils.StrSliceContains(t.pinnedCertIDs, certID) + 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 { 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: From ab4751d4a7be3ea25916ac724f70e31a1fa357ce Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Tue, 19 Apr 2016 10:59:44 -0700 Subject: [PATCH 14/14] Check that leaf cert is first in bundle Signed-off-by: Riyaz Faizullabhoy --- trustpinning/certs.go | 7 ++++++- trustpinning/certs_test.go | 7 ++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/trustpinning/certs.go b/trustpinning/certs.go index 1187c40e7e..4ad275efde 100644 --- a/trustpinning/certs.go +++ b/trustpinning/certs.go @@ -123,7 +123,7 @@ func ValidateRoot(certStore trustmanager.X509Store, root *data.Signed, gun strin return &ErrValidationFail{Reason: "failed to validate data with current trusted certificates"} } } else { - logrus.Debugf("found no currently valid root certificates for %s, using trust_pinning config to bootstrap trust:", gun, trustPinning) + 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()} @@ -275,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/trustpinning/certs_test.go b/trustpinning/certs_test.go index e1644709c0..4b64778931 100644 --- a/trustpinning/certs_test.go +++ b/trustpinning/certs_test.go @@ -202,10 +202,11 @@ func TestValidateRoot(t *testing.T) { 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() @@ -217,7 +218,7 @@ func TestValidateRoot(t *testing.T) { 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) {