From: Roland Shoemaker Date: Tue, 19 Apr 2022 19:05:10 +0000 (-0700) Subject: crypto/x509: use SAN when comparing certs during path building X-Git-Tag: go1.19beta1~610 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=b941a10e384e6772792bc9e05e7ebe58e98bc20e;p=gostls13.git crypto/x509: use SAN when comparing certs during path building Per RFC 4158 Section 2.4.2, when we are discarding candidate certificates during path building, use the SANs as well as subject and public key when checking whether a certificate is already present in the built path. This supports the case where a certificate in the chain (typically a leaf) has the exact same subject and public key as another certificate in the chain (typically its parent) but has SANs which don't match. Change-Id: I212c234e94a1f6afbe9691e4a3ba257461db3a7e Reviewed-on: https://go-review.googlesource.com/c/go/+/401115 Reviewed-by: Roland Shoemaker Run-TryBot: Roland Shoemaker TryBot-Result: Gopher Robot Reviewed-by: Damien Neil --- diff --git a/src/crypto/x509/verify.go b/src/crypto/x509/verify.go index 77ad6868fa..a739956cfe 100644 --- a/src/crypto/x509/verify.go +++ b/src/crypto/x509/verify.go @@ -7,6 +7,7 @@ package x509 import ( "bytes" "crypto" + "crypto/x509/pkix" "errors" "fmt" "net" @@ -825,6 +826,50 @@ func appendToFreshChain(chain []*Certificate, cert *Certificate) []*Certificate return n } +// alreadyInChain checks whether a candidate certificate is present in a chain. +// Rather than doing a direct byte for byte equivalency check, we check if the +// subject, public key, and SAN, if present, are equal. This prevents loops that +// are created by mutual cross-signatures, or other cross-signature bridge +// oddities. +func alreadyInChain(candidate *Certificate, chain []*Certificate) bool { + type pubKeyEqual interface { + Equal(crypto.PublicKey) bool + } + + var candidateSAN *pkix.Extension + for _, ext := range candidate.Extensions { + if ext.Id.Equal(oidExtensionSubjectAltName) { + candidateSAN = &ext + break + } + } + + for _, cert := range chain { + if !bytes.Equal(candidate.RawSubject, cert.RawSubject) { + continue + } + if !candidate.PublicKey.(pubKeyEqual).Equal(cert.PublicKey) { + continue + } + var certSAN *pkix.Extension + for _, ext := range cert.Extensions { + if ext.Id.Equal(oidExtensionSubjectAltName) { + certSAN = &ext + break + } + } + if candidateSAN == nil && certSAN == nil { + return true + } else if candidateSAN == nil || certSAN == nil { + return false + } + if bytes.Equal(candidateSAN.Value, certSAN.Value) { + return true + } + } + return false +} + // maxChainSignatureChecks is the maximum number of CheckSignatureFrom calls // that an invocation of buildChains will (transitively) make. Most chains are // less than 15 certificates long, so this leaves space for multiple chains and @@ -837,18 +882,9 @@ func (c *Certificate) buildChains(currentChain []*Certificate, sigChecks *int, o hintCert *Certificate ) - type pubKeyEqual interface { - Equal(crypto.PublicKey) bool - } - considerCandidate := func(certType int, candidate *Certificate) { - for _, cert := range currentChain { - // If a certificate already appeared in the chain we've built, don't - // reconsider it. This prevents loops, for isntance those created by - // mutual cross-signatures, or other cross-signature bridges oddities. - if bytes.Equal(cert.RawSubject, candidate.RawSubject) && cert.PublicKey.(pubKeyEqual).Equal(candidate.PublicKey) { - return - } + if alreadyInChain(candidate, currentChain) { + return } if sigChecks == nil { diff --git a/src/crypto/x509/verify_test.go b/src/crypto/x509/verify_test.go index 1b2cbe34dd..8a7b08ab58 100644 --- a/src/crypto/x509/verify_test.go +++ b/src/crypto/x509/verify_test.go @@ -2340,6 +2340,29 @@ func TestPathBuilding(t *testing.T) { "CN=leaf -> CN=inter b -> CN=inter c -> CN=root", }, }, + { + // Build a simple two node graph, where the leaf is directly issued from + // the root and both certificates have matching subject and public key, but + // the leaf has SANs. + name: "leaf with same subject, key, as parent but with SAN", + graph: trustGraphDescription{ + Roots: []string{"root"}, + Leaf: "root", + Graph: []trustGraphEdge{ + { + Issuer: "root", + Subject: "root", + Type: leafCertificate, + MutateTemplate: func(c *Certificate) { + c.DNSNames = []string{"localhost"} + }, + }, + }, + }, + expectedChains: []string{ + "CN=root -> CN=root", + }, + }, } for _, tc := range tests {