Skip to content

Commit

Permalink
Output warnings for cert within 6 months expiry, check int cert expir…
Browse files Browse the repository at this point in the history
…y in root

Signed-off-by: Riyaz Faizullabhoy <[email protected]>
  • Loading branch information
riyazdf committed Jul 25, 2016
1 parent 75321dd commit 4ff9f14
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 16 deletions.
63 changes: 51 additions & 12 deletions trustpinning/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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: utils.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"}
Expand All @@ -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
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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("certificate with CN %s is expired", 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
}
249 changes: 246 additions & 3 deletions trustpinning/certs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -782,9 +784,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) {
Expand All @@ -800,3 +802,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")
}
6 changes: 5 additions & 1 deletion tuf/utils/x509.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,11 @@ func ValidateCertificate(c *x509.Certificate) error {
tomorrow := now.AddDate(0, 0, 1)
// Give one day leeway on creation "before" time, check "after" against today
if (tomorrow).Before(c.NotBefore) || now.After(c.NotAfter) {
return fmt.Errorf("certificate is expired")
return fmt.Errorf("certificate with CN %s is expired", c.Subject.CommonName)
}
// 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 with CN %s is near expiry", c.Subject.CommonName)
}
// If we have an RSA key, make sure it's long enough
if c.PublicKeyAlgorithm == x509.RSA {
Expand Down

0 comments on commit 4ff9f14

Please sign in to comment.