mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-04 23:45:34 +00:00
289 lines
8.2 KiB
Go
289 lines
8.2 KiB
Go
|
// 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 debugDarwinRoots = 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
|
||
|
// (Issue 26830) 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 debugDarwinRoots {
|
||
|
fmt.Fprintf(os.Stderr, "crypto/x509: %d certs have a trust policy\n", len(hasPolicy))
|
||
|
}
|
||
|
|
||
|
keychains := []string{"/Library/Keychains/System.keychain"}
|
||
|
|
||
|
// Note that this results in trusting roots from $HOME/... (the environment
|
||
|
// variable), which might not be expected.
|
||
|
home, err := os.UserHomeDir()
|
||
|
if err != nil {
|
||
|
if debugDarwinRoots {
|
||
|
fmt.Fprintf(os.Stderr, "crypto/x509: can't get user home directory: %v\n", err)
|
||
|
}
|
||
|
} else {
|
||
|
keychains = append(keychains,
|
||
|
filepath.Join(home, "/Library/Keychains/login.keychain"),
|
||
|
|
||
|
// Fresh installs of Sierra use a slightly different path for the login keychain
|
||
|
filepath.Join(home, "/Library/Keychains/login.keychain-db"),
|
||
|
)
|
||
|
}
|
||
|
|
||
|
type rootCandidate struct {
|
||
|
c *Certificate
|
||
|
system bool
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
mu sync.Mutex
|
||
|
roots = NewCertPool()
|
||
|
numVerified int // number of execs of 'security verify-cert', for debug stats
|
||
|
wg sync.WaitGroup
|
||
|
verifyCh = make(chan rootCandidate)
|
||
|
)
|
||
|
|
||
|
// 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 cert := range verifyCh {
|
||
|
sha1CapHex := fmt.Sprintf("%X", sha1.Sum(cert.c.Raw))
|
||
|
|
||
|
var valid bool
|
||
|
verifyChecks := 0
|
||
|
if hasPolicy[sha1CapHex] {
|
||
|
verifyChecks++
|
||
|
valid = verifyCertWithSystem(cert.c)
|
||
|
} else {
|
||
|
// Certificates not in SystemRootCertificates without user
|
||
|
// or admin trust settings are not trusted.
|
||
|
valid = cert.system
|
||
|
}
|
||
|
|
||
|
mu.Lock()
|
||
|
numVerified += verifyChecks
|
||
|
if valid {
|
||
|
roots.AddCert(cert.c)
|
||
|
}
|
||
|
mu.Unlock()
|
||
|
}
|
||
|
}()
|
||
|
}
|
||
|
err = forEachCertInKeychains(keychains, func(cert *Certificate) {
|
||
|
verifyCh <- rootCandidate{c: cert, system: false}
|
||
|
})
|
||
|
if err != nil {
|
||
|
close(verifyCh)
|
||
|
return nil, err
|
||
|
}
|
||
|
err = forEachCertInKeychains([]string{
|
||
|
"/System/Library/Keychains/SystemRootCertificates.keychain",
|
||
|
}, func(cert *Certificate) {
|
||
|
verifyCh <- rootCandidate{c: cert, system: true}
|
||
|
})
|
||
|
if err != nil {
|
||
|
close(verifyCh)
|
||
|
return nil, err
|
||
|
}
|
||
|
close(verifyCh)
|
||
|
wg.Wait()
|
||
|
|
||
|
if debugDarwinRoots {
|
||
|
fmt.Fprintf(os.Stderr, "crypto/x509: ran security verify-cert %d times\n", numVerified)
|
||
|
}
|
||
|
|
||
|
return roots, nil
|
||
|
}
|
||
|
|
||
|
func forEachCertInKeychains(paths []string, f func(*Certificate)) error {
|
||
|
args := append([]string{"find-certificate", "-a", "-p"}, paths...)
|
||
|
cmd := exec.Command("/usr/bin/security", args...)
|
||
|
data, err := cmd.Output()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
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
|
||
|
}
|
||
|
cert, err := ParseCertificate(block.Bytes)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
f(cert)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func verifyCertWithSystem(cert *Certificate) bool {
|
||
|
data := pem.EncodeToMemory(&pem.Block{
|
||
|
Type: "CERTIFICATE", Bytes: cert.Raw,
|
||
|
})
|
||
|
|
||
|
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", "-p", "ssl", "-c", f.Name(), "-l", "-L")
|
||
|
var stderr bytes.Buffer
|
||
|
if debugDarwinRoots {
|
||
|
cmd.Stderr = &stderr
|
||
|
}
|
||
|
if err := cmd.Run(); err != nil {
|
||
|
if debugDarwinRoots {
|
||
|
fmt.Fprintf(os.Stderr, "crypto/x509: verify-cert rejected %s: %q\n", cert.Subject, bytes.TrimSpace(stderr.Bytes()))
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
if debugDarwinRoots {
|
||
|
fmt.Fprintf(os.Stderr, "crypto/x509: verify-cert approved %s\n", cert.Subject)
|
||
|
}
|
||
|
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 debugDarwinRoots {
|
||
|
fmt.Fprintf(os.Stderr, "crypto/x509: exec %q: %v, %s\n", 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
|
||
|
}
|