Skip to content
This repository was archived by the owner on Jul 31, 2025. It is now read-only.

Commit 3baf0e4

Browse files
committed
Output warnings for cert within 6 months expiry, check int cert expiry in root
Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
1 parent 88702ca commit 3baf0e4

File tree

3 files changed

+302
-16
lines changed

3 files changed

+302
-16
lines changed

trustpinning/certs.go

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus
9898
// Retrieve all the leaf and intermediate certificates in root for which the CN matches the GUN
9999
allLeafCerts, allIntCerts := parseAllCerts(signedRoot)
100100
certsFromRoot, err := validRootLeafCerts(allLeafCerts, gun, true)
101+
validIntCerts := validRootIntCerts(allIntCerts)
101102

102103
if err != nil {
103104
logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err)
@@ -140,7 +141,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus
140141
validPinnedCerts := map[string]*x509.Certificate{}
141142
for id, cert := range certsFromRoot {
142143
logrus.Debugf("checking trust-pinning for cert: %s", id)
143-
if ok := trustPinCheckFunc(cert, allIntCerts[id]); !ok {
144+
if ok := trustPinCheckFunc(cert, validIntCerts[id]); !ok {
144145
logrus.Debugf("trust-pinning check failed for cert: %s", id)
145146
continue
146147
}
@@ -156,7 +157,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus
156157
// Note that certsFromRoot is guaranteed to be unchanged only if we had prior cert data for this GUN or enabled TOFUS
157158
// If we attempted to pin a certain certificate or CA, certsFromRoot could have been pruned accordingly
158159
err = signed.VerifySignatures(root, data.BaseRole{
159-
Keys: utils.CertsToKeys(certsFromRoot, allIntCerts), Threshold: rootRole.Threshold})
160+
Keys: trustmanager.CertsToKeys(certsFromRoot, validIntCerts), Threshold: rootRole.Threshold})
160161
if err != nil {
161162
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err)
162163
return nil, &ErrValidationFail{Reason: "failed to validate integrity of roots"}
@@ -181,17 +182,13 @@ func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, c
181182
continue
182183
}
183184
// Make sure the certificate is not expired if checkExpiry is true
184-
if checkExpiry && time.Now().After(cert.NotAfter) {
185-
logrus.Debugf("error leaf certificate is expired")
186-
continue
185+
// and warn if it hasn't expired yet but is within 6 months of expiry
186+
if checkExpiry {
187+
if err := checkCertExpiry(cert); err != nil {
188+
continue
189+
}
187190
}
188-
189-
// We don't allow root certificates that use SHA1
190-
if cert.SignatureAlgorithm == x509.SHA1WithRSA ||
191-
cert.SignatureAlgorithm == x509.DSAWithSHA1 ||
192-
cert.SignatureAlgorithm == x509.ECDSAWithSHA1 {
193-
194-
logrus.Debugf("error certificate uses deprecated hashing algorithm (SHA1)")
191+
if err := checkCertSigAlgorithm(cert); err != nil {
195192
continue
196193
}
197194

@@ -208,6 +205,27 @@ func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, c
208205
return validLeafCerts, nil
209206
}
210207

208+
// validRootIntCerts filters the passed in structure of intermediate certificates to only include non-expired, non-sha1 certificates
209+
// Note that this "validity" alone does not imply any measure of trust.
210+
func validRootIntCerts(allIntCerts map[string][]*x509.Certificate) map[string][]*x509.Certificate {
211+
validIntCerts := make(map[string][]*x509.Certificate)
212+
213+
// Go through every leaf cert ID, and build its valid intermediate certificate list
214+
for leafID, intCertList := range allIntCerts {
215+
for _, intCert := range intCertList {
216+
if err := checkCertExpiry(intCert); err != nil {
217+
continue
218+
}
219+
if err := checkCertSigAlgorithm(intCert); err != nil {
220+
continue
221+
}
222+
validIntCerts[leafID] = append(validIntCerts[leafID], intCert)
223+
}
224+
225+
}
226+
return validIntCerts
227+
}
228+
211229
// parseAllCerts returns two maps, one with all of the leafCertificates and one
212230
// with all the intermediate certificates found in signedRoot
213231
func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, map[string][]*x509.Certificate) {
@@ -270,3 +288,24 @@ func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, m
270288

271289
return leafCerts, intCerts
272290
}
291+
292+
func checkCertExpiry(cert *x509.Certificate) error {
293+
if time.Now().After(cert.NotAfter) {
294+
logrus.Debugf("certificate with CN %s is expired", cert.Subject.CommonName)
295+
return fmt.Errorf("certificate expired: %s", cert.Subject.CommonName)
296+
} else if cert.NotAfter.Before(time.Now().AddDate(0, 6, 0)) {
297+
logrus.Warnf("certificate with CN %s is near expiry", cert.Subject.CommonName)
298+
}
299+
return nil
300+
}
301+
302+
func checkCertSigAlgorithm(cert *x509.Certificate) error {
303+
// We don't allow root certificates that use SHA1
304+
if cert.SignatureAlgorithm == x509.SHA1WithRSA ||
305+
cert.SignatureAlgorithm == x509.DSAWithSHA1 ||
306+
cert.SignatureAlgorithm == x509.ECDSAWithSHA1 {
307+
logrus.Debugf("error certificate uses deprecated hashing algorithm (SHA1)")
308+
return fmt.Errorf("invalid signature algorithm for certificate with CN %s", cert.Subject.CommonName)
309+
}
310+
return nil
311+
}

