Skip to content

Commit 7e5b2e0

Browse files
committed
crypto/x509: read Darwin trust settings for root CAs
Darwin separately stores bits indicating whether a root certificate should be trusted; this changes Go to read and use those when initializing SystemCertPool. Unfortunately, the trust API is very slow. To avoid a delay of up to 0.5s in initializing the system cert pool, we assume that the trust settings found in kSecTrustSettingsDomainSystem will always indicate trust. (That is, all root certs Apple distributes are trusted.) This is not guaranteed by the API but is true in practice. In the non-cgo codepath, we do not have that benefit, so we must check the trust status of every certificate. This causes about 0.5s of delay in initializing the SystemCertPool. On OS X 10.11 and older, the "security" command requires a certificate to be provided in a file and not on stdin, so the non-cgo codepath creates temporary files for each certificate, further slowing initialization. Updates #18141. Change-Id: If681c514047afe5e1a68de6c9d40ceabbce54755 Reviewed-on: https://go-review.googlesource.com/33721 Run-TryBot: Quentin Smith <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 9fbfe7c commit 7e5b2e0

File tree

3 files changed

+185
-11
lines changed

3 files changed

+185
-11
lines changed

src/crypto/x509/root_cgo_darwin.go

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,11 @@ int useOldCode() {
7373
//
7474
// On success it returns 0 and fills pemRoots with a CFDataRef that contains the extracted root
7575
// certificates of the system. On failure, the function returns -1.
76+
// Additionally, it fills untrustedPemRoots with certs that must be removed from pemRoots.
7677
//
77-
// Note: The CFDataRef returned in pemRoots must be released (using CFRelease) after
78-
// we've consumed its content.
79-
int FetchPEMRoots(CFDataRef *pemRoots) {
78+
// Note: The CFDataRef returned in pemRoots and untrustedPemRoots must
79+
// be released (using CFRelease) after we've consumed its content.
80+
int FetchPEMRoots(CFDataRef *pemRoots, CFDataRef *untrustedPemRoots) {
8081
if (useOldCode()) {
8182
return FetchPEMRoots_MountainLion(pemRoots);
8283
}
@@ -93,23 +94,69 @@ int FetchPEMRoots(CFDataRef *pemRoots) {
9394
return -1;
9495
}
9596
97+
// kSecTrustSettingsResult is defined as CFSTR("kSecTrustSettingsResult"),
98+
// but the Go linker's internal linking mode can't handle CFSTR relocations.
99+
// Create our own dynamic string instead and release it below.
100+
CFStringRef policy = CFStringCreateWithCString(NULL, "kSecTrustSettingsResult", kCFStringEncodingUTF8);
101+
96102
CFMutableDataRef combinedData = CFDataCreateMutable(kCFAllocatorDefault, 0);
103+
CFMutableDataRef combinedUntrustedData = CFDataCreateMutable(kCFAllocatorDefault, 0);
97104
for (int i = 0; i < numDomains; i++) {
98105
CFArrayRef certs = NULL;
99-
// Only get certificates from domain that are trusted
100106
OSStatus err = SecTrustSettingsCopyCertificates(domains[i], &certs);
101107
if (err != noErr) {
102108
continue;
103109
}
104110
105-
int numCerts = CFArrayGetCount(certs);
111+
CFIndex numCerts = CFArrayGetCount(certs);
106112
for (int j = 0; j < numCerts; j++) {
107113
CFDataRef data = NULL;
108114
CFErrorRef errRef = NULL;
115+
CFArrayRef trustSettings = NULL;
109116
SecCertificateRef cert = (SecCertificateRef)CFArrayGetValueAtIndex(certs, j);
110117
if (cert == NULL) {
111118
continue;
112119
}
120+
// We only want trusted certs.
121+
int untrusted = 0;
122+
if (i != 0) {
123+
// Certs found in the system domain are always trusted. If the user
124+
// configures "Never Trust" on such a cert, it will also be found in the
125+
// admin or user domain, causing it to be added to untrustedPemRoots. The
126+
// Go code will then clean this up.
127+
128+
// Trust may be stored in any of the domains. According to Apple's
129+
// SecTrustServer.c, "user trust settings overrule admin trust settings",
130+
// so take the last trust settings array we find.
131+
// Skip the system domain since it is always trusted.
132+
for (int k = 1; k < numDomains; k++) {
133+
CFArrayRef domainTrustSettings = NULL;
134+
err = SecTrustSettingsCopyTrustSettings(cert, domains[k], &domainTrustSettings);
135+
if (err == errSecSuccess && domainTrustSettings != NULL) {
136+
if (trustSettings) {
137+
CFRelease(trustSettings);
138+
}
139+
trustSettings = domainTrustSettings;
140+
}
141+
}
142+
if (trustSettings == NULL) {
143+
// "this certificate must be verified to a known trusted certificate"; aka not a root.
144+
continue;
145+
}
146+
for (CFIndex k = 0; k < CFArrayGetCount(trustSettings); k++) {
147+
CFNumberRef cfNum;
148+
CFDictionaryRef tSetting = (CFDictionaryRef)CFArrayGetValueAtIndex(trustSettings, k);
149+
if (CFDictionaryGetValueIfPresent(tSetting, policy, (const void**)&cfNum)){
150+
SInt32 result = 0;
151+
CFNumberGetValue(cfNum, kCFNumberSInt32Type, &result);
152+
// TODO: The rest of the dictionary specifies conditions for evaluation.
153+
if (result == kSecTrustSettingsResultDeny) {
154+
untrusted = 1;
155+
}
156+
}
157+
}
158+
CFRelease(trustSettings);
159+
}
113160
// We only want to add Root CAs, so make sure Subject and Issuer Name match
114161
CFDataRef subjectName = SecCertificateCopyNormalizedSubjectContent(cert, &errRef);
115162
if (errRef != NULL) {
@@ -138,13 +185,16 @@ int FetchPEMRoots(CFDataRef *pemRoots) {
138185
}
139186
140187
if (data != NULL) {
141-
CFDataAppendBytes(combinedData, CFDataGetBytePtr(data), CFDataGetLength(data));
188+
CFMutableDataRef appendTo = untrusted ? combinedUntrustedData : combinedData;
189+
CFDataAppendBytes(appendTo, CFDataGetBytePtr(data), CFDataGetLength(data));
142190
CFRelease(data);
143191
}
144192
}
145193
CFRelease(certs);
146194
}
195+
CFRelease(policy);
147196
*pemRoots = combinedData;
197+
*untrustedPemRoots = combinedUntrustedData;
148198
return 0;
149199
}
150200
*/
@@ -158,7 +208,8 @@ func loadSystemRoots() (*CertPool, error) {
158208
roots := NewCertPool()
159209

160210
var data C.CFDataRef = nil
161-
err := C.FetchPEMRoots(&data)
211+
var untrustedData C.CFDataRef = nil
212+
err := C.FetchPEMRoots(&data, &untrustedData)
162213
if err == -1 {
163214
// TODO: better error message
164215
return nil, errors.New("crypto/x509: failed to load darwin system roots with cgo")
@@ -167,5 +218,19 @@ func loadSystemRoots() (*CertPool, error) {
167218
defer C.CFRelease(C.CFTypeRef(data))
168219
buf := C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(data)), C.int(C.CFDataGetLength(data)))
169220
roots.AppendCertsFromPEM(buf)
170-
return roots, nil
221+
if untrustedData == nil {
222+
return roots, nil
223+
}
224+
defer C.CFRelease(C.CFTypeRef(untrustedData))
225+
buf = C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(untrustedData)), C.int(C.CFDataGetLength(untrustedData)))
226+
untrustedRoots := NewCertPool()
227+
untrustedRoots.AppendCertsFromPEM(buf)
228+
229+
trustedRoots := NewCertPool()
230+
for _, c := range roots.certs {
231+
if !untrustedRoots.contains(c) {
232+
trustedRoots.AddCert(c)
233+
}
234+
}
235+
return trustedRoots, nil
171236
}

src/crypto/x509/root_darwin.go

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,128 @@
66

77
package x509
88

9-
import "os/exec"
9+
import (
10+
"bytes"
11+
"encoding/pem"
12+
"fmt"
13+
"io/ioutil"
14+
"os"
15+
"os/exec"
16+
"strconv"
17+
"sync"
18+
"syscall"
19+
)
1020

1121
func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) {
1222
return nil, nil
1323
}
1424

