// NewTrustPinChecker returns a new certChecker function from a TrustPinConfig for a GUN func NewTrustPinChecker(trustPinConfig TrustPinConfig, gun string) (CertChecker, error) { t := trustPinChecker{gun: gun, config: trustPinConfig} // Determine the mode, and if it's even valid if pinnedCerts, ok := trustPinConfig.Certs[gun]; ok { t.pinnedCertIDs = pinnedCerts return t.certsCheck, nil } if caFilepath, err := getPinnedCAFilepathByPrefix(gun, trustPinConfig); err == nil { // Try to add the CA certs from its bundle file to our certificate store, // and use it to validate certs in the root.json later caCerts, err := trustmanager.LoadCertBundleFromFile(caFilepath) if err != nil { return nil, fmt.Errorf("could not load root cert from CA path") } // Now only consider certificates that are direct children from this CA cert chain caRootPool := x509.NewCertPool() for _, caCert := range caCerts { if err = trustmanager.ValidateCertificate(caCert); err != nil { continue } caRootPool.AddCert(caCert) } // If we didn't have any valid CA certs, error out if len(caRootPool.Subjects()) == 0 { return nil, fmt.Errorf("invalid CA certs provided") } t.pinnedCAPool = caRootPool return t.caCheck, nil } if !trustPinConfig.DisableTOFU { return t.tofusCheck, nil } return nil, fmt.Errorf("invalid trust pinning specified") }
func TestValidateRootWithPinnedCA(t *testing.T) { var testSignedRoot data.Signed var signedRootBytes bytes.Buffer // Temporary directory where test files will be created tempBaseDir, err := ioutil.TempDir("", "notary-test-") defer os.RemoveAll(tempBaseDir) require.NoError(t, err, "failed to create a temporary directory: %s", err) templ, _ := template.New("SignedRSARootTemplate").Parse(signedRSARootTemplate) templ.Execute(&signedRootBytes, SignedRSARootTemplate{RootPem: validPEMEncodedRSARoot}) // Unmarshal our signedRoot json.Unmarshal(signedRootBytes.Bytes(), &testSignedRoot) typedSignedRoot, err := data.RootFromSigned(&testSignedRoot) require.NoError(t, err) // This call to trustpinning.ValidateRoot will fail because we have an invalid path for the CA _, err = trustpinning.ValidateRoot(nil, &testSignedRoot, "docker.com/notary", trustpinning.TrustPinConfig{CA: map[string]string{"docker.com/notary": filepath.Join(tempBaseDir, "nonexistent")}}) require.Error(t, err) // This call to trustpinning.ValidateRoot will fail because we have no valid GUNs to use, and TOFUS is disabled _, err = trustpinning.ValidateRoot(nil, &testSignedRoot, "docker.com/notary", trustpinning.TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, DisableTOFU: true}) require.Error(t, err) // This call to trustpinning.ValidateRoot will succeed because we have no valid GUNs to use and we fall back to enabled TOFUS validatedRoot, err := trustpinning.ValidateRoot(nil, &testSignedRoot, "docker.com/notary", trustpinning.TrustPinConfig{CA: map[string]string{"othergun": filepath.Join(tempBaseDir, "nonexistent")}, DisableTOFU: false}) require.NoError(t, err) generateRootKeyIDs(typedSignedRoot) require.Equal(t, typedSignedRoot, validatedRoot) // 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 trustpinning.ValidateRoot _, err = trustpinning.ValidateRoot(nil, &testSignedRoot, "docker.com/notary", trustpinning.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 = trustpinning.ValidateRoot(nil, &testSignedRoot, "docker.com/notary", trustpinning.TrustPinConfig{Certs: map[string][]string{"docker.com/notary": {"invalidID"}}, CA: map[string]string{"docker.com/notary": validCAFilepath}, DisableTOFU: true}) require.Error(t, err) // Now construct a new root with a valid cert chain, such that signatures are correct over the 'notary-signer' GUN. Pin the root-ca and validate leafCert, err := trustmanager.LoadCertFromFile("../fixtures/notary-signer.crt") require.NoError(t, err) intermediateCert, err := trustmanager.LoadCertFromFile("../fixtures/intermediate-ca.crt") require.NoError(t, err) pemChainBytes, err := trustmanager.CertChainToPEM([]*x509.Certificate{leafCert, intermediateCert}) require.NoError(t, err) newRootKey := data.NewPublicKey(data.RSAx509Key, pemChainBytes) rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{newRootKey.ID()}, nil) require.NoError(t, err) testRoot, err := data.NewRoot( map[string]data.PublicKey{newRootKey.ID(): newRootKey}, map[string]*data.RootRole{ data.CanonicalRootRole: &rootRole.RootRole, data.CanonicalTimestampRole: &rootRole.RootRole, data.CanonicalTargetsRole: &rootRole.RootRole, data.CanonicalSnapshotRole: &rootRole.RootRole}, false, ) testRoot.Signed.Version = 1 require.NoError(t, err, "Failed to create new root") keyReader, err := os.Open("../fixtures/notary-signer.key") require.NoError(t, err, "could not open key file") pemBytes, err := ioutil.ReadAll(keyReader) require.NoError(t, err, "could not read key file") privKey, err := trustmanager.ParsePEMPrivateKey(pemBytes, "") require.NoError(t, err) store, err := trustmanager.NewKeyFileStore(tempBaseDir, passphraseRetriever) require.NoError(t, err) cs := cryptoservice.NewCryptoService(store) err = store.AddKey(trustmanager.KeyInfo{Role: data.CanonicalRootRole, Gun: "notary-signer"}, privKey) require.NoError(t, err) newTestSignedRoot, err := testRoot.ToSigned() require.NoError(t, err) err = signed.Sign(cs, newTestSignedRoot, []data.PublicKey{newRootKey}, 1, nil) require.NoError(t, err) newTypedSignedRoot, err := data.RootFromSigned(newTestSignedRoot) require.NoError(t, err) // Check that we validate correctly against a pinned CA and provided bundle validatedRoot, err = trustpinning.ValidateRoot(nil, newTestSignedRoot, "notary-signer", trustpinning.TrustPinConfig{CA: map[string]string{"notary-signer": validCAFilepath}, DisableTOFU: true}) require.NoError(t, err) generateRootKeyIDs(newTypedSignedRoot) require.Equal(t, newTypedSignedRoot, validatedRoot) // Add an expired CA for the same gun to our previous pinned bundle, ensure that we still validate correctly 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 validatedRoot, err = trustpinning.ValidateRoot(nil, newTestSignedRoot, "notary-signer", trustpinning.TrustPinConfig{CA: map[string]string{"notary-signer": bundleWithExpiredCertPath}, DisableTOFU: true}) require.NoError(t, err) require.Equal(t, newTypedSignedRoot, validatedRoot) 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 = trustpinning.ValidateRoot(nil, newTestSignedRoot, "notary-signer", trustpinning.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 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 = trustpinning.ValidateRoot(nil, newTestSignedRoot, "notary-signer", trustpinning.TrustPinConfig{CA: map[string]string{"notary-signer": bundleWithWrongCertPath}, DisableTOFU: true}) require.Error(t, err) }