Skip to content

Commit

Permalink
crypto/x509: use Security.framework without cgo for roots on macOS
Browse files Browse the repository at this point in the history
+----------------------------------------------------------------------+
| Hello, if you are reading this and run macOS, please test this code: |
|                                                                      |
| $ GO111MODULE=on go get golang.org/dl/gotip@latest                   |
| $ gotip download                                              |
| $ GODEBUG=x509roots=1 gotip test crypto/x509 -v -run TestSystemRoots |
+----------------------------------------------------------------------+

We currently have two code paths to extract system roots on macOS: one
uses cgo to invoke a maze of Security.framework APIs; the other is a
horrible fallback that runs "/usr/bin/security verify-cert" on every
root that has custom policies to check if it's trusted for SSL.

The fallback is not only terrifying because it shells out to a binary,
but also because it lets in certificates that are not trusted roots but
are signed by trusted roots, and because it applies some filters (EKUs
and expiration) only to roots with custom policies, as the others are
not passed to verify-cert. The other code path, of course, requires cgo,
so can't be used when cross-compiling and involves a large ball of C.

It's all a mess, and it broke oh-so-many times (#14514, #16532, #19436,
 #20990, #21416, #24437, #24652, #25649, #26073, #27958, #28025, #28092,
 #29497, #30471, #30672, #30763, #30889, #32891, #38215, #38365, ...).

Since macOS does not have a stable syscall ABI, we already dynamically
link and invoke libSystem.dylib regardless of cgo availability (#17490).

How that works is that functions in package syscall (like syscall.Open)
take the address of assembly trampolines (like libc_open_trampoline)
that jump to symbols imported with cgo_import_dynamic (like libc_open),
and pass them along with arguments to syscall.syscall (which is
implemented as runtime.syscall_syscall). syscall_syscall informs the
scheduler and profiler, and then uses asmcgocall to switch to a system
stack and invoke runtime.syscall. The latter is an assembly trampoline
that unpacks the Go ABI arguments passed to syscall.syscall, finally
calls the remote function, and puts the return value on the Go stack.
(This last bit is the part that cgo compiles from a C wrapper.)

We can do something similar to link and invoke Security.framework!

The one difference is that runtime.syscall and friends check errors
based on the errno convention, which Security doesn't follow, so I added
runtime.syscallNoErr which just skips interpreting the return value.
We only need a variant with six arguments because the calling convention
is register-based, and extra arguments simply zero out some registers.

That's plumbed through as crypto/x509/internal/macOS.syscall. The rest
of that package is a set of wrappers for Security.framework and Core
Foundation functions, like syscall is for libSystem. In theory, as long
as macOS respects ABI backwards compatibility (a.k.a. as long as
binaries built for a previous OS version keep running) this should be
stable, as the final result is not different from what a C compiler
would make. (One exception might be dictionary key strings, which we
make our own copy of instead of using the dynamic symbol. If they change
the value of those strings things might break. But why would they.)

Finally, I rewrote the crypto/x509 cgo logic in Go using those wrappers.
It works! I tried to make it match 1:1 the old logic, so that
root_darwin_amd64.go can be reviewed by comparing it to
root_cgo_darwin_amd64.go. The only difference is that we do proper error
handling now, and assume that if there is no error the return values are
there, while before we'd just check for nil pointers and move on.

I kept the cgo logic to help with review and testing, but we should
delete it once we are confident the new code works.

The nocgo logic is gone and we shall never speak of it again.

Fixes #32604
Fixes #19561
Fixes #38365
Awakens Cthulhu

Change-Id: Id850962bad667f71e3af594bdfebbbb1edfbcbb4
Reviewed-on: https://go-review.googlesource.com/c/go/+/227037
Reviewed-by: Katie Hockman <[email protected]>
  • Loading branch information
FiloSottile committed May 7, 2020
1 parent 6ea19bb commit 6f52790
Show file tree
Hide file tree
Showing 15 changed files with 620 additions and 394 deletions.
141 changes: 141 additions & 0 deletions src/crypto/x509/internal/macOS/corefoundation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2020 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.

// +build darwin,amd64

// Package macOS provides cgo-less wrappers for Core Foundation and
// Security.framework, similarly to how package syscall provides access to
// libSystem.dylib.
package macOS

import (
"errors"
"reflect"
"runtime"
"unsafe"
)

// CFRef is an opaque reference to a Core Foundation object. It is a pointer,
// but to memory not owned by Go, so not an unsafe.Pointer.
type CFRef uintptr

// CFDataToSlice returns a copy of the contents of data as a bytes slice.
func CFDataToSlice(data CFRef) []byte {
length := CFDataGetLength(data)
ptr := CFDataGetBytePtr(data)
src := (*[1 << 20]byte)(unsafe.Pointer(ptr))[:length:length]
out := make([]byte, length)
copy(out, src)
return out
}

type CFString CFRef

const kCFAllocatorDefault = 0
const kCFStringEncodingUTF8 = 0x08000100

//go:linkname x509_CFStringCreateWithBytes x509_CFStringCreateWithBytes
//go:cgo_import_dynamic x509_CFStringCreateWithBytes CFStringCreateWithBytes "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"

// StringToCFString returns a copy of the UTF-8 contents of s as a new CFString.
func StringToCFString(s string) CFString {
p := unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&s)).Data)
ret := syscall(funcPC(x509_CFStringCreateWithBytes_trampoline), kCFAllocatorDefault, uintptr(p),
uintptr(len(s)), uintptr(kCFStringEncodingUTF8), 0 /* isExternalRepresentation */, 0)
runtime.KeepAlive(p)
return CFString(ret)
}
func x509_CFStringCreateWithBytes_trampoline()

