diff --git a/trustmanager/x509utils.go b/trustmanager/x509utils.go index 4289a2890f..1d9b006de8 100644 --- a/trustmanager/x509utils.go +++ b/trustmanager/x509utils.go @@ -233,6 +233,10 @@ func ValidateCertificate(c *x509.Certificate) error { if (tomorrow).Before(c.NotBefore) || now.After(c.NotAfter) { return fmt.Errorf("certificate is expired") } + // If this certificate is expiring within 6 months, put out a warning + if (c.NotAfter).Before(time.Now().AddDate(0, 6, 0)) { + logrus.Warn("certificate is expiring within 6 months") + } // If we have an RSA key, make sure it's long enough if c.PublicKeyAlgorithm == x509.RSA { rsaKey, ok := c.PublicKey.(*rsa.PublicKey) diff --git a/trustpinning/certs.go b/trustpinning/certs.go index 0e584b743e..380fae7ee9 100644 --- a/trustpinning/certs.go +++ b/trustpinning/certs.go @@ -98,6 +98,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus // 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, true) + validIntCerts := validRootIntCerts(allIntCerts) if err != nil { logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err) @@ -137,7 +138,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus validPinnedCerts := map[string]*x509.Certificate{} for id, cert := range certsFromRoot { - if ok := trustPinCheckFunc(cert, allIntCerts[id]); !ok { + if ok := trustPinCheckFunc(cert, validIntCerts[id]); !ok { continue } validPinnedCerts[id] = cert @@ -152,7 +153,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus // 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.VerifySignatures(root, data.BaseRole{ - Keys: trustmanager.CertsToKeys(certsFromRoot, allIntCerts), Threshold: rootRole.Threshold}) + Keys: trustmanager.CertsToKeys(certsFromRoot, validIntCerts), Threshold: rootRole.Threshold}) if err != nil { logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err) return nil, &ErrValidationFail{Reason: "failed to validate integrity of roots"} @@ -177,17 +178,13 @@ func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, c continue } // Make sure the certificate is not expired if checkExpiry is true - if checkExpiry && time.Now().After(cert.NotAfter) { - logrus.Debugf("error leaf certificate is expired") - continue + // and warn if it hasn't expired yet but is within 6 months of expiry + if checkExpiry { + if err := checkCertExpiry(cert); err != nil { + continue + } } - - // We don't allow root certificates that use SHA1 - if cert.SignatureAlgorithm == x509.SHA1WithRSA || - cert.SignatureAlgorithm == x509.DSAWithSHA1 || - cert.SignatureAlgorithm == x509.ECDSAWithSHA1 { - - logrus.Debugf("error certificate uses deprecated hashing algorithm (SHA1)") + if err := checkCertSigAlgorithm(cert); err != nil { continue } @@ -204,6 +201,27 @@ func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, c return validLeafCerts, nil } +// validRootIntCerts filters the passed in structure of intermediate certificates to only include non-expired, non-sha1 certificates +// Note that this "validity" alone does not imply any measure of trust. +func validRootIntCerts(allIntCerts map[string][]*x509.Certificate) map[string][]*x509.Certificate { + validIntCerts := make(map[string][]*x509.Certificate) + + // Go through every leaf cert ID, and build its valid intermediate certificate list + for leafID, intCertList := range allIntCerts { + for _, intCert := range intCertList { + if err := checkCertExpiry(intCert); err != nil { + continue + } + if err := checkCertSigAlgorithm(intCert); err != nil { + continue + } + validIntCerts[leafID] = append(validIntCerts[leafID], intCert) + } + + } + return validIntCerts +} + // parseAllCerts returns two maps, one with all of the leafCertificates and one // with all the intermediate certificates found in signedRoot func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, map[string][]*x509.Certificate) { @@ -266,3 +284,24 @@ func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, m return leafCerts, intCerts } + +func checkCertExpiry(cert *x509.Certificate) error { + if time.Now().After(cert.NotAfter) { + logrus.Debugf("expired certificate for certificate with CN %s", cert.Subject.CommonName) + return fmt.Errorf("certificate expired: %s", cert.Subject.CommonName) + } else if cert.NotAfter.Before(time.Now().AddDate(0, 6, 0)) { + logrus.Warnf("certificate with CN %s is near expiry", cert.Subject.CommonName) + } + return nil +} + +func checkCertSigAlgorithm(cert *x509.Certificate) error { + // We don't allow root certificates that use SHA1 + if cert.SignatureAlgorithm == x509.SHA1WithRSA || + cert.SignatureAlgorithm == x509.DSAWithSHA1 || + cert.SignatureAlgorithm == x509.ECDSAWithSHA1 { + logrus.Debugf("error certificate uses deprecated hashing algorithm (SHA1)") + return fmt.Errorf("invalid signature algorithm for certificate with CN %s", cert.Subject.CommonName) + } + return nil +} diff --git a/trustpinning/certs_test.go b/trustpinning/certs_test.go index 4f26936d10..15d0ac258e 100644 --- a/trustpinning/certs_test.go +++ b/trustpinning/certs_test.go @@ -9,15 +9,17 @@ import ( "crypto/x509/pkix" "encoding/json" "encoding/pem" + "fmt" "io/ioutil" "math/big" "os" "path/filepath" "testing" "text/template" - "time" + "github.com/Sirupsen/logrus" + "github.com/docker/notary" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/trustmanager" "github.com/docker/notary/trustpinning" @@ -781,9 +783,9 @@ func testValidateRootRotationMissingNewSig(t *testing.T, keyAlg, rootKeyType str require.Error(t, err, "insuficient signatures on root") } -func generateTestingCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) { +func generateTestingCertificate(rootKey data.PrivateKey, gun string, timeToExpire time.Duration) (*x509.Certificate, error) { startTime := time.Now() - return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.AddDate(10, 0, 0)) + return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.Add(timeToExpire)) } func generateExpiredTestingCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) { @@ -799,3 +801,244 @@ func generateRootKeyIDs(r *data.SignedRoot) { } } } + +func TestCheckingCertExpiry(t *testing.T) { + gun := "notary" + 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) + testPubKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey) + require.NoError(t, err) + testPrivKey, _, err := memStore.GetKey(testPubKey.ID()) + require.NoError(t, err) + + almostExpiredCert, err := generateTestingCertificate(testPrivKey, gun, notary.Day*30) + require.NoError(t, err) + almostExpiredPubKey, err := trustmanager.ParsePEMPublicKey(trustmanager.CertToPEM(almostExpiredCert)) + require.NoError(t, err) + + // set up a logrus logger to capture warning output + origLevel := logrus.GetLevel() + logrus.SetLevel(logrus.WarnLevel) + defer logrus.SetLevel(origLevel) + logBuf := bytes.NewBuffer(nil) + logrus.SetOutput(logBuf) + + rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{almostExpiredPubKey.ID()}, nil) + require.NoError(t, err) + testRoot, err := data.NewRoot( + map[string]data.PublicKey{almostExpiredPubKey.ID(): almostExpiredPubKey}, + 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{almostExpiredPubKey}, 1, nil) + require.NoError(t, err) + + // This is a valid root certificate, but check that we get a Warn-level message that the certificate is near expiry + _, err = trustpinning.ValidateRoot(nil, signedTestRoot, gun, trustpinning.TrustPinConfig{}) + require.NoError(t, err) + require.Contains(t, logBuf.String(), fmt.Sprintf("certificate with CN %s is near expiry", gun)) + + expiredCert, err := generateExpiredTestingCertificate(testPrivKey, gun) + require.NoError(t, err) + expiredPubKey := trustmanager.CertToKey(expiredCert) + + rootRole, err = data.NewRole(data.CanonicalRootRole, 1, []string{expiredPubKey.ID()}, nil) + require.NoError(t, err) + testRoot, err = data.NewRoot( + map[string]data.PublicKey{expiredPubKey.ID(): expiredPubKey}, + 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{expiredPubKey}, 1, nil) + require.NoError(t, err) + + // This is an invalid root certificate since it's expired + _, err = trustpinning.ValidateRoot(nil, signedTestRoot, gun, trustpinning.TrustPinConfig{}) + require.Error(t, err) +} + +func TestValidateRootWithExpiredIntermediate(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 expired intermediate + intTmpl := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "EXPIRED notary 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/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": { + 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.Error(t, err, "failed to invalidate expired intermediate certificate") +}