Skip to content

Commit 7701306

Browse files
FiloSottiledmitshur
authored andcommitted
crypto/x509: limit number of signature checks for each verification
That number grows quadratically with the number of intermediate certificates in certain pathological cases (for example if they all have the same Subject) leading to a CPU DoS. Set a fixed budget that should fit all real world chains, given we only look at intermediates provided by the peer. The algorithm can be improved, but that's left for follow-up CLs: * the cache logic should be reviewed for correctness, as it seems to override the entire chain with the cached one * the equality check should compare Subject and public key, not the whole certificate * certificates with the right SKID but the wrong Subject should not be considered, and in particular should not take priority over certificates with the right Subject Fixes golang#29233 Change-Id: Ib257c12cd5563df7723f9c81231d82b882854213 Reviewed-on: https://team-review.git.corp.google.com/c/370475 Reviewed-by: Andrew Bonventre <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/154105 Reviewed-by: Filippo Valsorda <[email protected]> Run-TryBot: Filippo Valsorda <[email protected]>
1 parent 9c075b7 commit 7701306

File tree

3 files changed

+176
-57
lines changed

3 files changed

+176
-57
lines changed

src/crypto/x509/cert_pool.go

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -65,32 +65,16 @@ func SystemCertPool() (*CertPool, error) {
6565
return loadSystemRoots()
6666
}
6767