//go:linkname x509_CFDictionaryGetValueIfPresent x509_CFDictionaryGetValueIfPresent
//go:cgo_import_dynamic x509_CFDictionaryGetValueIfPresent CFDictionaryGetValueIfPresent "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"

func CFDictionaryGetValueIfPresent(dict CFRef, key CFString) (value CFRef, ok bool) {
ret := syscall(funcPC(x509_CFDictionaryGetValueIfPresent_trampoline), uintptr(dict), uintptr(key),
uintptr(unsafe.Pointer(&value)), 0, 0, 0)
if ret == 0 {
return 0, false
}
return value, true
}
func x509_CFDictionaryGetValueIfPresent_trampoline()

const kCFNumberSInt32Type = 3

//go:linkname x509_CFNumberGetValue x509_CFNumberGetValue
//go:cgo_import_dynamic x509_CFNumberGetValue CFNumberGetValue "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"

func CFNumberGetValue(num CFRef) (int32, error) {
var value int32
ret := syscall(funcPC(x509_CFNumberGetValue_trampoline), uintptr(num), uintptr(kCFNumberSInt32Type),
uintptr(unsafe.Pointer(&value)), 0, 0, 0)
if ret == 0 {
return 0, errors.New("CFNumberGetValue call failed")
}
return value, nil
}
func x509_CFNumberGetValue_trampoline()

//go:linkname x509_CFDataGetLength x509_CFDataGetLength
//go:cgo_import_dynamic x509_CFDataGetLength CFDataGetLength "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"

func CFDataGetLength(data CFRef) int {
ret := syscall(funcPC(x509_CFDataGetLength_trampoline), uintptr(data), 0, 0, 0, 0, 0)
return int(ret)
}
func x509_CFDataGetLength_trampoline()

//go:linkname x509_CFDataGetBytePtr x509_CFDataGetBytePtr
//go:cgo_import_dynamic x509_CFDataGetBytePtr CFDataGetBytePtr "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"

func CFDataGetBytePtr(data CFRef) uintptr {
ret := syscall(funcPC(x509_CFDataGetBytePtr_trampoline), uintptr(data), 0, 0, 0, 0, 0)
return ret
}
func x509_CFDataGetBytePtr_trampoline()

