7
7
package x509
8
8
9
9
import (
10
+ "bufio"
10
11
"bytes"
12
+ "crypto/sha1"
11
13
"encoding/pem"
12
14
"fmt"
15
+ "io"
13
16
"io/ioutil"
14
17
"os"
15
18
"os/exec"
16
- "strconv"
19
+ "path/filepath"
20
+ "strings"
17
21
"sync"
18
- "syscall"
19
22
)
20
23
24
+ var debugExecDarwinRoots = strings .Contains (os .Getenv ("GODEBUG" ), "x509roots=1" )
25
+
21
26
func (c * Certificate ) systemVerify (opts * VerifyOptions ) (chains [][]* Certificate , err error ) {
22
27
return nil , nil
23
28
}
@@ -27,30 +32,85 @@ func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate
27
32
// even if the tests are run with cgo enabled.
28
33
// The linker will not include these unused functions in binaries built with cgo enabled.
29
34
35
+ // execSecurityRoots finds the macOS list of trusted root certificates
36
+ // using only command-line tools. This is our fallback path when cgo isn't available.
37
+ //
38
+ // The strategy is as follows:
39
+ //
40
+ // 1. Run "security trust-settings-export" and "security
41
+ // trust-settings-export -d" to discover the set of certs with some
42
+ // user-tweaked trust policy. We're too lazy to parse the XML (at
43
+ // least at this stage of Go 1.8) to understand what the trust
44
+ // policy actually is. We just learn that there is _some_ policy.
45
+ //
46
+ // 2. Run "security find-certificate" to dump the list of system root
47
+ // CAs in PEM format.
48
+ //
49
+ // 3. For each dumped cert, conditionally verify it with "security
50
+ // verify-cert" if that cert was in the set discovered in Step 1.
51
+ // Without the Step 1 optimization, running "security verify-cert"
52
+ // 150-200 times takes 3.5 seconds. With the optimization, the
53
+ // whole process takes about 180 milliseconds with 1 untrusted root
54
+ // CA. (Compared to 110ms in the cgo path)
30
55
func execSecurityRoots () (* CertPool , error ) {
56
+ hasPolicy , err := getCertsWithTrustPolicy ()
57
+ if err != nil {
58
+ return nil , err
59
+ }
60
+ if debugExecDarwinRoots {
61
+ println (fmt .Sprintf ("crypto/x509: %d certs have a trust policy" , len (hasPolicy )))
62
+ }
63
+
31
64
cmd := exec .Command ("/usr/bin/security" , "find-certificate" , "-a" , "-p" , "/System/Library/Keychains/SystemRootCertificates.keychain" )
32
65
data , err := cmd .Output ()
33
66
if err != nil {
34
67
return nil , err
35
68
}
36
69
37
70
var (
38
- mu sync.Mutex
39
- roots = NewCertPool ()
71
+ mu sync.Mutex
72
+ roots = NewCertPool ()
73
+ numVerified int // number of execs of 'security verify-cert', for debug stats
40
74
)
41
- add := func (cert * Certificate ) {
42
- mu .Lock ()
43
- defer mu .Unlock ()
44
- roots .AddCert (cert )
45
- }
75
+
46
76
blockCh := make (chan * pem.Block )
47
77
var wg sync.WaitGroup
78
+
79
+ // Using 4 goroutines to pipe into verify-cert seems to be
80
+ // about the best we can do. The verify-cert binary seems to
81
+ // just RPC to another server with coarse locking anyway, so
82
+ // running 16 at a time for instance doesn't help at all. Due
83
+ // to the "if hasPolicy" check below, though, we will rarely
84
+ // (or never) call verify-cert on stock macOS systems, though.
85
+ // The hope is that we only call verify-cert when the user has
86
+ // tweaked their trust poliy. These 4 goroutines are only
87
+ // defensive in the pathological case of many trust edits.
48
88
for i := 0 ; i < 4 ; i ++ {
49
89
wg .Add (1 )
50
90
go func () {
51
91
defer wg .Done ()
52
92
for block := range blockCh {
53
- verifyCertWithSystem (block , add )
93
+ cert , err := ParseCertificate (block .Bytes )
94
+ if err != nil {
95
+ continue
96
+ }
97
+ sha1CapHex := fmt .Sprintf ("%X" , sha1 .Sum (block .Bytes ))
98
+
99
+ valid := true
100
+ verifyChecks := 0
101
+ if hasPolicy [sha1CapHex ] {
102
+ verifyChecks ++
103
+ if ! verifyCertWithSystem (block , cert ) {
104
+ valid = false
105
+ }
106
+ }
107
+
108
+ mu .Lock ()
109
+ numVerified += verifyChecks
110
+ if valid {
111
+ roots .AddCert (cert )
112
+ }
113
+ mu .Unlock ()
54
114
}
55
115
}()
56
116
}
@@ -67,67 +127,118 @@ func execSecurityRoots() (*CertPool, error) {
67
127
}
68
128
close (blockCh )
69
129
wg .Wait ()
130
+
131
+ if debugExecDarwinRoots {
132
+ mu .Lock ()
133
+ defer mu .Unlock ()
134
+ println (fmt .Sprintf ("crypto/x509: ran security verify-cert %d times" , numVerified ))
135
+ }
136
+
70
137
return roots , nil
71
138
}
72
139
73
- func verifyCertWithSystem (block * pem.Block , add func ( * Certificate )) {
140
+ func verifyCertWithSystem (block * pem.Block , cert * Certificate ) bool {
74
141
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
142
103
- add (cert )
143
+ f , err := ioutil .TempFile ("" , "cert" )
144
+ if err != nil {
145
+ fmt .Fprintf (os .Stderr , "can't create temporary file for cert: %v" , err )
146
+ return false
147
+ }
148
+ defer os .Remove (f .Name ())
149
+ if _ , err := f .Write (data ); err != nil {
150
+ fmt .Fprintf (os .Stderr , "can't write temporary file for cert: %v" , err )
151
+ return false
152
+ }
153
+ if err := f .Close (); err != nil {
154
+ fmt .Fprintf (os .Stderr , "can't write temporary file for cert: %v" , err )
155
+ return false
156
+ }
157
+ cmd := exec .Command ("/usr/bin/security" , "verify-cert" , "-c" , f .Name (), "-l" , "-L" )
158
+ var stderr bytes.Buffer
159
+ if debugExecDarwinRoots {
160
+ cmd .Stderr = & stderr
161
+ }
162
+ if err := cmd .Run (); err != nil {
163
+ if debugExecDarwinRoots {
164
+ println (fmt .Sprintf ("crypto/x509: verify-cert rejected %s: %q" , cert .Subject .CommonName , bytes .TrimSpace (stderr .Bytes ())))
165
+ }
166
+ return false
167
+ }
168
+ if debugExecDarwinRoots {
169
+ println (fmt .Sprintf ("crypto/x509: verify-cert approved %s" , cert .Subject .CommonName ))
104
170
}
171
+ return true
105
172
}
106
173
107
- var versionCache struct {
108
- sync.Once
109
- major int
110
- }
174
+ // getCertsWithTrustPolicy returns the set of certs that have a
175
+ // possibly-altered trust policy. The keys of the map are capitalized
176
+ // sha1 hex of the raw cert.
177
+ // They are the certs that should be checked against `security
178
+ // verify-cert` to see whether the user altered the default trust
179
+ // settings. This code is only used for cgo-disabled builds.
180
+ func getCertsWithTrustPolicy () (map [string ]bool , error ) {
181
+ set := map [string ]bool {}
182
+ td , err := ioutil .TempDir ("" , "x509trustpolicy" )
183
+ if err != nil {
184
+ return nil , err
185
+ }
186
+ defer os .RemoveAll (td )
187
+ run := func (file string , args ... string ) error {
188
+ file = filepath .Join (td , file )
189
+ args = append (args , file )
190
+ cmd := exec .Command ("/usr/bin/security" , args ... )
191
+ var stderr bytes.Buffer
192
+ cmd .Stderr = & stderr
193
+ if err := cmd .Run (); err != nil {
194
+ // If there are no trust settings, the
195
+ // `security trust-settings-export` command
196
+ // fails with:
197
+ // exit status 1, SecTrustSettingsCreateExternalRepresentation: No Trust Settings were found.
198
+ // Rather than match on English substrings that are probably localized
199
+ // on macOS, just treat interpret any failure as meaning that there are
200
+ // no trust settings.
201
+ if debugExecDarwinRoots {
202
+ println (fmt .Sprintf ("crypto/x509: exec %q: %v, %s" , cmd .Args , err , stderr .Bytes ()))
203
+ }
204
+ return nil
205
+ }
111
206
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" )
207
+ f , err := os .Open (file )
117
208
if err != nil {
118
- return
209
+ return err
119
210
}
120
- for i , c := range release {
121
- if c == '.' {
122
- release = release [:i ]
211
+ defer f .Close ()
212
+
213
+ // Gather all the runs of 40 capitalized hex characters.
214
+ br := bufio .NewReader (f )
215
+ var hexBuf bytes.Buffer
216
+ for {
217
+ b , err := br .ReadByte ()
218
+ isHex := ('A' <= b && b <= 'F' ) || ('0' <= b && b <= '9' )
219
+ if isHex {
220
+ hexBuf .WriteByte (b )
221
+ } else {
222
+ if hexBuf .Len () == 40 {
223
+ set [hexBuf .String ()] = true
224
+ }
225
+ hexBuf .Reset ()
226
+ }
227
+ if err == io .EOF {
123
228
break
124
229
}
230
+ if err != nil {
231
+ return err
232
+ }
125
233
}
126
- major , err := strconv .Atoi (release )
127
- if err != nil {
128
- return
129
- }
130
- versionCache .major = major
131
- })
132
- return versionCache .major <= 15
234
+
235
+ return nil
236
+ }
237
+ if err := run ("user" , "trust-settings-export" ); err != nil {
238
+ return nil , fmt .Errorf ("dump-trust-settings (user): %v" , err )
239
+ }
240
+ if err := run ("admin" , "trust-settings-export" , "-d" ); err != nil {
241
+ return nil , fmt .Errorf ("dump-trust-settings (admin): %v" , err )
242
+ }
243
+ return set , nil
133
244
}
0 commit comments