diff --git a/trustpinning/certs.go b/trustpinning/certs.go index 7711903e6..b777a3fc8 100644 --- a/trustpinning/certs.go +++ b/trustpinning/certs.go @@ -12,6 +12,8 @@ import ( "github.com/docker/notary/tuf/utils" ) +const wildcard = "*" + // ErrValidationFail is returned when there is no valid trusted certificates // being served inside of the roots.json type ErrValidationFail struct { @@ -175,6 +177,17 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus return data.RootFromSigned(root) } +// MatchCNToGun checks that the common name in a cert is valid for the given gun. +// This allows wildcards as suffixes, e.g. `namespace/*` +func MatchCNToGun(commonName, gun string) bool { + if strings.HasSuffix(commonName, wildcard) { + prefix := strings.TrimRight(commonName, wildcard) + logrus.Debugf("checking gun %s against wildcard prefix %s", gun, prefix) + return strings.HasPrefix(gun, prefix) + } + return commonName == gun +} + // validRootLeafCerts returns a list of possibly (if checkExpiry is true) 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. @@ -183,8 +196,8 @@ func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, c // Go through every leaf certificate and check that the CN matches the gun for id, cert := range allLeafCerts { - // Validate that this leaf certificate has a CN that matches the exact gun - if cert.Subject.CommonName != gun { + // Validate that this leaf certificate has a CN that matches the gun + if !MatchCNToGun(cert.Subject.CommonName, gun) { logrus.Debugf("error leaf certificate CN: %s doesn't match the given GUN: %s", cert.Subject.CommonName, gun) continue diff --git a/trustpinning/certs_test.go b/trustpinning/certs_test.go index 8a0d3e783..cbb36e67a 100644 --- a/trustpinning/certs_test.go +++ b/trustpinning/certs_test.go @@ -1299,3 +1299,79 @@ func TestValidateRootWithExpiredIntermediate(t *testing.T) { ) require.Error(t, err, "failed to invalidate expired intermediate certificate") } + +func TestCheckingWildcardCert(t *testing.T) { + memStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) + cs := cryptoservice.NewCryptoService(memStore) + testPubKey, err := cs.Create(data.CanonicalRootRole, "docker.io/notary/*", data.ECDSAKey) + require.NoError(t, err) + testPrivKey, _, err := memStore.GetKey(testPubKey.ID()) + require.NoError(t, err) + + testCert, err := generateTestingCertificate(testPrivKey, "docker.io/notary/*", notary.Year) + require.NoError(t, err) + testCertPubKey, err := utils.ParsePEMPublicKey(utils.CertToPEM(testCert)) + require.NoError(t, err) + + rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{testCertPubKey.ID()}, nil) + require.NoError(t, err) + testRoot, err := data.NewRoot( + map[string]data.PublicKey{testCertPubKey.ID(): testCertPubKey}, + 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") + + signedTestRoot, err := testRoot.ToSigned() + require.NoError(t, err) + + err = signed.Sign(cs, signedTestRoot, []data.PublicKey{testCertPubKey}, 1, nil) + require.NoError(t, err) + + _, err = trustpinning.ValidateRoot( + nil, + signedTestRoot, + "docker.io/notary/test", + trustpinning.TrustPinConfig{}, + ) + require.NoError(t, err, "expected wildcard cert to validate") + + _, err = trustpinning.ValidateRoot( + nil, + signedTestRoot, + "docker.io/not-a-match", + trustpinning.TrustPinConfig{}, + ) + require.Error(t, err, "expected wildcard cert not to validate") +} + +func TestWildcardMatching(t *testing.T) { + var wildcardTests = []struct { + CN string + gun string + out bool + }{ + {"docker.com/*", "docker.com/notary", true}, + {"docker.com/**", "docker.com/notary", true}, + {"*", "docker.com/any", true}, + {"*", "", true}, + {"**", "docker.com/any", true}, + {"test/*******", "test/many/wildcard", true}, + {"test/**/*/", "test/test", false}, + {"test/*/wild", "test/test/wild", false}, + {"*/all", "test/all", false}, + {"docker.com/*/*", "docker.com/notary/test", false}, + {"docker.com/*/**", "docker.com/notary/test", false}, + {"", "*", false}, + {"*abc*", "abc", false}, + {"test/*/wild*", "test/test/wild", false}, + } + for _, tt := range wildcardTests { + require.Equal(t, trustpinning.MatchCNToGun(tt.CN, tt.gun), tt.out) + } +}