//go:linkname x509_CFArrayGetCount x509_CFArrayGetCount
//go:cgo_import_dynamic x509_CFArrayGetCount CFArrayGetCount "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"

func CFArrayGetCount(array CFRef) int {
ret := syscall(funcPC(x509_CFArrayGetCount_trampoline), uintptr(array), 0, 0, 0, 0, 0)
return int(ret)
}
func x509_CFArrayGetCount_trampoline()

//go:linkname x509_CFArrayGetValueAtIndex x509_CFArrayGetValueAtIndex
//go:cgo_import_dynamic x509_CFArrayGetValueAtIndex CFArrayGetValueAtIndex "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"

func CFArrayGetValueAtIndex(array CFRef, index int) CFRef {
ret := syscall(funcPC(x509_CFArrayGetValueAtIndex_trampoline), uintptr(array), uintptr(index), 0, 0, 0, 0)
return CFRef(ret)
}
func x509_CFArrayGetValueAtIndex_trampoline()

//go:linkname x509_CFEqual x509_CFEqual
//go:cgo_import_dynamic x509_CFEqual CFEqual "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"

func CFEqual(a, b CFRef) bool {
ret := syscall(funcPC(x509_CFEqual_trampoline), uintptr(a), uintptr(b), 0, 0, 0, 0)
return ret == 1
}
func x509_CFEqual_trampoline()

//go:linkname x509_CFRelease x509_CFRelease
//go:cgo_import_dynamic x509_CFRelease CFRelease "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"

func CFRelease(ref CFRef) {
syscall(funcPC(x509_CFRelease_trampoline), uintptr(ref), 0, 0, 0, 0, 0)
}
func x509_CFRelease_trampoline()

// syscall is implemented in the runtime package (runtime/sys_darwin.go)
func syscall(fn, a1, a2, a3, a4, a5, a6 uintptr) uintptr

// funcPC returns the entry point for f. See comments in runtime/proc.go
// for the function of the same name.
//go:nosplit
func funcPC(f func()) uintptr {
return **(**uintptr)(unsafe.Pointer(&f))
}
26 changes: 26 additions & 0 deletions src/crypto/x509/internal/macOS/corefoundation.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2020 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.

// +build darwin,amd64

#include "textflag.h"

TEXT ·x509_CFArrayGetCount_trampoline(SB),NOSPLIT,$0-0
JMP x509_CFArrayGetCount(SB)
TEXT ·x509_CFArrayGetValueAtIndex_trampoline(SB),NOSPLIT,$0-0
JMP x509_CFArrayGetValueAtIndex(SB)
TEXT ·x509_CFDataGetBytePtr_trampoline(SB),NOSPLIT,$0-0
JMP x509_CFDataGetBytePtr(SB)
TEXT ·x509_CFDataGetLength_trampoline(SB),NOSPLIT,$0-0
JMP x509_CFDataGetLength(SB)
TEXT ·x509_CFStringCreateWithBytes_trampoline(SB),NOSPLIT,$0-0
JMP x509_CFStringCreateWithBytes(SB)
TEXT ·x509_CFRelease_trampoline(SB),NOSPLIT,$0-0
JMP x509_CFRelease(SB)
TEXT ·x509_CFDictionaryGetValueIfPresent_trampoline(SB),NOSPLIT,$0-0
JMP x509_CFDictionaryGetValueIfPresent(SB)
TEXT ·x509_CFNumberGetValue_trampoline(SB),NOSPLIT,$0-0
JMP x509_CFNumberGetValue(SB)
TEXT ·x509_CFEqual_trampoline(SB),NOSPLIT,$0-0
JMP x509_CFEqual(SB)
116 changes: 116 additions & 0 deletions src/crypto/x509/internal/macOS/security.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2020 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.

// +build darwin,amd64

package macOS

import (
"errors"
"strconv"
"unsafe"
)

