Skip to content

Commit af9c5e5

Browse files
crypto/x509: prioritize potential parents in chain building
When building a x509 chain the algorithm currently looks for parents that have a subject key identifier (SKID) that matches the child authority key identifier (AKID), if it is present, and returns all matches. If the child doesn't have an AKID, or there are no parents with matching SKID it will instead return all parents that have a subject DN matching the child's issuer DN. Prioritizing AKID/SKID matches over issuer/subject matches means that later in buildChains we have to throw away any pairs where these DNs do not match. This also prevents validation when a child has a SKID with two possible parents, one with matching AKID but mismatching subject DN, and one with a matching subject but missing AKID. In this case the former will be chosen and the latter ignored, meaning a valid chain cannot be built. This change alters how possible parents are chosen. Instead of doing a two step search it instead only consults the CertPool.byName subject DN map, avoiding issues where possible parents may be shadowed by parents that have SKID but bad subject DNs. Additionally it orders the list of possible parents by the likelihood that they are in fact a match. This ordering follows this pattern: * AKID and SKID match * AKID present, SKID missing / AKID missing, SKID present * AKID and SKID don't match In an ideal world this should save a handful of cycles when there are multiple possible matching parents by prioritizing parents that have the highest likelihood. This does diverge from past behavior in that it also means there are cases where _more_ parents will be considered than in the past. Another version of this change could just retain the past behavior, and only consider parents where both the subject and issuer DNs match, and if both parent and child have SKID and AKID also compare those, without any prioritization of the candidate parents. This change removes an existing test case as it assumes that the CertPool will return a possible candidate where the issuer/subject DNs do not match. Fixes #30079 Change-Id: I629f579cabb0b3d0c8cae5ad0429cc5a536b3e58 Reviewed-on: https://go-review.googlesource.com/c/go/+/232993 Trust: Roland Shoemaker <[email protected]> Run-TryBot: Roland Shoemaker <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Filippo Valsorda <[email protected]>
1 parent 567ef8b commit af9c5e5

File tree

2 files changed

+79
-41
lines changed

2 files changed

+79
-41
lines changed

src/crypto/x509/cert_pool.go

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,29 @@
55
package x509
66

77
import (
8+
"bytes"
89
"encoding/pem"
910
"errors"
1011
"runtime"
1112
)
1213

1314
// CertPool is a set of certificates.
1415
type CertPool struct {
15-
bySubjectKeyId map[string][]int
16-
byName map[string][]int
17-
certs []*Certificate
16+
byName map[string][]int
17+
certs []*Certificate
1818
}
1919

2020
// NewCertPool returns a new, empty CertPool.
2121
func NewCertPool() *CertPool {
2222
return &CertPool{
23-
bySubjectKeyId: make(map[string][]int),
24-
byName: make(map[string][]int),
23+
byName: make(map[string][]int),
2524
}
2625
}
2726

