Home | History | Annotate | Download | only in x509
      1 // Copyright 2013 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 //go:generate go run root_darwin_arm_gen.go -output root_darwin_armx.go
      6 
      7 package x509
      8 
      9 import (
     10 	"bufio"
     11 	"bytes"
     12 	"crypto/sha1"
     13 	"encoding/pem"
     14 	"fmt"
     15 	"io"
     16 	"io/ioutil"
     17 	"os"
     18 	"os/exec"
     19 	"path/filepath"
     20 	"strings"
     21 	"sync"
     22 )
     23 
     24 var debugExecDarwinRoots = strings.Contains(os.Getenv("GODEBUG"), "x509roots=1")
     25 
     26 func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) {
     27 	return nil, nil
     28 }
     29 
     30 // This code is only used when compiling without cgo.
     31 // It is here, instead of root_nocgo_darwin.go, so that tests can check it
     32 // even if the tests are run with cgo enabled.
     33 // The linker will not include these unused functions in binaries built with cgo enabled.
     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)
     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 
     64 	cmd := exec.Command("/usr/bin/security", "find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain")
     65 	data, err := cmd.Output()
     66 	if err != nil {
     67 		return nil, err
     68 	}
     69 
     70 	var (
     71 		mu          sync.Mutex
     72 		roots       = NewCertPool()
     73 		numVerified int // number of execs of 'security verify-cert', for debug stats
     74 	)
     75 
     76 	blockCh := make(chan *pem.Block)
     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 policy. These 4 goroutines are only
     87 	// defensive in the pathological case of many trust edits.
     88 	for i := 0; i < 4; i++ {
     89 		wg.Add(1)
     90 		go func() {
     91 			defer wg.Done()
     92 			for block := range blockCh {
     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()
    114 			}
    115 		}()
    116 	}
    117 	for len(data) > 0 {
    118 		var block *pem.Block
    119 		block, data = pem.Decode(data)
    120 		if block == nil {
    121 			break
    122 		}
    123 		if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
    124 			continue
    125 		}
    126 		blockCh <- block
    127 	}
    128 	close(blockCh)
    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 
    137 	return roots, nil
    138 }
    139 
    140 func verifyCertWithSystem(block *pem.Block, cert *Certificate) bool {
    141 	data := pem.EncodeToMemory(block)
    142 
    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))
    170 	}
    171 	return true
    172 }
    173 
    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
    199 			// localized on macOS, just interpret any failure to mean that
    200 			// there are 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 		}
    206 
    207 		f, err := os.Open(file)
    208 		if err != nil {
    209 			return err
    210 		}
    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 {
    228 				break
    229 			}
    230 			if err != nil {
    231 				return err
    232 			}
    233 		}
    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
    244 }
    245