// Based on https://opensource.apple.com/source/Security/Security-59306.41.2/base/Security.h

type SecTrustSettingsResult int32

const (
SecTrustSettingsResultInvalid SecTrustSettingsResult = iota
SecTrustSettingsResultTrustRoot
SecTrustSettingsResultTrustAsRoot
SecTrustSettingsResultDeny
SecTrustSettingsResultUnspecified
)

type SecTrustSettingsDomain int32

const (
SecTrustSettingsDomainUser SecTrustSettingsDomain = iota
SecTrustSettingsDomainAdmin
SecTrustSettingsDomainSystem
)

type OSStatus struct {
call string
status int32
}

func (s OSStatus) Error() string {
return s.call + " error: " + strconv.Itoa(int(s.status))
}

// Dictionary keys are defined as build-time strings with CFSTR, but the Go
// linker's internal linking mode can't handle CFSTR relocations. Create our
// own dynamic strings instead and just never release them.
//
// Note that this might be the only thing that can break over time if
// these values change, as the ABI arguably requires using the strings
// pointed to by the symbols, not values that happen to be equal to them.

var SecTrustSettingsResultKey = StringToCFString("kSecTrustSettingsResult")
var SecTrustSettingsPolicy = StringToCFString("kSecTrustSettingsPolicy")
var SecTrustSettingsPolicyString = StringToCFString("kSecTrustSettingsPolicyString")
var SecPolicyOid = StringToCFString("SecPolicyOid")
var SecPolicyAppleSSL = StringToCFString("1.2.840.113635.100.1.3") // defined by POLICYMACRO

var ErrNoTrustSettings = errors.New("no trust settings found")

const errSecNoTrustSettings = -25263

//go:linkname x509_SecTrustSettingsCopyCertificates x509_SecTrustSettingsCopyCertificates
//go:cgo_import_dynamic x509_SecTrustSettingsCopyCertificates SecTrustSettingsCopyCertificates "/System/Library/Frameworks/Security.framework/Versions/A/Security"

func SecTrustSettingsCopyCertificates(domain SecTrustSettingsDomain) (certArray CFRef, err error) {
ret := syscall(funcPC(x509_SecTrustSettingsCopyCertificates_trampoline), uintptr(domain),
uintptr(unsafe.Pointer(&certArray)), 0, 0, 0, 0)
if int32(ret) == errSecNoTrustSettings {
return 0, ErrNoTrustSettings
} else if ret != 0 {
return 0, OSStatus{"SecTrustSettingsCopyCertificates", int32(ret)}
}
return certArray, nil
}
func x509_SecTrustSettingsCopyCertificates_trampoline()

const kSecFormatX509Cert int32 = 9

//go:linkname x509_SecItemExport x509_SecItemExport
//go:cgo_import_dynamic x509_SecItemExport SecItemExport "/System/Library/Frameworks/Security.framework/Versions/A/Security"

func SecItemExport(cert CFRef) (data CFRef, err error) {
ret := syscall(funcPC(x509_SecItemExport_trampoline), uintptr(cert), uintptr(kSecFormatX509Cert),
0 /* flags */, 0 /* keyParams */, uintptr(unsafe.Pointer(&data)), 0)
if ret != 0 {
return 0, OSStatus{"SecItemExport", int32(ret)}
}
return data, nil
}
func x509_SecItemExport_trampoline()

const errSecItemNotFound = -25300

//go:linkname x509_SecTrustSettingsCopyTrustSettings x509_SecTrustSettingsCopyTrustSettings
//go:cgo_import_dynamic x509_SecTrustSettingsCopyTrustSettings SecTrustSettingsCopyTrustSettings "/System/Library/Frameworks/Security.framework/Versions/A/Security"

