diff --git a/trustpinning/certs.go b/trustpinning/certs.go index 7711903e65..068dbab8ea 100644 --- a/trustpinning/certs.go +++ b/trustpinning/certs.go @@ -175,6 +175,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 { + wildcard := "*" + if strings.HasSuffix(commonName, wildcard) { + prefix := commonName[:len(commonName)-len(wildcard)] + 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 +194,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 8a0d3e783d..06f57c92d4 100644 --- a/trustpinning/certs_test.go +++ b/trustpinning/certs_test.go @@ -1299,3 +1299,189 @@ func TestValidateRootWithExpiredIntermediate(t *testing.T) { ) require.Error(t, err, "failed to invalidate expired intermediate certificate") } + +func TestCheckingWilcardCert(t *testing.T) { + now := time.Now() + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + memStore := trustmanager.NewKeyMemoryStore(passphraseRetriever) + 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, + ) + require.NoError(t, err) + + // generate expired intermediate + intTmpl := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "valid testing intermediate", + }, + NotBefore: now.Add(-2 * notary.Year), + NotAfter: now.Add(notary.Year), + 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/*", + }, + 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, + ) + require.NoError(t, err) + + rootBundleWriter := bytes.NewBuffer(nil) + pem.Encode( + rootBundleWriter, + &pem.Block{ + Type: "CERTIFICATE", + Bytes: leafCert, + }, + ) + pem.Encode( + rootBundleWriter, + &pem.Block{ + Type: "CERTIFICATE", + Bytes: intCert, + }, + ) + + rootBundle := rootBundleWriter.Bytes() + + ecdsax509Key := data.NewECDSAx509PublicKey(rootBundle) + + otherKey, err := cs.Create("targets", "docker.io/notary/test", data.ED25519Key) + require.NoError(t, err) + + root := data.SignedRoot{ + Signatures: make([]data.Signature, 0), + Signed: data.Root{ + SignedCommon: data.SignedCommon{ + Type: "Root", + Expires: now.Add(time.Hour), + Version: 1, + }, + Keys: map[string]data.PublicKey{ + ecdsax509Key.ID(): ecdsax509Key, + otherKey.ID(): otherKey, + }, + Roles: map[string]*data.RootRole{ + "root": { + KeyIDs: []string{ecdsax509Key.ID()}, + Threshold: 1, + }, + "targets": { + KeyIDs: []string{otherKey.ID()}, + Threshold: 1, + }, + "snapshot": { + KeyIDs: []string{otherKey.ID()}, + Threshold: 1, + }, + "timestamp": { + KeyIDs: []string{otherKey.ID()}, + Threshold: 1, + }, + }, + }, + Dirty: true, + } + + signedRoot, err := root.ToSigned() + require.NoError(t, err) + err = signed.Sign(cs, signedRoot, []data.PublicKey{ecdsax509Key}, 1, nil) + require.NoError(t, err) + + tempBaseDir, err := ioutil.TempDir("", "notary-test-") + defer os.RemoveAll(tempBaseDir) + require.NoError(t, err, "failed to create a temporary directory: %s", err) + + _, err = trustpinning.ValidateRoot( + nil, + signedRoot, + "docker.io/notary/test", + trustpinning.TrustPinConfig{}, + ) + require.NoError(t, err, "expected wildcard cert to validate") + + _, err = trustpinning.ValidateRoot( + nil, + signedRoot, + "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}, + {"test/*/wild", "test/test/wild", false}, + {"*/all", "test/all", false}, + {"docker.com/*/*", "docker.com/notary/test", false}, + {"*", "docker.com/any", true}, + } + for _, tt := range wildcardTests { + require.Equal(t, trustpinning.MatchCNToGun(tt.CN, tt.gun), tt.out) + } +}