2827
func (s *CertPool) copy() *CertPool {
2928
p := &CertPool{
30-
bySubjectKeyId: make(map[string][]int, len(s.bySubjectKeyId)),
31-
byName: make(map[string][]int, len(s.byName)),
32-
certs: make([]*Certificate, len(s.certs)),
33-
}
34-
for k, v := range s.bySubjectKeyId {
35-
indexes := make([]int, len(v))
36-
copy(indexes, v)
37-
p.bySubjectKeyId[k] = indexes
29+
byName: make(map[string][]int, len(s.byName)),
30+
certs: make([]*Certificate, len(s.certs)),
3831
}
3932
for k, v := range s.byName {
4033
indexes := make([]int, len(v))
@@ -70,19 +63,42 @@ func SystemCertPool() (*CertPool, error) {
7063
}
7164

7265
// findPotentialParents returns the indexes of certificates in s which might
73-
// have signed cert. The caller must not modify the returned slice.
66+
// have signed cert.
7467
func (s *CertPool) findPotentialParents(cert *Certificate) []int {
7568
if s == nil {
7669
return nil
7770
}
7871

79-
var candidates []int
80-
if len(cert.AuthorityKeyId) > 0 {
81-
candidates = s.bySubjectKeyId[string(cert.AuthorityKeyId)]
72+
// consider all candidates where cert.Issuer matches cert.Subject.
73+
// when picking possible candidates the list is built in the order
74+
// of match plausibility as to save cycles in buildChains:
75+
// AKID and SKID match
76+
// AKID present, SKID missing / AKID missing, SKID present
77+
// AKID and SKID don't match
78+
var matchingKeyID, oneKeyID, mismatchKeyID []int
79+
for _, c := range s.byName[string(cert.RawIssuer)] {
80+
candidate := s.certs[c]
81+
kidMatch := bytes.Equal(candidate.SubjectKeyId, cert.AuthorityKeyId)
82+
switch {
83+
case kidMatch:
84+
matchingKeyID = append(matchingKeyID, c)
85+
case (len(candidate.SubjectKeyId) == 0 && len(cert.AuthorityKeyId) > 0) ||
86+
(len(candidate.SubjectKeyId) > 0 && len(cert.AuthorityKeyId) == 0):
87+
oneKeyID = append(oneKeyID, c)
88+
default:
89+
mismatchKeyID = append(mismatchKeyID, c)
90+
}
8291
}
83-
if len(candidates) == 0 {
84-
candidates = s.byName[string(cert.RawIssuer)]
92+
93+
found := len(matchingKeyID) + len(oneKeyID) + len(mismatchKeyID)
94+
if found == 0 {
95+
return nil
8596
}
97+
candidates := make([]int, 0, found)
98+
candidates = append(candidates, matchingKeyID...)
99+
candidates = append(candidates, oneKeyID...)
100+
candidates = append(candidates, mismatchKeyID...)
101+
86102
return candidates
87103
}
88104

@@ -115,10 +131,6 @@ func (s *CertPool) AddCert(cert *Certificate) {
115131
n := len(s.certs)
116132
s.certs = append(s.certs, cert)
117133

118-
if len(cert.SubjectKeyId) > 0 {
119-
keyId := string(cert.SubjectKeyId)
120-
s.bySubjectKeyId[keyId] = append(s.bySubjectKeyId[keyId], n)
121-
}
122134
name := string(cert.RawSubject)
123135
s.byName[name] = append(s.byName[name], n)
124136
}

src/crypto/x509/verify_test.go

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -284,18 +284,6 @@ var verifyTests = []verifyTest{
284284

285285
errorCallback: expectHostnameError("certificate is valid for"),
286286
},
287-
{
288-
// The issuer name in the leaf doesn't exactly match the
289-
// subject name in the root. Go does not perform
290-
// canonicalization and so should reject this. See issue 14955.
291-
name: "IssuerSubjectMismatch",
292-
leaf: issuerSubjectMatchLeaf,
293-
roots: []string{issuerSubjectMatchRoot},
294-
currentTime: 1475787715,
295-
systemSkip: true, // does not chain to a system root
296-
297-
errorCallback: expectSubjectIssuerMismatcthError,
298-
},
299287
{
300288
// An X.509 v1 certificate should not be accepted as an
301289
// intermediate.
@@ -430,6 +418,20 @@ var verifyTests = []verifyTest{
430418
{"Acme LLC", "Acme Co"},
431419
},
432420
},
421+
{
422+
// When there are two parents, one with a incorrect subject but matching SKID
423+
// and one with a correct subject but missing SKID, the latter should be
424+
// considered as a possible parent.
425+
leaf: leafMatchingAKIDMatchingIssuer,
426+
roots: []string{rootMatchingSKIDMismatchingSubject, rootMismatchingSKIDMatchingSubject},
427+
currentTime: 1550000000,
428+
dnsName: "example",
429+
systemSkip: true,
430+
431+
expectedChains: [][]string{
432+
{"Leaf", "Root B"},
433+
},
434+
},
433435
}
434436

435437
func expectHostnameError(msg string) func(*testing.T, error) {
@@ -474,12 +476,6 @@ func expectHashError(t *testing.T, err error) {
474476
}
475477
}
476478

477-
func expectSubjectIssuerMismatcthError(t *testing.T, err error) {
478-
if inval, ok := err.(CertificateInvalidError); !ok || inval.Reason != NameMismatch {
479-
t.Fatalf("error was not a NameMismatch: %v", err)
480-
}
481-
}
482-
483479
func expectNameConstraintsError(t *testing.T, err error) {
484480
if inval, ok := err.(CertificateInvalidError); !ok || inval.Reason != CANotAuthorizedForThisName {
485481
t.Fatalf("error was not a CANotAuthorizedForThisName: %v", err)
@@ -1615,6 +1611,36 @@ ssWvTAveakIwEgYDVR0RBAswCYIHZXhhbXBsZTAKBggqhkjOPQQDAgNHADBEAiBk
16151611
ZZMqeJS7JldLx91sPUArY5A=
16161612
-----END CERTIFICATE-----`
16171613

1614+
const rootMatchingSKIDMismatchingSubject = `-----BEGIN CERTIFICATE-----
1615+
MIIBQjCB6aADAgECAgEAMAoGCCqGSM49BAMCMBExDzANBgNVBAMTBlJvb3QgQTAe
1616+
Fw0wOTExMTAyMzAwMDBaFw0xOTExMDgyMzAwMDBaMBExDzANBgNVBAMTBlJvb3Qg
1617+
QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPK4p1uXq2aAeDtKDHIokg2rTcPM
1618+
2gq3N9Y96wiW6/7puBK1+INEW//cO9x6FpzkcsHw/TriAqy4sck/iDAvf9WjMjAw
1619+
MA8GA1UdJQQIMAYGBFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAMBgNVHQ4EBQQDAQID
1620+
MAoGCCqGSM49BAMCA0gAMEUCIQDgtAp7iVHxMnKxZPaLQPC+Tv2r7+DJc88k2SKH
1621+
MPs/wQIgFjjNvBoQEl7vSHTcRGCCcFMdlN4l0Dqc9YwGa9fyrQs=
1622+
-----END CERTIFICATE-----`
1623+
1624+
const rootMismatchingSKIDMatchingSubject = `-----BEGIN CERTIFICATE-----
1625+
MIIBNDCB26ADAgECAgEAMAoGCCqGSM49BAMCMBExDzANBgNVBAMTBlJvb3QgQjAe
1626+
Fw0wOTExMTAyMzAwMDBaFw0xOTExMDgyMzAwMDBaMBExDzANBgNVBAMTBlJvb3Qg
1627+
QjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI1YRFcIlkWzm9BdEVrIsEQJ2dT6
1628+
qiW8/WV9GoIhmDtX9SEDHospc0Cgm+TeD2QYW2iMrS5mvNe4GSw0Jezg/bOjJDAi
1629+
MA8GA1UdJQQIMAYGBFUdJQAwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNI
1630+
ADBFAiEAukWOiuellx8bugRiwCS5XQ6IOJ1SZcjuZxj76WojwxkCIHqa71qNw8FM
1631+
DtA5yoL9M2pDFF6ovFWnaCe+KlzSwAW/
1632+
-----END CERTIFICATE-----`
1633+
1634+
const leafMatchingAKIDMatchingIssuer = `-----BEGIN CERTIFICATE-----
1635+
MIIBNTCB26ADAgECAgEAMAoGCCqGSM49BAMCMBExDzANBgNVBAMTBlJvb3QgQjAe
1636+
Fw0wOTExMTAyMzAwMDBaFw0xOTExMDgyMzAwMDBaMA8xDTALBgNVBAMTBExlYWYw
1637+
WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASNWERXCJZFs5vQXRFayLBECdnU+qol
1638+
vP1lfRqCIZg7V/UhAx6LKXNAoJvk3g9kGFtojK0uZrzXuBksNCXs4P2zoyYwJDAO
1639+
BgNVHSMEBzAFgAMBAgMwEgYDVR0RBAswCYIHZXhhbXBsZTAKBggqhkjOPQQDAgNJ
1640+
ADBGAiEAnV9XV7a4h0nfJB8pWv+pBUXRlRFA2uZz3mXEpee8NYACIQCWa+wL70GL
1641+
ePBQCV1F9sE2q4ZrnsT9TZoNrSe/bMDjzA==
1642+
-----END CERTIFICATE-----`
1643+
16181644
var unknownAuthorityErrorTests = []struct {
16191645
cert string
16201646
expected string

0 commit comments

Comments
 (0)