Skip to content

Commit 0a4a37f

Browse files
committed
[release-branch.go1.10-security] 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 Change-Id: Ib257c12cd5563df7723f9c81231d82b882854213 Reviewed-on: https://team-review.git.corp.google.com/c/370475 Reviewed-by: Andrew Bonventre <[email protected]> (cherry picked from commit 09d57361bc99cbbfb9755ee30ddcb42ff5a9d7d6) Reviewed-on: https://team-review.git.corp.google.com/c/372923 Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent 1ae7397 commit 0a4a37f

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
@@ -38,32 +38,16 @@ func SystemCertPool() (*CertPool, error) {
3838
return loadSystemRoots()
3939
}
4040

41-
// findVerifiedParents attempts to find certificates in s which have signed the
42-
// given certificate. If any candidates were rejected then errCert will be set
43-
// to one of them, arbitrarily, and err will contain the reason that it was
44-
// rejected.
45-
func (s *CertPool) findVerifiedParents(cert *Certificate) (parents []int, errCert *Certificate, err error) {
41+
// findPotentialParents returns the indexes of certificates in s which might
42+
// have signed cert. The caller must not modify the returned slice.
43+
func (s *CertPool) findPotentialParents(cert *Certificate) []int {
4644
if s == nil {
47-
return
45+
return nil
4846
}
49-
var candidates []int
50-
5147
if len(cert.AuthorityKeyId) > 0 {
52-
candidates = s.bySubjectKeyId[string(cert.AuthorityKeyId)]
53-
}
54-
if len(candidates) == 0 {
55-
candidates = s.byName[string(cert.RawIssuer)]
48+
return s.bySubjectKeyId[string(cert.AuthorityKeyId)]
5649
}
57-
58-
for _, c := range candidates {
59-
if err = cert.CheckSignatureFrom(s.certs[c]); err == nil {
60-
parents = append(parents, c)
61-
} else {
62-
errCert = s.certs[c]
63-
}
64-
}
65-
66-
return
50+
return s.byName[string(cert.RawIssuer)]
6751
}
6852

6953
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
@@ -765,7 +765,7 @@ func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err e
765765
if opts.Roots.contains(c) {
766766
candidateChains = append(candidateChains, []*Certificate{c})
767767
} else {
768-
if candidateChains, err = c.buildChains(make(map[int][][]*Certificate), []*Certificate{c}, &opts); err != nil {
768+
if candidateChains, err = c.buildChains(nil, []*Certificate{c}, nil, &opts); err != nil {
769769
return nil, err
770770
}
771771
}
@@ -802,58 +802,74 @@ func appendToFreshChain(chain []*Certificate, cert *Certificate) []*Certificate
802802
return n
803803
}
804804

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

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

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

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

846869
if len(chains) > 0 {
847870
err = nil
848871
}
849-
850872
if len(chains) == 0 && err == nil {
851-
hintErr := rootErr
852-
hintCert := failedRoot
853-
if hintErr == nil {
854-
hintErr = intermediateErr
855-
hintCert = failedIntermediate
856-
}
857873
err = UnknownAuthorityError{c, hintErr, hintCert}
858874
}
859875

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"
@@ -1707,3 +1712,117 @@ UNhY4JhezH9gQYqvDMWrWDAbBgNVHSMEFDASgBArF29S5Bnqw7de8GzGA1nfMAoG
17071712
CCqGSM49BAMCA0gAMEUCIQClA3d4tdrDu9Eb5ZBpgyC+fU1xTZB0dKQHz6M5fPZA
17081713
2AIgN96lM+CPGicwhN24uQI6flOsO3H0TJ5lNzBYLtnQtlc=
17091714
-----END CERTIFICATE-----`
1715+
1716+
func generateCert(cn string, isCA bool, issuer *Certificate, issuerKey crypto.PrivateKey) (*Certificate, crypto.PrivateKey, error) {
1717+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
1718+
if err != nil {
1719+
return nil, nil, err
1720+
}
1721+
1722+
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
1723+
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
1724+
1725+
template := &Certificate{
1726+
SerialNumber: serialNumber,
1727+
Subject: pkix.Name{CommonName: cn},
1728+
NotBefore: time.Now().Add(-1 * time.Hour),
1729+
NotAfter: time.Now().Add(24 * time.Hour),
1730+
1731+
KeyUsage: KeyUsageKeyEncipherment | KeyUsageDigitalSignature | KeyUsageCertSign,
1732+
ExtKeyUsage: []ExtKeyUsage{ExtKeyUsageServerAuth},
1733+
BasicConstraintsValid: true,
1734+
IsCA: isCA,
1735+
}
1736+
if issuer == nil {
1737+
issuer = template
1738+
issuerKey = priv
1739+
}
1740+
1741+
derBytes, err := CreateCertificate(rand.Reader, template, issuer, priv.Public(), issuerKey)
1742+
if err != nil {
1743+
return nil, nil, err
1744+
}
1745+
cert, err := ParseCertificate(derBytes)
1746+
if err != nil {
1747+
return nil, nil, err
1748+
}
1749+
1750+
return cert, priv, nil
1751+
}
1752+
1753+
func TestPathologicalChain(t *testing.T) {
1754+
if testing.Short() {
1755+
t.Skip("skipping generation of a long chain of certificates in short mode")
1756+
}
1757+
1758+
// Build a chain where all intermediates share the same subject, to hit the
1759+
// path building worst behavior.
1760+
roots, intermediates := NewCertPool(), NewCertPool()
1761+
1762+
parent, parentKey, err := generateCert("Root CA", true, nil, nil)
1763+
if err != nil {
1764+
t.Fatal(err)
1765+
}
1766+
roots.AddCert(parent)
1767+
1768+
for i := 1; i < 100; i++ {
1769+
parent, parentKey, err = generateCert("Intermediate CA", true, parent, parentKey)
1770+
if err != nil {
1771+
t.Fatal(err)
1772+
}
1773+
intermediates.AddCert(parent)
1774+
}
1775+
1776+
leaf, _, err := generateCert("Leaf", false, parent, parentKey)
1777+
if err != nil {
1778+
t.Fatal(err)
1779+
}
1780+
1781+
start := time.Now()
1782+
_, err = leaf.Verify(VerifyOptions{
1783+
Roots: roots,
1784+
Intermediates: intermediates,
1785+
})
1786+
t.Logf("verification took %v", time.Since(start))
1787+
1788+
if err == nil || !strings.Contains(err.Error(), "signature check attempts limit") {
1789+
t.Errorf("expected verification to fail with a signature checks limit error; got %v", err)
1790+
}
1791+
}
1792+
1793+
func TestLongChain(t *testing.T) {
1794+
if testing.Short() {
1795+
t.Skip("skipping generation of a long chain of certificates in short mode")
1796+
}
1797+
1798+
roots, intermediates := NewCertPool(), NewCertPool()
1799+
1800+
parent, parentKey, err := generateCert("Root CA", true, nil, nil)
1801+
if err != nil {
1802+
t.Fatal(err)
1803+
}
1804+
roots.AddCert(parent)
1805+
1806+
for i := 1; i < 15; i++ {
1807+
name := fmt.Sprintf("Intermediate CA #%d", i)
1808+
parent, parentKey, err = generateCert(name, true, parent, parentKey)
1809+
if err != nil {
1810+
t.Fatal(err)
1811+
}
1812+
intermediates.AddCert(parent)
1813+
}
1814+
1815+
leaf, _, err := generateCert("Leaf", false, parent, parentKey)
1816+
if err != nil {
1817+
t.Fatal(err)
1818+
}
1819+
1820+
start := time.Now()
1821+
if _, err := leaf.Verify(VerifyOptions{
1822+
Roots: roots,
1823+
Intermediates: intermediates,
1824+
}); err != nil {
1825+
t.Error(err)
1826+
}
1827+
t.Logf("verification took %v", time.Since(start))
1828+
}

0 commit comments

Comments
 (0)