diff options
-rw-r--r-- | src/crypto/x509/root_darwin.go | 229 | ||||
-rw-r--r-- | src/crypto/x509/root_darwin_test.go | 11 |
2 files changed, 180 insertions, 60 deletions
diff --git a/src/crypto/x509/root_darwin.go b/src/crypto/x509/root_darwin.go index 59b303d64f..acdf43c94a 100644 --- a/src/crypto/x509/root_darwin.go +++ b/src/crypto/x509/root_darwin.go @@ -7,17 +7,22 @@ package x509 import ( + "bufio" "bytes" + "crypto/sha1" "encoding/pem" "fmt" + "io" "io/ioutil" "os" "os/exec" - "strconv" + "path/filepath" + "strings" "sync" - "syscall" ) +var debugExecDarwinRoots = strings.Contains(os.Getenv("GODEBUG"), "x509roots=1") + func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) { return nil, nil } @@ -27,7 +32,35 @@ func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate // even if the tests are run with cgo enabled. // The linker will not include these unused functions in binaries built with cgo enabled. +// execSecurityRoots finds the macOS list of trusted root certificates +// using only command-line tools. This is our fallback path when cgo isn't available. +// +// The strategy is as follows: +// +// 1. Run "security trust-settings-export" and "security +// trust-settings-export -d" to discover the set of certs with some +// user-tweaked trust policy. We're too lazy to parse the XML (at +// least at this stage of Go 1.8) to understand what the trust +// policy actually is. We just learn that there is _some_ policy. +// +// 2. Run "security find-certificate" to dump the list of system root +// CAs in PEM format. +// +// 3. For each dumped cert, conditionally verify it with "security +// verify-cert" if that cert was in the set discovered in Step 1. +// Without the Step 1 optimization, running "security verify-cert" +// 150-200 times takes 3.5 seconds. With the optimization, the +// whole process takes about 180 milliseconds with 1 untrusted root +// CA. (Compared to 110ms in the cgo path) func execSecurityRoots() (*CertPool, error) { + hasPolicy, err := getCertsWithTrustPolicy() + if err != nil { + return nil, err + } + if debugExecDarwinRoots { + println(fmt.Sprintf("crypto/x509: %d certs have a trust policy", len(hasPolicy))) + } + cmd := exec.Command("/usr/bin/security", "find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain") data, err := cmd.Output() if err != nil { @@ -35,22 +68,49 @@ func execSecurityRoots() (*CertPool, error) { } var ( - mu sync.Mutex - roots = NewCertPool() + mu sync.Mutex + roots = NewCertPool() + numVerified int // number of execs of 'security verify-cert', for debug stats ) - add := func(cert *Certificate) { - mu.Lock() - defer mu.Unlock() - roots.AddCert(cert) - } + blockCh := make(chan *pem.Block) var wg sync.WaitGroup + + // Using 4 goroutines to pipe into verify-cert seems to be + // about the best we can do. The verify-cert binary seems to + // just RPC to another server with coarse locking anyway, so + // running 16 at a time for instance doesn't help at all. Due + // to the "if hasPolicy" check below, though, we will rarely + // (or never) call verify-cert on stock macOS systems, though. + // The hope is that we only call verify-cert when the user has + // tweaked their trust poliy. These 4 goroutines are only + // defensive in the pathological case of many trust edits. for i := 0; i < 4; i++ { wg.Add(1) go func() { defer wg.Done() for block := range blockCh { - verifyCertWithSystem(block, add) + cert, err := ParseCertificate(block.Bytes) + if err != nil { + continue + } + sha1CapHex := fmt.Sprintf("%X", sha1.Sum(block.Bytes)) + + valid := true + verifyChecks := 0 + if hasPolicy[sha1CapHex] { + verifyChecks++ + if !verifyCertWithSystem(block, cert) { + valid = false + } + } + + mu.Lock() + numVerified += verifyChecks + if valid { + roots.AddCert(cert) + } + mu.Unlock() } }() } @@ -67,67 +127,118 @@ func execSecurityRoots() (*CertPool, error) { } close(blockCh) wg.Wait() + + if debugExecDarwinRoots { + mu.Lock() + defer mu.Unlock() + println(fmt.Sprintf("crypto/x509: ran security verify-cert %d times", numVerified)) + } + return roots, nil } -func verifyCertWithSystem(block *pem.Block, add func(*Certificate)) { +func verifyCertWithSystem(block *pem.Block, cert *Certificate) bool { data := pem.EncodeToMemory(block) - var cmd *exec.Cmd - if needsTmpFiles() { - f, err := ioutil.TempFile("", "cert") - if err != nil { - fmt.Fprintf(os.Stderr, "can't create temporary file for cert: %v", err) - return - } - defer os.Remove(f.Name()) - if _, err := f.Write(data); err != nil { - fmt.Fprintf(os.Stderr, "can't write temporary file for cert: %v", err) - return - } - if err := f.Close(); err != nil { - fmt.Fprintf(os.Stderr, "can't write temporary file for cert: %v", err) - return - } - cmd = exec.Command("/usr/bin/security", "verify-cert", "-c", f.Name(), "-l") - } else { - cmd = exec.Command("/usr/bin/security", "verify-cert", "-c", "/dev/stdin", "-l") - cmd.Stdin = bytes.NewReader(data) - } - if cmd.Run() == nil { - // Non-zero exit means untrusted - cert, err := ParseCertificate(block.Bytes) - if err != nil { - return - } - add(cert) + f, err := ioutil.TempFile("", "cert") + if err != nil { + fmt.Fprintf(os.Stderr, "can't create temporary file for cert: %v", err) + return false + } + defer os.Remove(f.Name()) + if _, err := f.Write(data); err != nil { + fmt.Fprintf(os.Stderr, "can't write temporary file for cert: %v", err) + return false + } + if err := f.Close(); err != nil { + fmt.Fprintf(os.Stderr, "can't write temporary file for cert: %v", err) + return false + } + cmd := exec.Command("/usr/bin/security", "verify-cert", "-c", f.Name(), "-l", "-L") + var stderr bytes.Buffer + if debugExecDarwinRoots { + cmd.Stderr = &stderr + } + if err := cmd.Run(); err != nil { + if debugExecDarwinRoots { + println(fmt.Sprintf("crypto/x509: verify-cert rejected %s: %q", cert.Subject.CommonName, bytes.TrimSpace(stderr.Bytes()))) + } + return false + } + if debugExecDarwinRoots { + println(fmt.Sprintf("crypto/x509: verify-cert approved %s", cert.Subject.CommonName)) } + return true } -var versionCache struct { - sync.Once - major int -} +// getCertsWithTrustPolicy returns the set of certs that have a +// possibly-altered trust policy. The keys of the map are capitalized +// sha1 hex of the raw cert. +// They are the certs that should be checked against `security +// verify-cert` to see whether the user altered the default trust +// settings. This code is only used for cgo-disabled builds. +func getCertsWithTrustPolicy() (map[string]bool, error) { + set := map[string]bool{} + td, err := ioutil.TempDir("", "x509trustpolicy") + if err != nil { + return nil, err + } + defer os.RemoveAll(td) + run := func(file string, args ...string) error { + file = filepath.Join(td, file) + args = append(args, file) + cmd := exec.Command("/usr/bin/security", args...) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + // If there are no trust settings, the + // `security trust-settings-export` command + // fails with: + // exit status 1, SecTrustSettingsCreateExternalRepresentation: No Trust Settings were found. + // Rather than match on English substrings that are probably localized + // on macOS, just treat interpret any failure as meaning that there are + // no trust settings. + if debugExecDarwinRoots { + println(fmt.Sprintf("crypto/x509: exec %q: %v, %s", cmd.Args, err, stderr.Bytes())) + } + return nil + } -// needsTmpFiles reports whether the OS is <= 10.11 (which requires real -// files as arguments to the security command). -func needsTmpFiles() bool { - versionCache.Do(func() { - release, err := syscall.Sysctl("kern.osrelease") + f, err := os.Open(file) if err != nil { - return + return err } - for i, c := range release { - if c == '.' { - release = release[:i] + defer f.Close() + + // Gather all the runs of 40 capitalized hex characters. + br := bufio.NewReader(f) + var hexBuf bytes.Buffer + for { + b, err := br.ReadByte() + isHex := ('A' <= b && b <= 'F') || ('0' <= b && b <= '9') + if isHex { + hexBuf.WriteByte(b) + } else { + if hexBuf.Len() == 40 { + set[hexBuf.String()] = true + } + hexBuf.Reset() + } + if err == io.EOF { break } + if err != nil { + return err + } } - major, err := strconv.Atoi(release) - if err != nil { - return - } - versionCache.major = major - }) - return versionCache.major <= 15 + + return nil + } + if err := run("user", "trust-settings-export"); err != nil { + return nil, fmt.Errorf("dump-trust-settings (user): %v", err) + } + if err := run("admin", "trust-settings-export", "-d"); err != nil { + return nil, fmt.Errorf("dump-trust-settings (admin): %v", err) + } + return set, nil } diff --git a/src/crypto/x509/root_darwin_test.go b/src/crypto/x509/root_darwin_test.go index c8ca3ead70..ac97e4e6bb 100644 --- a/src/crypto/x509/root_darwin_test.go +++ b/src/crypto/x509/root_darwin_test.go @@ -7,6 +7,7 @@ package x509 import ( "runtime" "testing" + "time" ) func TestSystemRoots(t *testing.T) { @@ -15,13 +16,21 @@ func TestSystemRoots(t *testing.T) { t.Skipf("skipping on %s/%s, no system root", runtime.GOOS, runtime.GOARCH) } - sysRoots := systemRootsPool() // actual system roots + t0 := time.Now() + sysRoots := systemRootsPool() // actual system roots + sysRootsDuration := time.Since(t0) + + t1 := time.Now() execRoots, err := execSecurityRoots() // non-cgo roots + execSysRootsDuration := time.Since(t1) if err != nil { t.Fatalf("failed to read system roots: %v", err) } + t.Logf(" cgo sys roots: %v", sysRootsDuration) + t.Logf("non-cgo sys roots: %v", execSysRootsDuration) + for _, tt := range []*CertPool{sysRoots, execRoots} { if tt == nil { t.Fatal("no system roots") |