Skip to content

Commit 3544082

Browse files
rolandshoemakerFiloSottile
authored andcommitted
crypto/x509: verification with system and custom roots
Make system cert pools special, such that when one has extra roots added to it we run verifications twice, once using the platform verifier, if available, and once using the Go verifier, merging the results. This change re-enables SystemCertPool on Windows, but explicitly does not return anything from CertPool.Subjects (which matches the behavior of macOS). CertPool.Subjects is also marked deprecated. Fixes #46287 Fixes #16736 Change-Id: Idc1843f715ae2b2d0108e55ab942c287181a340a Reviewed-on: https://go-review.googlesource.com/c/go/+/353589 Reviewed-by: Filippo Valsorda <[email protected]> Trust: Roland Shoemaker <[email protected]>
1 parent 4f083c7 commit 3544082

File tree

6 files changed

+229
-62
lines changed

6 files changed

+229
-62
lines changed

src/crypto/x509/cert_pool.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import (
88
"bytes"
99
"crypto/sha256"
1010
"encoding/pem"
11-
"errors"
12-
"runtime"
1311
"sync"
1412
)
1513

@@ -29,6 +27,12 @@ type CertPool struct {
2927
// call getCert and otherwise negate savings from lazy getCert
3028
// funcs).
3129
haveSum map[sum224]bool
30+
31+
// systemPool indicates whether this is a special pool derived from the
32+
// system roots. If it includes additional roots, it requires doing two
33+
// verifications, one using the roots provided by the caller, and one using
34+
// the system platform verifier.
35+
systemPool bool
3236
}
3337

