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

Commit dba9c36

Browse files
authored
Merge pull request #802 from docker/cert-warnings-int-expiry
Output warnings for cert within 6 months expiry, check intermediate cert expiry
2 parents 07de505 + fb03348 commit dba9c36

File tree

9 files changed

+476
-26
lines changed

9 files changed

+476
-26
lines changed

trustpinning/ca.crt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
-----BEGIN CERTIFICATE-----
2+
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
35+
lKmXlK4dTg==
36+
-----END CERTIFICATE-----
37+

trustpinning/certs.go

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
"fmt"
77
"strings"
8-
"time"
98

109
"github.com/Sirupsen/logrus"
1110
"github.com/docker/notary/tuf/data"
@@ -98,6 +97,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus
9897
// Retrieve all the leaf and intermediate certificates in root for which the CN matches the GUN
9998
allLeafCerts, allIntCerts := parseAllCerts(signedRoot)
10099
certsFromRoot, err := validRootLeafCerts(allLeafCerts, gun, true)
100+
validIntCerts := validRootIntCerts(allIntCerts)
101101

102102
if err != nil {
103103
logrus.Debugf("error retrieving valid leaf certificates for: %s, %v", gun, err)
@@ -140,7 +140,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus
140140
validPinnedCerts := map[string]*x509.Certificate{}
141141
for id, cert := range certsFromRoot {
142142
logrus.Debugf("checking trust-pinning for cert: %s", id)
143-
if ok := trustPinCheckFunc(cert, allIntCerts[id]); !ok {
143+
if ok := trustPinCheckFunc(cert, validIntCerts[id]); !ok {
144144
logrus.Debugf("trust-pinning check failed for cert: %s", id)
145145
continue
146146
}
@@ -156,7 +156,7 @@ func ValidateRoot(prevRoot *data.SignedRoot, root *data.Signed, gun string, trus
156156
// Note that certsFromRoot is guaranteed to be unchanged only if we had prior cert data for this GUN or enabled TOFUS
157157
// If we attempted to pin a certain certificate or CA, certsFromRoot could have been pruned accordingly
158158
err = signed.VerifySignatures(root, data.BaseRole{
159-
Keys: utils.CertsToKeys(certsFromRoot, allIntCerts), Threshold: rootRole.Threshold})
159+
Keys: utils.CertsToKeys(certsFromRoot, validIntCerts), Threshold: rootRole.Threshold})
160160
if err != nil {
161161
logrus.Debugf("failed to verify TUF data for: %s, %v", gun, err)
162162
return nil, &ErrValidationFail{Reason: "failed to validate integrity of roots"}
@@ -181,17 +181,8 @@ func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, c
181181
continue
182182
}
183183
// 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
187-
}
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)")
184+
// and warn if it hasn't expired yet but is within 6 months of expiry
185+
if err := utils.ValidateCertificate(cert, checkExpiry); err != nil {
195186
continue
196187
}
197188

@@ -208,6 +199,24 @@ func validRootLeafCerts(allLeafCerts map[string]*x509.Certificate, gun string, c
208199
return validLeafCerts, nil
209200
}
210201

202+
// validRootIntCerts filters the passed in structure of intermediate certificates to only include non-expired, non-sha1 certificates
203+
// Note that this "validity" alone does not imply any measure of trust.
204+
func validRootIntCerts(allIntCerts map[string][]*x509.Certificate) map[string][]*x509.Certificate {
205+
validIntCerts := make(map[string][]*x509.Certificate)
206+
207+
// Go through every leaf cert ID, and build its valid intermediate certificate list
208+
for leafID, intCertList := range allIntCerts {
209+
for _, intCert := range intCertList {
210+
if err := utils.ValidateCertificate(intCert, true); err != nil {
211+
continue
212+
}
213+
validIntCerts[leafID] = append(validIntCerts[leafID], intCert)
214+
}
215+
216+
}
217+
return validIntCerts
218+
}
219+
211220
// parseAllCerts returns two maps, one with all of the leafCertificates and one
212221
// with all the intermediate certificates found in signedRoot
213222
func parseAllCerts(signedRoot *data.SignedRoot) (map[string]*x509.Certificate, map[string][]*x509.Certificate) {

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"
@@ -786,9 +788,9 @@ func testValidateRootRotationMissingNewSig(t *testing.T, keyAlg, rootKeyType str
786788
require.Error(t, err, "insuficient signatures on root")
787789
}
788790

789-
func generateTestingCertificate(rootKey data.PrivateKey, gun string) (*x509.Certificate, error) {
791+
func generateTestingCertificate(rootKey data.PrivateKey, gun string, timeToExpire time.Duration) (*x509.Certificate, error) {
790792
startTime := time.Now()
791-
return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.AddDate(10, 0, 0))
793+
return cryptoservice.GenerateCertificate(rootKey, gun, startTime, startTime.Add(timeToExpire))
792794
}
793795

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

0 commit comments

Comments
 (0)