func SecTrustSettingsCopyTrustSettings(cert CFRef, domain SecTrustSettingsDomain) (trustSettings CFRef, err error) {
ret := syscall(funcPC(x509_SecTrustSettingsCopyTrustSettings_trampoline), uintptr(cert), uintptr(domain),
uintptr(unsafe.Pointer(&trustSettings)), 0, 0, 0)
if int32(ret) == errSecItemNotFound {
return 0, ErrNoTrustSettings
} else if ret != 0 {
return 0, OSStatus{"SecTrustSettingsCopyTrustSettings", int32(ret)}
}
return trustSettings, nil
}
func x509_SecTrustSettingsCopyTrustSettings_trampoline()

//go:linkname x509_SecPolicyCopyProperties x509_SecPolicyCopyProperties
//go:cgo_import_dynamic x509_SecPolicyCopyProperties SecPolicyCopyProperties "/System/Library/Frameworks/Security.framework/Versions/A/Security"

func SecPolicyCopyProperties(policy CFRef) CFRef {
ret := syscall(funcPC(x509_SecPolicyCopyProperties_trampoline), uintptr(policy), 0, 0, 0, 0, 0)
return CFRef(ret)
}
func x509_SecPolicyCopyProperties_trampoline()
16 changes: 16 additions & 0 deletions src/crypto/x509/internal/macOS/security.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2020 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.

// +build darwin,amd64

#include "textflag.h"

TEXT ·x509_SecTrustSettingsCopyCertificates_trampoline(SB),NOSPLIT,$0-0
JMP x509_SecTrustSettingsCopyCertificates(SB)
TEXT ·x509_SecItemExport_trampoline(SB),NOSPLIT,$0-0
JMP x509_SecItemExport(SB)
TEXT ·x509_SecTrustSettingsCopyTrustSettings_trampoline(SB),NOSPLIT,$0-0
JMP x509_SecTrustSettingsCopyTrustSettings(SB)
TEXT ·x509_SecPolicyCopyProperties_trampoline(SB),NOSPLIT,$0-0
JMP x509_SecPolicyCopyProperties(SB)
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build cgo,!arm64,!ios

package x509

// This cgo implementation exists only to support side-by-side testing by
// TestSystemRoots. It can be removed once we are confident in the no-cgo
// implementation.

/*
#cgo CFLAGS: -mmacosx-version-min=10.11
#cgo LDFLAGS: -framework CoreFoundation -framework Security
Expand Down Expand Up @@ -283,7 +285,11 @@ import (
"unsafe"
)

func loadSystemRoots() (*CertPool, error) {
func init() {
loadSystemRootsWithCgo = _loadSystemRootsWithCgo
}

func _loadSystemRootsWithCgo() (*CertPool, error) {
var data, untrustedData C.CFDataRef
err := C.CopyPEMRoots(&data, &untrustedData, C.bool(debugDarwinRoots))
if err == -1 {
Expand Down
Loading

1 comment on commit 6f52790

@jeremyschlatter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, if you are reading this and run macOS, please test this code:
...

It works on my machine:

$ gotip version
go version devel +d286e61 Mon Jun 15 23:29:23 2020 +0000 darwin/amd64

$ GODEBUG=x509roots=1 gotip test crypto/x509 -v -run TestSystemRoots
=== RUN   TestSystemRoots
crypto/x509: trust settings for CN=Caddy Local Authority - 2020 ECC Root: 1
    root_darwin_test.go:23: loadSystemRoots: 110.464947ms
crypto/x509: kSecTrustSettingsResultInvalid = 0
crypto/x509: kSecTrustSettingsResultTrustRoot = 1
crypto/x509: kSecTrustSettingsResultTrustAsRoot = 2
crypto/x509: kSecTrustSettingsResultDeny = 3
crypto/x509: kSecTrustSettingsResultUnspecified = 4
crypto/x509: Caddy Local Authority - 2020 ECC Root returned 1
    root_darwin_test.go:43: loadSystemRootsWithCgo: 97.262559ms
--- PASS: TestSystemRoots (0.21s)
PASS
ok  	crypto/x509	0.217s

Please sign in to comment.