trustpinning/certs_test.go

Lines changed: 246 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ import (
99
"crypto/x509/pkix"
1010
"encoding/json"
1111
"encoding/pem"
12+
"fmt"
1213
"io/ioutil"
1314
"math/big"
1415
"os"
1516
"path/filepath"
1617
"testing"
1718
"text/template"
18-
1919
"time"
2020

21+
"github.com/Sirupsen/logrus"
22+
"github.com/docker/notary"
2123
"github.com/docker/notary/cryptoservice"
2224
"github.com/docker/notary/trustmanager"
2325
"github.com/docker/notary/trustpinning"
@@ -782,9 +784,9 @@ func testValidateRootRotationMissingNewSig(t *testing.T, keyAlg, rootKeyType str
782784
require.Error(t, err, "insuficient signatures on root")
783785
}
784786

785-
func generateTestingCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) {
787+
func generateTestingCertificate(rootKey data.PrivateKey, gun string, timeToExpire time.Duration) (*x509.Certificate, error) {
786788
startTime := time.Now()
787-
return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.AddDate(10, 0, 0))
789+
return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.Add(timeToExpire))
788790
}
789791

790792
func generateExpiredTestingCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) {
@@ -800,3 +802,244 @@ func generateRootKeyIDs(r *data.SignedRoot) {
800802
}
801803
}
802804
}
805+
806+
func TestCheckingCertExpiry(t *testing.T) {
807+
gun := "notary"
808+
pass := func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) {
809+
return "password", false, nil
810+
}
811+
memStore := trustmanager.NewKeyMemoryStore(pass)
812+
cs := cryptoservice.NewCryptoService(memStore)
813+
testPubKey, err := cs.Create(data.CanonicalRootRole, gun, data.ECDSAKey)
814+
require.NoError(t, err)
815+
testPrivKey, _, err := memStore.GetKey(testPubKey.ID())
816+
require.NoError(t, err)
817+
818+
almostExpiredCert, err := generateTestingCertificate(testPrivKey, gun, notary.Day*30)
819+
require.NoError(t, err)
820+
almostExpiredPubKey, err := trustmanager.ParsePEMPublicKey(trustmanager.CertToPEM(almostExpiredCert))
821+
require.NoError(t, err)
822+
823+
// set up a logrus logger to capture warning output
824+
origLevel := logrus.GetLevel()
825+
logrus.SetLevel(logrus.WarnLevel)
826+
defer logrus.SetLevel(origLevel)
827+
logBuf := bytes.NewBuffer(nil)
828+
logrus.SetOutput(logBuf)
829+
830+
rootRole, err := data.NewRole(data.CanonicalRootRole, 1, []string{almostExpiredPubKey.ID()}, nil)
831+
require.NoError(t, err)
832+
testRoot, err := data.NewRoot(
833+
map[string]data.PublicKey{almostExpiredPubKey.ID(): almostExpiredPubKey},
834+
map[string]*data.RootRole{
835+
data.CanonicalRootRole: &rootRole.RootRole,
836+
data.CanonicalTimestampRole: &rootRole.RootRole,
837+
data.CanonicalTargetsRole: &rootRole.RootRole,
838+
data.CanonicalSnapshotRole: &rootRole.RootRole},
839+
false,
840+
)
841+
testRoot.Signed.Version = 1
842+
require.NoError(t, err, "Failed to create new root")
843+
844+
signedTestRoot, err := testRoot.ToSigned()
845+
require.NoError(t, err)
846+
847+
err = signed.Sign(cs, signedTestRoot, []data.PublicKey{almostExpiredPubKey}, 1, nil)
848+
require.NoError(t, err)
849+
850+
// This is a valid root certificate, but check that we get a Warn-level message that the certificate is near expiry
851+
_, err = trustpinning.ValidateRoot(nil, signedTestRoot, gun, trustpinning.TrustPinConfig{})
852+
require.NoError(t, err)
853+
require.Contains(t, logBuf.String(), fmt.Sprintf("certificate with CN %s is near expiry", gun))
854+
855+
expiredCert, err := generateExpiredTestingCertificate(testPrivKey, gun)
856+
require.NoError(t, err)
857+
expiredPubKey := trustmanager.CertToKey(expiredCert)
858+
859+
rootRole, err = data.NewRole(data.CanonicalRootRole, 1, []string{expiredPubKey.ID()}, nil)
860+
require.NoError(t, err)
861+
testRoot, err = data.NewRoot(
862+
map[string]data.PublicKey{expiredPubKey.ID(): expiredPubKey},
863+
map[string]*data.RootRole{
864+
data.CanonicalRootRole: &rootRole.RootRole,
865+
data.CanonicalTimestampRole: &rootRole.RootRole,
866+
data.CanonicalTargetsRole: &rootRole.RootRole,
867+
data.CanonicalSnapshotRole: &rootRole.RootRole},
868+
false,
869+
)
870+
testRoot.Signed.Version = 1
871+
require.NoError(t, err, "Failed to create new root")
872+
873+
signedTestRoot, err = testRoot.ToSigned()
874+
require.NoError(t, err)
875+
876+
err = signed.Sign(cs, signedTestRoot, []data.PublicKey{expiredPubKey}, 1, nil)
877+
require.NoError(t, err)
878+
879+
// This is an invalid root certificate since it's expired
880+
_, err = trustpinning.ValidateRoot(nil, signedTestRoot, gun, trustpinning.TrustPinConfig{})
881+
require.Error(t, err)
882+
}
883+
884+
func TestValidateRootWithExpiredIntermediate(t *testing.T) {
885+
now := time.Now()
886+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
887+
888+
pass := func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) {
889+
return "password", false, nil
890+
}
891+
memStore := trustmanager.NewKeyMemoryStore(pass)
892+
cs := cryptoservice.NewCryptoService(memStore)
893+
894+
// generate CA cert
895+
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
896+
require.NoError(t, err)
897+
caTmpl := x509.Certificate{
898+
SerialNumber: serialNumber,
899+
Subject: pkix.Name{
900+
CommonName: "notary testing CA",
901+
},
902+
NotBefore: now.Add(-time.Hour),
903+
NotAfter: now.Add(time.Hour),
904+
KeyUsage: x509.KeyUsageCertSign,
905+
BasicConstraintsValid: true,
906+
IsCA: true,
907+
MaxPathLen: 3,
908+
}
909+
caPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
910+
require.NoError(t, err)
911+
_, err = x509.CreateCertificate(
912+
rand.Reader,
913+
&caTmpl,
914+
&caTmpl,
915+
caPrivKey.Public(),
916+
caPrivKey,
917+
)
918+
919+
// generate expired intermediate
920+
intTmpl := x509.Certificate{
921+
SerialNumber: serialNumber,
922+
Subject: pkix.Name{
923+
CommonName: "EXPIRED notary testing intermediate",
924+
},
925+
NotBefore: now.Add(-2 * notary.Year),
926+
NotAfter: now.Add(-notary.Year),
927+
KeyUsage: x509.KeyUsageCertSign,
928+
BasicConstraintsValid: true,
929+
IsCA: true,
930+
MaxPathLen: 2,
931+
}
932+
intPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
933+
require.NoError(t, err)
934+
intCert, err := x509.CreateCertificate(
935+
rand.Reader,
936+
&intTmpl,
937+
&caTmpl,
938+
intPrivKey.Public(),
939+
caPrivKey,
940+
)
941+
require.NoError(t, err)
942+
943+
// generate leaf
944+
serialNumber, err = rand.Int(rand.Reader, serialNumberLimit)
945+
require.NoError(t, err)
946+
leafTmpl := x509.Certificate{
947+
SerialNumber: serialNumber,
948+
Subject: pkix.Name{
949+
CommonName: "docker.io/notary/test",
950+
},
951+
NotBefore: now.Add(-time.Hour),
952+
NotAfter: now.Add(time.Hour),
953+
954+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
955+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
956+
BasicConstraintsValid: true,
957+
}
958+
959+
leafPubKey, err := cs.Create("root", "docker.io/notary/test", data.ECDSAKey)
960+
require.NoError(t, err)
961+
leafPrivKey, _, err := cs.GetPrivateKey(leafPubKey.ID())
962+
require.NoError(t, err)
963+
signer := leafPrivKey.CryptoSigner()
964+
leafCert, err := x509.CreateCertificate(
965+
rand.Reader,
966+
&leafTmpl,
967+
&intTmpl,
968+
signer.Public(),
969+
intPrivKey,
970+
)
971+
972+
rootBundleWriter := bytes.NewBuffer(nil)
973+
pem.Encode(
974+
rootBundleWriter,
975+
&pem.Block{
976+
Type: "CERTIFICATE",
977+
Bytes: leafCert,
978+
},
979+
)
980+
pem.Encode(
981+
rootBundleWriter,
982+
&pem.Block{
983+
Type: "CERTIFICATE",
984+
Bytes: intCert,
985+
},
986+
)
987+
988+
rootBundle := rootBundleWriter.Bytes()
989+
990+
ecdsax509Key := data.NewECDSAx509PublicKey(rootBundle)
991+
992+
otherKey, err := cs.Create("targets", "docker.io/notary/test", data.ED25519Key)
993+
require.NoError(t, err)
994+
995+
root := data.SignedRoot{
996+
Signatures: make([]data.Signature, 0),
997+
Signed: data.Root{
998+
SignedCommon: data.SignedCommon{
999+
Type: "Root",
1000+
Expires: now.Add(time.Hour),
1001+
Version: 1,
1002+
},
1003+
Keys: map[string]data.PublicKey{
1004+
ecdsax509Key.ID(): ecdsax509Key,
1005+
otherKey.ID(): otherKey,
1006+
},
1007+
Roles: map[string]*data.RootRole{
1008+
"root": {
1009+
KeyIDs: []string{ecdsax509Key.ID()},
1010+
Threshold: 1,
1011+
},
1012+
"targets": {
1013+
KeyIDs: []string{otherKey.ID()},
1014+
Threshold: 1,
1015+
},
1016+
"snapshot": {
1017+
KeyIDs: []string{otherKey.ID()},
1018+
Threshold: 1,
1019+
},
1020+
"timestamp": {
1021+
KeyIDs: []string{otherKey.ID()},
1022+
Threshold: 1,
1023+
},
1024+
},
1025+
},
1026+
Dirty: true,
1027+
}
1028+
1029+
signedRoot, err := root.ToSigned()
1030+
require.NoError(t, err)
1031+
err = signed.Sign(cs, signedRoot, []data.PublicKey{ecdsax509Key}, 1, nil)
1032+
require.NoError(t, err)
1033+
1034+
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
1035+
defer os.RemoveAll(tempBaseDir)
1036+
require.NoError(t, err, "failed to create a temporary directory: %s", err)
1037+
1038+
_, err = trustpinning.ValidateRoot(
1039+
nil,
1040+
signedRoot,
1041+
"docker.io/notary/test",
1042+
trustpinning.TrustPinConfig{},
1043+
)
1044+
require.Error(t, err, "failed to invalidate expired intermediate certificate")
1045+
}

tuf/utils/x509.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,11 @@ func ValidateCertificate(c *x509.Certificate) error {
267267
tomorrow := now.AddDate(0, 0, 1)
268268
// Give one day leeway on creation "before" time, check "after" against today
269269
if (tomorrow).Before(c.NotBefore) || now.After(c.NotAfter) {
270-
return fmt.Errorf("certificate is expired")
270+
return fmt.Errorf("certificate with CN %s is expired", c.Subject.CommonName)
271+
}
272+
// If this certificate is expiring within 6 months, put out a warning
273+
if (c.NotAfter).Before(time.Now().AddDate(0, 6, 0)) {
274+
logrus.Warn("certificate with CN %s is near expiry", c.Subject.CommonName)
271275
}
272276
// If we have an RSA key, make sure it's long enough
273277
if c.PublicKeyAlgorithm == x509.RSA {

0 commit comments

Comments
 (0)