blob: 66cdb5ea261f0d2535224bbce4fa186872291f8f [file] [log] [blame]
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run root_darwin_arm_gen.go -output root_darwin_armx.go
package x509
import (
"bufio"
"bytes"
"crypto/sha1"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
)
var debugExecDarwinRoots = strings.Contains(os.Getenv("GODEBUG"), "x509roots=1")
func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) {
return nil, nil
}
// This code is only used when compiling without cgo.
// It is here, instead of root_nocgo_darwin.go, so that tests can check it
// 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 {
return nil, err
}
var (
mu sync.Mutex
roots = NewCertPool()
numVerified int // number of execs of 'security verify-cert', for debug stats
)
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 policy. 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 {
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()
}
}()
}
for len(data) > 0 {
var block *pem.Block
block, data = pem.Decode(data)
if block == nil {
break
}
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
continue
}
blockCh <- block
}
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, cert *Certificate) bool {
data := pem.EncodeToMemory(block)
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
}
// 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 interpret any failure to mean that
// there are no trust settings.
if debugExecDarwinRoots {
println(fmt.Sprintf("crypto/x509: exec %q: %v, %s", cmd.Args, err, stderr.Bytes()))
}
return nil
}
f, err := os.Open(file)
if err != nil {
return err
}
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
}
}
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
}