3438
// lazyCert is minimal metadata about a Cert and a func to retrieve it
@@ -75,9 +79,10 @@ func (s *CertPool) cert(n int) (*Certificate, error) {
7579

7680
func (s *CertPool) copy() *CertPool {
7781
p := &CertPool{
78-
byName: make(map[string][]int, len(s.byName)),
79-
lazyCerts: make([]lazyCert, len(s.lazyCerts)),
80-
haveSum: make(map[sum224]bool, len(s.haveSum)),
82+
byName: make(map[string][]int, len(s.byName)),
83+
lazyCerts: make([]lazyCert, len(s.lazyCerts)),
84+
haveSum: make(map[sum224]bool, len(s.haveSum)),
85+
systemPool: s.systemPool,
8186
}
8287
for k, v := range s.byName {
8388
indexes := make([]int, len(v))
@@ -103,15 +108,6 @@ func (s *CertPool) copy() *CertPool {
103108
//
104109
// New changes in the system cert pool might not be reflected in subsequent calls.
105110
func SystemCertPool() (*CertPool, error) {
106-
if runtime.GOOS == "windows" {
107-
// Issue 16736, 18609:
108-
return nil, errors.New("crypto/x509: system root pool is not available on Windows")
109-
} else if runtime.GOOS == "darwin" {
110-
return nil, errors.New("crypto/x509: system root pool is not available on macOS")
111-
} else if runtime.GOOS == "ios" {
112-
return nil, errors.New("crypto/x509: system root pool is not available on iOS")
113-
}
114-
115111
if sysRoots := systemRootsPool(); sysRoots != nil {
116112
return sysRoots.copy(), nil
117113
}
@@ -243,6 +239,9 @@ func (s *CertPool) AppendCertsFromPEM(pemCerts []byte) (ok bool) {
243239

244240
// Subjects returns a list of the DER-encoded subjects of
245241
// all of the certificates in the pool.
242+
//
243+
// Deprecated: if s was returned by SystemCertPool, Subjects
244+
// will not include the system roots.
246245
func (s *CertPool) Subjects() [][]byte {
247246
res := make([][]byte, s.len())
248247
for i, lc := range s.lazyCerts {

src/crypto/x509/hybrid_pool_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2011 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package x509_test
6+
7+
import (
8+
"crypto/ecdsa"
9+
"crypto/elliptic"
10+
"crypto/rand"
11+
"crypto/tls"
12+
"crypto/x509"
13+
"crypto/x509/pkix"
14+
"internal/testenv"
15+
"math/big"
16+
"runtime"
17+
"testing"
18+
"time"
19+
)
20+
21+
func TestHybridPool(t *testing.T) {
22+
if !(runtime.GOOS == "windows" || runtime.GOOS == "darwin" || runtime.GOOS == "ios") {
23+
t.Skipf("platform verifier not available on %s", runtime.GOOS)
24+
}
25+
if !testenv.HasExternalNetwork() {
26+
t.Skip()
27+
}
28+
29+
// Get the google.com chain, which should be valid on all platforms we
30+
// are testing
31+
c, err := tls.Dial("tcp", "google.com:443", &tls.Config{InsecureSkipVerify: true})
32+
if err != nil {
33+
t.Fatalf("tls connection failed: %s", err)
34+
}
35+
googChain := c.ConnectionState().PeerCertificates
36+
37+
rootTmpl := &x509.Certificate{
38+
SerialNumber: big.NewInt(1),
39+
Subject: pkix.Name{CommonName: "Go test root"},
40+
IsCA: true,
41+
BasicConstraintsValid: true,
42+
NotBefore: time.Now().Add(-time.Hour),
43+
NotAfter: time.Now().Add(time.Hour * 10),
44+
}
45+
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
46+
if err != nil {
47+
t.Fatalf("failed to generate test key: %s", err)
48+
}
49+
rootDER, err := x509.CreateCertificate(rand.Reader, rootTmpl, rootTmpl, k.Public(), k)
50+
if err != nil {
51+
t.Fatalf("failed to create test cert: %s", err)
52+
}
53+
root, err := x509.ParseCertificate(rootDER)
54+
if err != nil {
55+
t.Fatalf("failed to parse test cert: %s", err)
56+
}
57+
58+
pool, err := x509.SystemCertPool()
59+
if err != nil {
60+
t.Fatalf("SystemCertPool failed: %s", err)
61+
}
62+
opts := x509.VerifyOptions{Roots: pool}
63+
64+
_, err = googChain[0].Verify(opts)
65+
if err != nil {
66+
t.Fatalf("verification failed for google.com chain (empty pool): %s", err)
67+
}
68+
69+
pool.AddCert(root)
70+
71+
_, err = googChain[0].Verify(opts)
72+
if err != nil {
73+
t.Fatalf("verification failed for google.com chain (hybrid pool): %s", err)
74+
}
75+
76+
certTmpl := &x509.Certificate{
77+
SerialNumber: big.NewInt(1),
78+
NotBefore: time.Now().Add(-time.Hour),
79+
NotAfter: time.Now().Add(time.Hour * 10),
80+
DNSNames: []string{"example.com"},
81+
}
82+
certDER, err := x509.CreateCertificate(rand.Reader, certTmpl, rootTmpl, k.Public(), k)
83+
if err != nil {
84+
t.Fatalf("failed to create test cert: %s", err)
85+
}
86+
cert, err := x509.ParseCertificate(certDER)
87+
if err != nil {
88+
t.Fatalf("failed to parse test cert: %s", err)
89+
}
90+
91+
_, err = cert.Verify(opts)
92+
if err != nil {
93+
t.Fatalf("verification failed for custom chain (hybrid pool): %s", err)
94+
}
95+
}

src/crypto/x509/root_darwin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,5 @@ func exportCertificate(cert macOS.CFRef) (*Certificate, error) {
107107
}
108108

109109
func loadSystemRoots() (*CertPool, error) {
110-
return nil, nil
110+
return &CertPool{systemPool: true}, nil
111111
}

src/crypto/x509/root_windows.go

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import (
1010
"unsafe"
1111
)
1212

13+
func loadSystemRoots() (*CertPool, error) {
14+
return &CertPool{systemPool: true}, nil
15+
}
16+
1317
// Creates a new *syscall.CertContext representing the leaf certificate in an in-memory
1418
// certificate store containing itself and all of the intermediate certificates specified
1519
// in the opts.Intermediates CertPool.
@@ -271,47 +275,3 @@ func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate
271275

272276
return chains, nil
273277
}
274-
275-
func loadSystemRoots() (*CertPool, error) {
276-
// TODO: restore this functionality on Windows. We tried to do
277-
// it in Go 1.8 but had to revert it. See Issue 18609.
278-
// Returning (nil, nil) was the old behavior, prior to CL 30578.
279-
// The if statement here avoids vet complaining about
280-
// unreachable code below.
281-
if true {
282-
return nil, nil
283-
}
284-
285-
const CRYPT_E_NOT_FOUND = 0x80092004
286-
287-
store, err := syscall.CertOpenSystemStore(0, syscall.StringToUTF16Ptr("ROOT"))
288-
if err != nil {
289-
return nil, err
290-
}
291-
defer syscall.CertCloseStore(store, 0)
292-
293-
roots := NewCertPool()
294-
var cert *syscall.CertContext
295-
for {
296-
cert, err = syscall.CertEnumCertificatesInStore(store, cert)
297-
if err != nil {
298-
if errno, ok := err.(syscall.Errno); ok {
299-
if errno == CRYPT_E_NOT_FOUND {
300-
break
301-
}
302-
}
303-
return nil, err
304-
}
305-
if cert == nil {
306-
break
307-
}
308-
// Copy the buf, since ParseCertificate does not create its own copy.
309-
buf := (*[1 << 20]byte)(unsafe.Pointer(cert.EncodedCert))[:cert.Length:cert.Length]
310-
buf2 := make([]byte, cert.Length)
311-
copy(buf2, buf)
312-
if c, err := ParseCertificate(buf2); err == nil {
313-
roots.AddCert(c)
314-
}
315-
}
316-
return roots, nil
317-
}

src/crypto/x509/root_windows_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package x509_test
6+
7+
import (
8+
"crypto/tls"
9+
"crypto/x509"
10+
"internal/testenv"
11+
"testing"
12+
"time"
13+
)
14+
15+
func TestPlatformVerifier(t *testing.T) {
16+
if !testenv.HasExternalNetwork() {
17+
t.Skip()
18+
}
19+
20+
getChain := func(host string) []*x509.Certificate {
21+
t.Helper()
22+
c, err := tls.Dial("tcp", host+":443", &tls.Config{InsecureSkipVerify: true})
23+
if err != nil {
24+
t.Fatalf("tls connection failed: %s", err)
25+
}
26+
return c.ConnectionState().PeerCertificates
27+
}
28+
29+
tests := []struct {
30+
name string
31+
host string
32+
verifyName string
33+
verifyTime time.Time
34+
expectedErr string
35+
}{
36+
{
37+
// whatever google.com serves should, hopefully, be trusted
38+
name: "valid chain",
39+
host: "google.com",
40+
},
41+
{
42+
name: "expired leaf",
43+
host: "expired.badssl.com",
44+
expectedErr: "x509: certificate has expired or is not yet valid: ",
45+
},
46+
{
47+
name: "wrong host for leaf",
48+
host: "wrong.host.badssl.com",
49+
verifyName: "wrong.host.badssl.com",
50+
expectedErr: "x509: certificate is valid for *.badssl.com, badssl.com, not wrong.host.badssl.com",
51+
},
52+
{
53+
name: "self-signed leaf",
54+
host: "self-signed.badssl.com",
55+
expectedErr: "x509: certificate signed by unknown authority",
56+
},
57+
{
58+
name: "untrusted root",
59+
host: "untrusted-root.badssl.com",
60+
expectedErr: "x509: certificate signed by unknown authority",
61+
},
62+
{
63+
name: "expired leaf (custom time)",
64+
host: "google.com",
65+
verifyTime: time.Time{}.Add(time.Hour),
66+
expectedErr: "x509: certificate has expired or is not yet valid: ",
67+
},
68+
{
69+
name: "valid chain (custom time)",
70+
host: "google.com",
71+
verifyTime: time.Now(),
72+
},
73+
}
74+
75+
for _, tc := range tests {
76+
t.Run(tc.name, func(t *testing.T) {
77+
chain := getChain(tc.host)
78+
var opts x509.VerifyOptions
79+
if len(chain) > 1 {
80+
opts.Intermediates = x509.NewCertPool()
81+
for _, c := range chain[1:] {
82+
opts.Intermediates.AddCert(c)
83+
}
84+
}
85+
if tc.verifyName != "" {
86+
opts.DNSName = tc.verifyName
87+
}
88+
if !tc.verifyTime.IsZero() {
89+
opts.CurrentTime = tc.verifyTime
90+
}
91+
92+
_, err := chain[0].Verify(opts)
93+
if err != nil && tc.expectedErr == "" {
94+
t.Errorf("unexpected verification error: %s", err)
95+
} else if err != nil && err.Error() != tc.expectedErr {
96+
t.Errorf("unexpected verification error: got %q, want %q", err.Error(), tc.expectedErr)
97+
} else if err == nil && tc.expectedErr != "" {
98+
t.Errorf("unexpected verification success: want %q", tc.expectedErr)
99+
}
100+
})
101+
}
102+
}

src/crypto/x509/verify.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -741,9 +741,20 @@ func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err e
741741
}
742742
}
743743

744-
// Use platform verifiers, where available
745-
if opts.Roots == nil && (runtime.GOOS == "windows" || runtime.GOOS == "darwin" || runtime.GOOS == "ios") {
746-
return c.systemVerify(&opts)
744+
// Use platform verifiers, where available, if Roots is from SystemCertPool.
745+
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
746+
if opts.Roots == nil {
747+
return c.systemVerify(&opts)
748+
}
749+
if opts.Roots != nil && opts.Roots.systemPool {
750+
platformChains, err := c.systemVerify(&opts)
751+
// If the platform verifier succeeded, or there are no additional
752+
// roots, return the platform verifier result. Otherwise, continue
753+
// with the Go verifier.
754+
if err == nil || opts.Roots.len() == 0 {
755+
return platformChains, err
756+
}
757+
}
747758
}
748759

749760
if opts.Roots == nil {

0 commit comments

Comments
 (0)