25+
// This code is only used when compiling without cgo.
26+
// It is here, instead of root_nocgo_darwin.go, so that tests can check it
27+
// even if the tests are run with cgo enabled.
28+
// The linker will not include these unused functions in binaries built with cgo enabled.
29+
1530
func execSecurityRoots() (*CertPool, error) {
1631
cmd := exec.Command("/usr/bin/security", "find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain")
1732
data, err := cmd.Output()
1833
if err != nil {
1934
return nil, err
2035
}
2136

22-
roots := NewCertPool()
23-
roots.AppendCertsFromPEM(data)
37+
var (
38+
mu sync.Mutex
39+
roots = NewCertPool()
40+
)
41+
add := func(cert *Certificate) {
42+
mu.Lock()
43+
defer mu.Unlock()
44+
roots.AddCert(cert)
45+
}
46+
blockCh := make(chan *pem.Block)
47+
var wg sync.WaitGroup
48+
for i := 0; i < 4; i++ {
49+
wg.Add(1)
50+
go func() {
51+
defer wg.Done()
52+
for block := range blockCh {
53+
verifyCertWithSystem(block, add)
54+
}
55+
}()
56+
}
57+
for len(data) > 0 {
58+
var block *pem.Block
59+
block, data = pem.Decode(data)
60+
if block == nil {
61+
break
62+
}
63+
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
64+
continue
65+
}
66+
blockCh <- block
67+
}
68+
close(blockCh)
69+
wg.Wait()
2470
return roots, nil
2571
}
72+
73+
func verifyCertWithSystem(block *pem.Block, add func(*Certificate)) {
74+
data := pem.EncodeToMemory(block)
75+
var cmd *exec.Cmd
76+
if needsTmpFiles() {
77+
f, err := ioutil.TempFile("", "cert")
78+
if err != nil {
79+
fmt.Fprintf(os.Stderr, "can't create temporary file for cert: %v", err)
80+
return
81+
}
82+
defer os.Remove(f.Name())
83+
if _, err := f.Write(data); err != nil {
84+
fmt.Fprintf(os.Stderr, "can't write temporary file for cert: %v", err)
85+
return
86+
}
87+
if err := f.Close(); err != nil {
88+
fmt.Fprintf(os.Stderr, "can't write temporary file for cert: %v", err)
89+
return
90+
}
91+
cmd = exec.Command("/usr/bin/security", "verify-cert", "-c", f.Name(), "-l")
92+
} else {
93+
cmd = exec.Command("/usr/bin/security", "verify-cert", "-c", "/dev/stdin", "-l")
94+
cmd.Stdin = bytes.NewReader(data)
95+
}
96+
if cmd.Run() == nil {
97+
// Non-zero exit means untrusted
98+
cert, err := ParseCertificate(block.Bytes)
99+
if err != nil {
100+
return
101+
}
102+
103+
add(cert)
104+
}
105+
}
106+
107+
var versionCache struct {
108+
sync.Once
109+
major int
110+
}
111+
112+
// needsTmpFiles reports whether the OS is <= 10.11 (which requires real
113+
// files as arguments to the security command).
114+
func needsTmpFiles() bool {
115+
versionCache.Do(func() {
116+
release, err := syscall.Sysctl("kern.osrelease")
117+
if err != nil {
118+
return
119+
}
120+
for i, c := range release {
121+
if c == '.' {
122+
release = release[:i]
123+
break
124+
}
125+
}
126+
major, err := strconv.Atoi(release)
127+
if err != nil {
128+
return
129+
}
130+
versionCache.major = major
131+
})
132+
return versionCache.major <= 15
133+
}

src/crypto/x509/root_darwin_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func TestSystemRoots(t *testing.T) {
2929
// On Mavericks, there are 212 bundled certs; require only
3030
// 150 here, since this is just a sanity check, and the
3131
// exact number will vary over time.
32+
t.Logf("got %d roots", len(tt.certs))
3233
if want, have := 150, len(tt.certs); have < want {
3334
t.Fatalf("want at least %d system roots, have %d", want, have)
3435
}

0 commit comments

Comments
 (0)