68-
// findVerifiedParents attempts to find certificates in s which have signed the
69-
// given certificate. If any candidates were rejected then errCert will be set
70-
// to one of them, arbitrarily, and err will contain the reason that it was
71-
// rejected.
72-
func (s *CertPool) findVerifiedParents(cert *Certificate) (parents []int, errCert *Certificate, err error) {
68+
// findPotentialParents returns the indexes of certificates in s which might
69+
// have signed cert. The caller must not modify the returned slice.
70+
func (s *CertPool) findPotentialParents(cert *Certificate) []int {
7371
if s == nil {
74-
return
72+
return nil
7573
}
76-
var candidates []int
77-
7874
if len(cert.AuthorityKeyId) > 0 {
79-
candidates = s.bySubjectKeyId[string(cert.AuthorityKeyId)]
80-
}
81-
if len(candidates) == 0 {
82-
candidates = s.byName[string(cert.RawIssuer)]
75+
return s.bySubjectKeyId[string(cert.AuthorityKeyId)]
8376
}
84-
85-
for _, c := range candidates {
86-
if err = cert.CheckSignatureFrom(s.certs[c]); err == nil {
87-
parents = append(parents, c)
88-
} else {
89-
errCert = s.certs[c]
90-
}
91-
}
92-
93-
return
77+
return s.byName[string(cert.RawIssuer)]
9478
}
9579

9680
func (s *CertPool) contains(cert *Certificate) bool {

src/crypto/x509/verify.go

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err e
763763
if opts.Roots.contains(c) {
764764
candidateChains = append(candidateChains, []*Certificate{c})
765765
} else {
766-
if candidateChains, err = c.buildChains(make(map[int][][]*Certificate), []*Certificate{c}, &opts); err != nil {
766+
if candidateChains, err = c.buildChains(nil, []*Certificate{c}, nil, &opts); err != nil {
767767
return nil, err
768768
}
769769
}
@@ -800,58 +800,74 @@ func appendToFreshChain(chain []*Certificate, cert *Certificate) []*Certificate
800800
return n
801801
}
802802

803-
func (c *Certificate) buildChains(cache map[int][][]*Certificate, currentChain []*Certificate, opts *VerifyOptions) (chains [][]*Certificate, err error) {
804-
possibleRoots, failedRoot, rootErr := opts.Roots.findVerifiedParents(c)
805-
nextRoot:
806-
for _, rootNum := range possibleRoots {
807-
root := opts.Roots.certs[rootNum]
803+
// maxChainSignatureChecks is the maximum number of CheckSignatureFrom calls
804+
// that an invocation of buildChains will (tranistively) make. Most chains are
805+
// less than 15 certificates long, so this leaves space for multiple chains and
806+
// for failed checks due to different intermediates having the same Subject.
807+
const maxChainSignatureChecks = 100
808808

809+
func (c *Certificate) buildChains(cache map[*Certificate][][]*Certificate, currentChain []*Certificate, sigChecks *int, opts *VerifyOptions) (chains [][]*Certificate, err error) {
810+
var (
811+
hintErr error
812+
hintCert *Certificate
813+
)
814+
815+
considerCandidate := func(certType int, candidate *Certificate) {
809816
for _, cert := range currentChain {
810-
if cert.Equal(root) {
811-
continue nextRoot
817+
if cert.Equal(candidate) {
818+
return
812819
}
813820
}
814821

815-
err = root.isValid(rootCertificate, currentChain, opts)
816-
if err != nil {
817-
continue
822+
if sigChecks == nil {
823+
sigChecks = new(int)
824+
}
825+
*sigChecks++
826+
if *sigChecks > maxChainSignatureChecks {
827+
err = errors.New("x509: signature check attempts limit reached while verifying certificate chain")
828+
return
818829
}
819-
chains = append(chains, appendToFreshChain(currentChain, root))
820-
}
821830

822-
possibleIntermediates, failedIntermediate, intermediateErr := opts.Intermediates.findVerifiedParents(c)
823-
nextIntermediate:
824-
for _, intermediateNum := range possibleIntermediates {
825-
intermediate := opts.Intermediates.certs[intermediateNum]
826-
for _, cert := range currentChain {
827-
if cert.Equal(intermediate) {
828-
continue nextIntermediate
831+
if err := c.CheckSignatureFrom(candidate); err != nil {
832+
if hintErr == nil {
833+
hintErr = err
834+
hintCert = candidate
829835
}
836+
return
830837
}
831-
err = intermediate.isValid(intermediateCertificate, currentChain, opts)
838+
839+
err = candidate.isValid(certType, currentChain, opts)
832840
if err != nil {
833-
continue
841+
return
834842
}
835-
var childChains [][]*Certificate
836-
childChains, ok := cache[intermediateNum]
837-
if !ok {
838-
childChains, err = intermediate.buildChains(cache, appendToFreshChain(currentChain, intermediate), opts)
839-
cache[intermediateNum] = childChains
843+
844+
switch certType {
845+
case rootCertificate:
846+
chains = append(chains, appendToFreshChain(currentChain, candidate))
847+
case intermediateCertificate:
848+
if cache == nil {
849+
cache = make(map[*Certificate][][]*Certificate)
850+
}
851+
childChains, ok := cache[candidate]
852+
if !ok {
853+
childChains, err = candidate.buildChains(cache, appendToFreshChain(currentChain, candidate), sigChecks, opts)
854+
cache[candidate] = childChains
855+
}
856+
chains = append(chains, childChains...)
840857
}
841-
chains = append(chains, childChains...)
858+
}
859+
860+
for _, rootNum := range opts.Roots.findPotentialParents(c) {
861+
considerCandidate(rootCertificate, opts.Roots.certs[rootNum])
862+
}
863+
for _, intermediateNum := range opts.Intermediates.findPotentialParents(c) {
864+
considerCandidate(intermediateCertificate, opts.Intermediates.certs[intermediateNum])
842865
}
843866

844867
if len(chains) > 0 {
845868
err = nil
846869
}
847-
848870
if len(chains) == 0 && err == nil {
849-
hintErr := rootErr
850-
hintCert := failedRoot
851-
if hintErr == nil {
852-
hintErr = intermediateErr
853-
hintCert = failedIntermediate
854-
}
855871
err = UnknownAuthorityError{c, hintErr, hintCert}
856872
}
857873

src/crypto/x509/verify_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
package x509
66

77
import (
8+
"crypto"
9+
"crypto/ecdsa"
10+
"crypto/elliptic"
11+
"crypto/rand"
812
"crypto/x509/pkix"
913
"encoding/pem"
1014
"errors"
1115
"fmt"
16+
"math/big"
1217
"runtime"
1318
"strings"
1419
"testing"
@@ -1889,3 +1894,117 @@ func TestValidHostname(t *testing.T) {
18891894
}
18901895
}
18911896
}
1897+
1898+
func generateCert(cn string, isCA bool, issuer *Certificate, issuerKey crypto.PrivateKey) (*Certificate, crypto.PrivateKey, error) {
1899+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
1900+
if err != nil {
1901+
return nil, nil, err
1902+
}
1903+
1904+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
1905+
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
1906+
1907+
template := &Certificate{
1908+
SerialNumber: serialNumber,
1909+
Subject: pkix.Name{CommonName: cn},
1910+
NotBefore: time.Now().Add(-1 * time.Hour),
1911+
NotAfter: time.Now().Add(24 * time.Hour),
1912+
1913+
KeyUsage: KeyUsageKeyEncipherment | KeyUsageDigitalSignature | KeyUsageCertSign,
1914+
ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
1915+
BasicConstraintsValid: true,
1916+
IsCA: isCA,
1917+
}
1918+
if issuer == nil {
1919+
issuer = template
1920+
issuerKey = priv
1921+
}
1922+
1923+
derBytes, err := CreateCertificate(rand.Reader, template, issuer, priv.Public(), issuerKey)
1924+
if err != nil {
1925+
return nil, nil, err
1926+
}
1927+
cert, err := ParseCertificate(derBytes)
1928+
if err != nil {
1929+
return nil, nil, err
1930+
}
1931+
1932+
return cert, priv, nil
1933+
}
1934+
1935+
func TestPathologicalChain(t *testing.T) {
1936+
if testing.Short() {
1937+
t.Skip("skipping generation of a long chain of certificates in short mode")
1938+
}
1939+
1940+
// Build a chain where all intermediates share the same subject, to hit the
1941+
// path building worst behavior.
1942+
roots, intermediates := NewCertPool(), NewCertPool()
1943+
1944+
parent, parentKey, err := generateCert("Root CA", true, nil, nil)
1945+
if err != nil {
1946+
t.Fatal(err)
1947+
}
1948+
roots.AddCert(parent)
1949+
1950+
for i := 1; i < 100; i++ {
1951+
parent, parentKey, err = generateCert("Intermediate CA", true, parent, parentKey)
1952+
if err != nil {
1953+
t.Fatal(err)
1954+
}
1955+
intermediates.AddCert(parent)
1956+
}
1957+
1958+
leaf, _, err := generateCert("Leaf", false, parent, parentKey)
1959+
if err != nil {
1960+
t.Fatal(err)
1961+
}
1962+
1963+
start := time.Now()
1964+
_, err = leaf.Verify(VerifyOptions{
1965+
Roots: roots,
1966+
Intermediates: intermediates,
1967+
})
1968+
t.Logf("verification took %v", time.Since(start))
1969+
1970+
if err == nil || !strings.Contains(err.Error(), "signature check attempts limit") {
1971+
t.Errorf("expected verification to fail with a signature checks limit error; got %v", err)
1972+
}
1973+
}
1974+
1975+
func TestLongChain(t *testing.T) {
1976+
if testing.Short() {
1977+
t.Skip("skipping generation of a long chain of certificates in short mode")
1978+
}
1979+
1980+
roots, intermediates := NewCertPool(), NewCertPool()
1981+
1982+
parent, parentKey, err := generateCert("Root CA", true, nil, nil)
1983+
if err != nil {
1984+
t.Fatal(err)
1985+
}
1986+
roots.AddCert(parent)
1987+
1988+
for i := 1; i < 15; i++ {
1989+
name := fmt.Sprintf("Intermediate CA #%d", i)
1990+
parent, parentKey, err = generateCert(name, true, parent, parentKey)
1991+
if err != nil {
1992+
t.Fatal(err)
1993+
}
1994+
intermediates.AddCert(parent)
1995+
}
1996+
1997+
leaf, _, err := generateCert("Leaf", false, parent, parentKey)
1998+
if err != nil {
1999+
t.Fatal(err)
2000+
}
2001+
2002+
start := time.Now()
2003+
if _, err := leaf.Verify(VerifyOptions{
2004+
Roots: roots,
2005+
Intermediates: intermediates,
2006+
}); err != nil {
2007+
t.Error(err)
2008+
}
2009+
t.Logf("verification took %v", time.Since(start))
2010+
}

0 commit comments

Comments
 (0)