mirror of
https://github.com/golang/go.git
synced 2026-06-27 03:11:23 +00:00
crypto/x509: honor SSL_CERT_{FILE,DIR} on windows/darwin
When these env vars are set, load roots from on-disk, and don't use the platform verifier. Fixes #77865 Change-Id: I3183d1636238bed924252fe1288cfa263d17f61d Reviewed-on: https://go-review.googlesource.com/c/go/+/776940 Reviewed-by: Neal Patel <nealpatel@google.com> Auto-Submit: Roland Shoemaker <roland@golang.org> Reviewed-by: Filippo Valsorda <filippo@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: David Chase <drchase@google.com>
This commit is contained in:
parent
93da30397d
commit
4bf23b51b8
12 changed files with 404 additions and 366 deletions
|
|
@ -170,6 +170,14 @@ to `1`. This opt-out is expected to be kept indefinitely in case goroutine
|
|||
labels acquire sensitive information that shouldn't be made available in
|
||||
tracebacks.
|
||||
|
||||
Go 1.27 added a new `x509sslcertoverrideplatform` setting that controls whether
|
||||
crypto/x509 will load roots from disk on Windows and Darwin when `SSL_CERT_FILE`
|
||||
or `SSL_CERT_DIR` are set. The default value `x509sslcertoverrideplatform=1` will
|
||||
cause roots to be loaded from disk when these environment variables are set.
|
||||
Setting `x509sslcertoverrideplatform=0` disables this behavior in favor of using
|
||||
the platform certificate store instead of honoring the environment variables. We
|
||||
plan to remove this setting in Go 1.31.
|
||||
|
||||
### Go 1.26
|
||||
|
||||
Go 1.26 added a new `httpcookiemaxnum` setting that controls the maximum number
|
||||
|
|
|
|||
5
doc/next/6-stdlib/99-minor/crypto/x509/77865.md
Normal file
5
doc/next/6-stdlib/99-minor/crypto/x509/77865.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[SystemCertPool] now respects SSL_CERT_FILE and SSL_CERT_DIR on Windows and
|
||||
Darwin. When these environment variables are set, roots are loaded from disk and
|
||||
instead of using the platform certificate verification APIs, the native Go
|
||||
verifier is used. This behavior can be disabled with
|
||||
`GODEBUG=x509sslcertoverrideplatform=0`.
|
||||
|
|
@ -105,10 +105,14 @@ func (s *CertPool) Clone() *CertPool {
|
|||
|
||||
// SystemCertPool returns a copy of the system cert pool.
|
||||
//
|
||||
// On Unix systems other than macOS the environment variables SSL_CERT_FILE and
|
||||
// SSL_CERT_DIR can be used to override the system default locations for the SSL
|
||||
// certificate file and SSL certificate files directory, respectively. The
|
||||
// latter can be a colon-separated list.
|
||||
// The environment variables SSL_CERT_FILE and SSL_CERT_DIR can be used to
|
||||
// override the system default locations for the SSL certificate file and SSL
|
||||
// certificate files directory, respectively. The latter can be a
|
||||
// colon-separated list, or a semicolon-separated list on Windows. On platforms
|
||||
// which have system APIs for certificate verification (macOS and Windows),
|
||||
// setting SSL_CERT_FILE or SSL_CERT_DIR will prevent those APIs from being
|
||||
// used, unless the x509sslcertoverrideplatform=0 GODEBUG setting is used. (This
|
||||
// changed in Go 1.27.)
|
||||
//
|
||||
// Any mutations to the returned pool are not written to disk and do not affect
|
||||
// any other pool returned by SystemCertPool.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ package x509
|
|||
|
||||
import (
|
||||
"internal/godebug"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
_ "unsafe" // for linkname
|
||||
)
|
||||
|
|
@ -115,3 +120,111 @@ func SetFallbackRoots(roots *CertPool) {
|
|||
|
||||
systemRoots, systemRootsErr = roots, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// certFileEnv is the environment variable which identifies where to locate
|
||||
// the SSL certificate file. If set this overrides the system default.
|
||||
certFileEnv = "SSL_CERT_FILE"
|
||||
|
||||
// certDirEnv is the environment variable which identifies which directory
|
||||
// to check for SSL certificate files. If set this overrides the system default.
|
||||
// See https://docs.openssl.org/4.0/man1/openssl-rehash/#environment.
|
||||
certDirEnv = "SSL_CERT_DIR"
|
||||
)
|
||||
|
||||
var x509sslcertoverrideplatform = godebug.New("x509sslcertoverrideplatform")
|
||||
|
||||
func loadSystemRoots() (*CertPool, error) {
|
||||
certFilePath, certDirPath := os.Getenv(certFileEnv), os.Getenv(certDirEnv)
|
||||
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
if certFilePath == "" && certDirPath == "" {
|
||||
return &CertPool{systemPool: true}, nil
|
||||
}
|
||||
if x509sslcertoverrideplatform.Value() == "0" {
|
||||
x509sslcertoverrideplatform.IncNonDefault()
|
||||
return &CertPool{systemPool: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return loadOnDiskRoots(certFilePath, certDirPath)
|
||||
}
|
||||
|
||||
func loadOnDiskRoots(certFilePath, certDirPath string) (*CertPool, error) {
|
||||
roots := NewCertPool()
|
||||
|
||||
files := certFiles
|
||||
if certFilePath != "" {
|
||||
files = []string{certFilePath}
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for _, file := range files {
|
||||
data, err := os.ReadFile(file)
|
||||
if err == nil {
|
||||
roots.AppendCertsFromPEM(data)
|
||||
break
|
||||
}
|
||||
if firstErr == nil && !os.IsNotExist(err) {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
|
||||
dirs := certDirectories
|
||||
if certDirPath != "" {
|
||||
// OpenSSL and BoringSSL both use ":" as the SSL_CERT_DIR separator on
|
||||
// Unix-like systems, and ";" on Windows.
|
||||
// See:
|
||||
// * https://golang.org/issue/35325
|
||||
// * https://docs.openssl.org/4.0/man1/openssl-rehash/#environment
|
||||
dirs = filepath.SplitList(certDirPath)
|
||||
}
|
||||
|
||||
for _, directory := range dirs {
|
||||
fis, err := readUniqueDirectoryEntries(directory)
|
||||
if err != nil {
|
||||
if firstErr == nil && !os.IsNotExist(err) {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, fi := range fis {
|
||||
data, err := os.ReadFile(filepath.Join(directory, fi.Name()))
|
||||
if err == nil {
|
||||
roots.AppendCertsFromPEM(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if roots.len() > 0 || firstErr == nil {
|
||||
return roots, nil
|
||||
}
|
||||
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
// readUniqueDirectoryEntries is like os.ReadDir but omits
|
||||
// symlinks that point within the directory.
|
||||
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uniq := files[:0]
|
||||
for _, f := range files {
|
||||
if !isSameDirSymlink(f, dir) {
|
||||
uniq = append(uniq, f)
|
||||
}
|
||||
}
|
||||
return uniq, nil
|
||||
}
|
||||
|
||||
// isSameDirSymlink reports whether f in dir is a symlink with a
|
||||
// target not containing a slash.
|
||||
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
||||
if f.Type()&fs.ModeSymlink == 0 {
|
||||
return false
|
||||
}
|
||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||
return err == nil && !strings.ContainsRune(target, filepath.Separator)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
// macOS has no default SSL_CERT_{FILE,DIR} paths.
|
||||
var certFiles, certDirectories []string
|
||||
|
||||
func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) {
|
||||
certs := macos.CFArrayCreateMutable()
|
||||
defer macos.ReleaseCFArray(certs)
|
||||
|
|
@ -125,7 +128,3 @@ func exportCertificate(cert macos.CFRef) (*Certificate, error) {
|
|||
}
|
||||
return ParseCertificate(data)
|
||||
}
|
||||
|
||||
func loadSystemRoots() (*CertPool, error) {
|
||||
return &CertPool{systemPool: true}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,34 +6,13 @@
|
|||
|
||||
package x509
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Possible certificate files; stop after finding one.
|
||||
var certFiles = []string{
|
||||
"/sys/lib/tls/ca.pem",
|
||||
}
|
||||
|
||||
var certDirectories = []string{}
|
||||
|
||||
func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func loadSystemRoots() (*CertPool, error) {
|
||||
roots := NewCertPool()
|
||||
var bestErr error
|
||||
for _, file := range certFiles {
|
||||
data, err := os.ReadFile(file)
|
||||
if err == nil {
|
||||
roots.AppendCertsFromPEM(data)
|
||||
return roots, nil
|
||||
}
|
||||
if bestErr == nil || (os.IsNotExist(bestErr) && !os.IsNotExist(err)) {
|
||||
bestErr = err
|
||||
}
|
||||
}
|
||||
if bestErr == nil {
|
||||
return roots, nil
|
||||
}
|
||||
return nil, bestErr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@
|
|||
package x509
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"internal/testenv"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -108,3 +116,252 @@ func TestFallback(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
testDirCN = "test-dir"
|
||||
testFile = "test-file.crt"
|
||||
testFileCN = "test-file"
|
||||
testMissing = "missing"
|
||||
)
|
||||
|
||||
func TestEnvVars(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testCert, err := os.ReadFile("testdata/test-dir.crt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read test cert: %s", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, testFile), testCert, 0644); err != nil {
|
||||
t.Fatalf("failed to write test cert: %s", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
fileEnv string
|
||||
dirEnv string
|
||||
files []string
|
||||
dirs []string
|
||||
cns []string
|
||||
}{
|
||||
{
|
||||
// Environment variables override the default locations preventing fall through.
|
||||
name: "override-defaults",
|
||||
fileEnv: testMissing,
|
||||
dirEnv: testMissing,
|
||||
files: []string{testFile},
|
||||
dirs: []string{tmpDir},
|
||||
cns: nil,
|
||||
},
|
||||
{
|
||||
// File environment overrides default file locations.
|
||||
name: "file",
|
||||
fileEnv: testFile,
|
||||
dirEnv: "",
|
||||
files: nil,
|
||||
dirs: nil,
|
||||
cns: []string{testFileCN},
|
||||
},
|
||||
{
|
||||
// Directory environment overrides default directory locations.
|
||||
name: "dir",
|
||||
fileEnv: "",
|
||||
dirEnv: tmpDir,
|
||||
files: nil,
|
||||
dirs: nil,
|
||||
cns: []string{testDirCN},
|
||||
},
|
||||
{
|
||||
// File & directory environment overrides both default locations.
|
||||
name: "file+dir",
|
||||
fileEnv: testFile,
|
||||
dirEnv: tmpDir,
|
||||
files: nil,
|
||||
dirs: nil,
|
||||
cns: []string{testFileCN, testDirCN},
|
||||
},
|
||||
{
|
||||
// Environment variable empty / unset uses default locations.
|
||||
name: "empty-fall-through",
|
||||
fileEnv: "",
|
||||
dirEnv: "",
|
||||
files: []string{testFile},
|
||||
dirs: []string{tmpDir},
|
||||
cns: []string{testFileCN, testDirCN},
|
||||
},
|
||||
}
|
||||
|
||||
// Save old settings so we can restore before the test ends.
|
||||
origCertFiles, origCertDirectories := certFiles, certDirectories
|
||||
origFile, origDir := os.Getenv(certFileEnv), os.Getenv(certDirEnv)
|
||||
defer func() {
|
||||
certFiles = origCertFiles
|
||||
certDirectories = origCertDirectories
|
||||
os.Setenv(certFileEnv, origFile)
|
||||
os.Setenv(certDirEnv, origDir)
|
||||
}()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := os.Setenv(certFileEnv, tc.fileEnv); err != nil {
|
||||
t.Fatalf("setenv %q failed: %v", certFileEnv, err)
|
||||
}
|
||||
if err := os.Setenv(certDirEnv, tc.dirEnv); err != nil {
|
||||
t.Fatalf("setenv %q failed: %v", certDirEnv, err)
|
||||
}
|
||||
|
||||
certFiles, certDirectories = tc.files, tc.dirs
|
||||
|
||||
r, err := loadSystemRoots()
|
||||
if err != nil {
|
||||
t.Fatal("unexpected failure:", err)
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
t.Fatal("nil roots")
|
||||
}
|
||||
|
||||
wantSystemPool := (runtime.GOOS == "darwin" || runtime.GOOS == "windows") && tc.dirEnv == "" && tc.fileEnv == ""
|
||||
|
||||
if wantSystemPool {
|
||||
if !r.systemPool {
|
||||
t.Fatal("expected returned cert pool to be a system pool")
|
||||
}
|
||||
if r.len() != 0 {
|
||||
t.Fatalf("expected empty system pool, pool has %d roots", r.len())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Verify that the returned certs match, otherwise report where the mismatch is.
|
||||
for i, cn := range tc.cns {
|
||||
if i >= r.len() {
|
||||
t.Errorf("missing cert %v @ %v", cn, i)
|
||||
} else if r.mustCert(t, i).Subject.CommonName != cn {
|
||||
fmt.Printf("%#v\n", r.mustCert(t, 0).Subject)
|
||||
t.Errorf("unexpected cert common name %q, want %q", r.mustCert(t, i).Subject.CommonName, cn)
|
||||
}
|
||||
}
|
||||
if r.len() > len(tc.cns) {
|
||||
t.Errorf("got %v certs, which is more than %v wanted", r.len(), len(tc.cns))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that "SSL_CERT_DIR" when used as the environment variable delimited by
|
||||
// colons on Unix-like systems, and semicolons on Windows, allows
|
||||
// loadSystemRoots to load all the roots from the respective directories.
|
||||
// See https://golang.org/issue/35325.
|
||||
func TestLoadSystemCertsLoadColonSeparatedDirs(t *testing.T) {
|
||||
origFile, origDir := os.Getenv(certFileEnv), os.Getenv(certDirEnv)
|
||||
origCertFiles := certFiles[:]
|
||||
|
||||
// To prevent any other certs from being loaded in
|
||||
// through "SSL_CERT_FILE" or from known "certFiles",
|
||||
// clear them all, and they'll be reverted on defer.
|
||||
certFiles = certFiles[:0]
|
||||
os.Setenv(certFileEnv, "")
|
||||
|
||||
defer func() {
|
||||
certFiles = origCertFiles[:]
|
||||
os.Setenv(certDirEnv, origDir)
|
||||
os.Setenv(certFileEnv, origFile)
|
||||
}()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
rootPEMs := []string{
|
||||
gtsRoot,
|
||||
googleLeaf,
|
||||
}
|
||||
|
||||
var certDirs []string
|
||||
for i, certPEM := range rootPEMs {
|
||||
certDir := filepath.Join(tmpDir, fmt.Sprintf("cert-%d", i))
|
||||
if err := os.MkdirAll(certDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create certificate dir: %v", err)
|
||||
}
|
||||
certOutFile := filepath.Join(certDir, "cert.crt")
|
||||
if err := os.WriteFile(certOutFile, []byte(certPEM), 0655); err != nil {
|
||||
t.Fatalf("failed to write certificate to file: %v", err)
|
||||
}
|
||||
certDirs = append(certDirs, certDir)
|
||||
}
|
||||
|
||||
// Sanity check: the number of certDirs should be equal to the number of roots.
|
||||
if g, w := len(certDirs), len(rootPEMs); g != w {
|
||||
t.Fatalf("failed sanity check: len(certsDir)=%d is not equal to len(rootsPEMS)=%d", g, w)
|
||||
}
|
||||
|
||||
// Now finally concatenate them with a colon/semicolon.
|
||||
concatCertDirs := strings.Join(certDirs, string(filepath.ListSeparator))
|
||||
os.Setenv(certDirEnv, concatCertDirs)
|
||||
gotPool, err := loadSystemRoots()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load system roots: %v", err)
|
||||
}
|
||||
subjects := gotPool.Subjects()
|
||||
// We expect exactly len(rootPEMs) subjects back.
|
||||
if g, w := len(subjects), len(rootPEMs); g != w {
|
||||
t.Fatalf("invalid number of subjects: got %d want %d", g, w)
|
||||
}
|
||||
|
||||
wantPool := NewCertPool()
|
||||
for _, certPEM := range rootPEMs {
|
||||
wantPool.AppendCertsFromPEM([]byte(certPEM))
|
||||
}
|
||||
strCertPool := func(p *CertPool) string {
|
||||
return string(bytes.Join(p.Subjects(), []byte("\n")))
|
||||
}
|
||||
|
||||
if !certPoolEqual(gotPool, wantPool) {
|
||||
got, want := strCertPool(gotPool), strCertPool(wantPool)
|
||||
t.Fatalf("mismatched certPools\nGot:\n%s\n\nWant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadUniqueDirectoryEntries(t *testing.T) {
|
||||
baseTmpDir := t.TempDir()
|
||||
path := func(base string) string { return filepath.Join(baseTmpDir, base) }
|
||||
if f, err := os.Create(path("file")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
f.Close()
|
||||
}
|
||||
if err := os.Symlink("target-in", path("link-in")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("../target-out", path("link-out")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := readUniqueDirectoryEntries(baseTmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotNames := []string{}
|
||||
for _, fi := range got {
|
||||
gotNames = append(gotNames, fi.Name())
|
||||
}
|
||||
wantNames := []string{"file", "link-out"}
|
||||
if !slices.Equal(gotNames, wantNames) {
|
||||
t.Errorf("got %q; want %q", gotNames, wantNames)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSLCertEnvOverride(t *testing.T) {
|
||||
testenv.SetGODEBUG(t, "x509sslcertoverrideplatform=0")
|
||||
t.Setenv(certFileEnv, "/tmp/nope")
|
||||
t.Setenv(certDirEnv, "/tmp/nope")
|
||||
|
||||
p, err := loadSystemRoots()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected failure: %s", err)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
if !p.systemPool {
|
||||
t.Fatal("x509sslcertoverrideplatform did not override SSL_CERT_{FILE,DIR}")
|
||||
}
|
||||
} else if p.systemPool {
|
||||
t.Fatal("x509sslcertoverrideplatform caused a systemPool to be returned on OS other than windows or darwin")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,103 +6,6 @@
|
|||
|
||||
package x509
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// certFileEnv is the environment variable which identifies where to locate
|
||||
// the SSL certificate file. If set this overrides the system default.
|
||||
certFileEnv = "SSL_CERT_FILE"
|
||||
|
||||
// certDirEnv is the environment variable which identifies which directory
|
||||
// to check for SSL certificate files. If set this overrides the system default.
|
||||
// It is a colon separated list of directories.
|
||||
// See https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html.
|
||||
certDirEnv = "SSL_CERT_DIR"
|
||||
)
|
||||
|
||||
func (c *Certificate) systemVerify(opts *VerifyOptions) (chains [][]*Certificate, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func loadSystemRoots() (*CertPool, error) {
|
||||
roots := NewCertPool()
|
||||
|
||||
files := certFiles
|
||||
if f := os.Getenv(certFileEnv); f != "" {
|
||||
files = []string{f}
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for _, file := range files {
|
||||
data, err := os.ReadFile(file)
|
||||
if err == nil {
|
||||
roots.AppendCertsFromPEM(data)
|
||||
break
|
||||
}
|
||||
if firstErr == nil && !os.IsNotExist(err) {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
|
||||
dirs := certDirectories
|
||||
if d := os.Getenv(certDirEnv); d != "" {
|
||||
// OpenSSL and BoringSSL both use ":" as the SSL_CERT_DIR separator.
|
||||
// See:
|
||||
// * https://golang.org/issue/35325
|
||||
// * https://www.openssl.org/docs/man1.0.2/man1/c_rehash.html
|
||||
dirs = strings.Split(d, ":")
|
||||
}
|
||||
|
||||
for _, directory := range dirs {
|
||||
fis, err := readUniqueDirectoryEntries(directory)
|
||||
if err != nil {
|
||||
if firstErr == nil && !os.IsNotExist(err) {
|
||||
firstErr = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, fi := range fis {
|
||||
data, err := os.ReadFile(directory + "/" + fi.Name())
|
||||
if err == nil {
|
||||
roots.AppendCertsFromPEM(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if roots.len() > 0 || firstErr == nil {
|
||||
return roots, nil
|
||||
}
|
||||
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
// readUniqueDirectoryEntries is like os.ReadDir but omits
|
||||
// symlinks that point within the directory.
|
||||
func readUniqueDirectoryEntries(dir string) ([]fs.DirEntry, error) {
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uniq := files[:0]
|
||||
for _, f := range files {
|
||||
if !isSameDirSymlink(f, dir) {
|
||||
uniq = append(uniq, f)
|
||||
}
|
||||
}
|
||||
return uniq, nil
|
||||
}
|
||||
|
||||
// isSameDirSymlink reports whether fi in dir is a symlink with a
|
||||
// target not containing a slash.
|
||||
func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
||||
if f.Type()&fs.ModeSymlink == 0 {
|
||||
return false
|
||||
}
|
||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||
return err == nil && !strings.Contains(target, "/")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,235 +0,0 @@
|
|||
// Copyright 2017 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:build dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
|
||||
package x509
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
testDirCN = "test-dir"
|
||||
testFile = "test-file.crt"
|
||||
testFileCN = "test-file"
|
||||
testMissing = "missing"
|
||||
)
|
||||
|
||||
func TestEnvVars(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testCert, err := os.ReadFile("testdata/test-dir.crt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read test cert: %s", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, testFile), testCert, 0644); err != nil {
|
||||
t.Fatalf("failed to write test cert: %s", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
fileEnv string
|
||||
dirEnv string
|
||||
files []string
|
||||
dirs []string
|
||||
cns []string
|
||||
}{
|
||||
{
|
||||
// Environment variables override the default locations preventing fall through.
|
||||
name: "override-defaults",
|
||||
fileEnv: testMissing,
|
||||
dirEnv: testMissing,
|
||||
files: []string{testFile},
|
||||
dirs: []string{tmpDir},
|
||||
cns: nil,
|
||||
},
|
||||
{
|
||||
// File environment overrides default file locations.
|
||||
name: "file",
|
||||
fileEnv: testFile,
|
||||
dirEnv: "",
|
||||
files: nil,
|
||||
dirs: nil,
|
||||
cns: []string{testFileCN},
|
||||
},
|
||||
{
|
||||
// Directory environment overrides default directory locations.
|
||||
name: "dir",
|
||||
fileEnv: "",
|
||||
dirEnv: tmpDir,
|
||||
files: nil,
|
||||
dirs: nil,
|
||||
cns: []string{testDirCN},
|
||||
},
|
||||
{
|
||||
// File & directory environment overrides both default locations.
|
||||
name: "file+dir",
|
||||
fileEnv: testFile,
|
||||
dirEnv: tmpDir,
|
||||
files: nil,
|
||||
dirs: nil,
|
||||
cns: []string{testFileCN, testDirCN},
|
||||
},
|
||||
{
|
||||
// Environment variable empty / unset uses default locations.
|
||||
name: "empty-fall-through",
|
||||
fileEnv: "",
|
||||
dirEnv: "",
|
||||
files: []string{testFile},
|
||||
dirs: []string{tmpDir},
|
||||
cns: []string{testFileCN, testDirCN},
|
||||
},
|
||||
}
|
||||
|
||||
// Save old settings so we can restore before the test ends.
|
||||
origCertFiles, origCertDirectories := certFiles, certDirectories
|
||||
origFile, origDir := os.Getenv(certFileEnv), os.Getenv(certDirEnv)
|
||||
defer func() {
|
||||
certFiles = origCertFiles
|
||||
certDirectories = origCertDirectories
|
||||
os.Setenv(certFileEnv, origFile)
|
||||
os.Setenv(certDirEnv, origDir)
|
||||
}()
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := os.Setenv(certFileEnv, tc.fileEnv); err != nil {
|
||||
t.Fatalf("setenv %q failed: %v", certFileEnv, err)
|
||||
}
|
||||
if err := os.Setenv(certDirEnv, tc.dirEnv); err != nil {
|
||||
t.Fatalf("setenv %q failed: %v", certDirEnv, err)
|
||||
}
|
||||
|
||||
certFiles, certDirectories = tc.files, tc.dirs
|
||||
|
||||
r, err := loadSystemRoots()
|
||||
if err != nil {
|
||||
t.Fatal("unexpected failure:", err)
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
t.Fatal("nil roots")
|
||||
}
|
||||
|
||||
// Verify that the returned certs match, otherwise report where the mismatch is.
|
||||
for i, cn := range tc.cns {
|
||||
if i >= r.len() {
|
||||
t.Errorf("missing cert %v @ %v", cn, i)
|
||||
} else if r.mustCert(t, i).Subject.CommonName != cn {
|
||||
fmt.Printf("%#v\n", r.mustCert(t, 0).Subject)
|
||||
t.Errorf("unexpected cert common name %q, want %q", r.mustCert(t, i).Subject.CommonName, cn)
|
||||
}
|
||||
}
|
||||
if r.len() > len(tc.cns) {
|
||||
t.Errorf("got %v certs, which is more than %v wanted", r.len(), len(tc.cns))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that "SSL_CERT_DIR" when used as the environment
|
||||
// variable delimited by colons, allows loadSystemRoots to
|
||||
// load all the roots from the respective directories.
|
||||
// See https://golang.org/issue/35325.
|
||||
func TestLoadSystemCertsLoadColonSeparatedDirs(t *testing.T) {
|
||||
origFile, origDir := os.Getenv(certFileEnv), os.Getenv(certDirEnv)
|
||||
origCertFiles := certFiles[:]
|
||||
|
||||
// To prevent any other certs from being loaded in
|
||||
// through "SSL_CERT_FILE" or from known "certFiles",
|
||||
// clear them all, and they'll be reverting on defer.
|
||||
certFiles = certFiles[:0]
|
||||
os.Setenv(certFileEnv, "")
|
||||
|
||||
defer func() {
|
||||
certFiles = origCertFiles[:]
|
||||
os.Setenv(certDirEnv, origDir)
|
||||
os.Setenv(certFileEnv, origFile)
|
||||
}()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
rootPEMs := []string{
|
||||
gtsRoot,
|
||||
googleLeaf,
|
||||
}
|
||||
|
||||
var certDirs []string
|
||||
for i, certPEM := range rootPEMs {
|
||||
certDir := filepath.Join(tmpDir, fmt.Sprintf("cert-%d", i))
|
||||
if err := os.MkdirAll(certDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create certificate dir: %v", err)
|
||||
}
|
||||
certOutFile := filepath.Join(certDir, "cert.crt")
|
||||
if err := os.WriteFile(certOutFile, []byte(certPEM), 0655); err != nil {
|
||||
t.Fatalf("Failed to write certificate to file: %v", err)
|
||||
}
|
||||
certDirs = append(certDirs, certDir)
|
||||
}
|
||||
|
||||
// Sanity check: the number of certDirs should be equal to the number of roots.
|
||||
if g, w := len(certDirs), len(rootPEMs); g != w {
|
||||
t.Fatalf("Failed sanity check: len(certsDir)=%d is not equal to len(rootsPEMS)=%d", g, w)
|
||||
}
|
||||
|
||||
// Now finally concatenate them with a colon.
|
||||
colonConcatCertDirs := strings.Join(certDirs, ":")
|
||||
os.Setenv(certDirEnv, colonConcatCertDirs)
|
||||
gotPool, err := loadSystemRoots()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load system roots: %v", err)
|
||||
}
|
||||
subjects := gotPool.Subjects()
|
||||
// We expect exactly len(rootPEMs) subjects back.
|
||||
if g, w := len(subjects), len(rootPEMs); g != w {
|
||||
t.Fatalf("Invalid number of subjects: got %d want %d", g, w)
|
||||
}
|
||||
|
||||
wantPool := NewCertPool()
|
||||
for _, certPEM := range rootPEMs {
|
||||
wantPool.AppendCertsFromPEM([]byte(certPEM))
|
||||
}
|
||||
strCertPool := func(p *CertPool) string {
|
||||
return string(bytes.Join(p.Subjects(), []byte("\n")))
|
||||
}
|
||||
|
||||
if !certPoolEqual(gotPool, wantPool) {
|
||||
g, w := strCertPool(gotPool), strCertPool(wantPool)
|
||||
t.Fatalf("Mismatched certPools\nGot:\n%s\n\nWant:\n%s", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadUniqueDirectoryEntries(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
temp := func(base string) string { return filepath.Join(tmp, base) }
|
||||
if f, err := os.Create(temp("file")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
f.Close()
|
||||
}
|
||||
if err := os.Symlink("target-in", temp("link-in")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("../target-out", temp("link-out")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := readUniqueDirectoryEntries(tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotNames := []string{}
|
||||
for _, fi := range got {
|
||||
gotNames = append(gotNames, fi.Name())
|
||||
}
|
||||
wantNames := []string{"file", "link-out"}
|
||||
if !slices.Equal(gotNames, wantNames) {
|
||||
t.Errorf("got %q; want %q", gotNames, wantNames)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,9 +12,8 @@ import (
|
|||
"unsafe"
|
||||
)
|
||||
|
||||
func loadSystemRoots() (*CertPool, error) {
|
||||
return &CertPool{systemPool: true}, nil
|
||||
}
|
||||
// Windows has no default SSL_CERT_{FILE,DIR} paths.
|
||||
var certFiles, certDirectories []string
|
||||
|
||||
// Creates a new *syscall.CertContext representing the leaf certificate in an in-memory
|
||||
// certificate store containing itself and all of the intermediate certificates specified
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ var All = []Info{
|
|||
{Name: "x509negativeserial", Package: "crypto/x509", Changed: 23, Old: "1"},
|
||||
{Name: "x509rsacrt", Package: "crypto/x509", Changed: 24, Old: "0"},
|
||||
{Name: "x509sha256skid", Package: "crypto/x509", Changed: 25, Old: "0"},
|
||||
{Name: "x509sslcertoverrideplatform", Package: "crypto/x509", Changed: 27, Old: "0"},
|
||||
{Name: "x509usefallbackroots", Package: "crypto/x509"},
|
||||
{Name: "x509usepolicies", Package: "crypto/x509", Changed: 24, Old: "0"},
|
||||
{Name: "zipinsecurepath", Package: "archive/zip"},
|
||||
|
|
|
|||
|
|
@ -441,6 +441,11 @@ Below is the full list of supported metrics, ordered lexicographically.
|
|||
The number of non-default behaviors executed by the crypto/x509
|
||||
package due to a non-default GODEBUG=x509sha256skid=... setting.
|
||||
|
||||
/godebug/non-default-behavior/x509sslcertoverrideplatform:events
|
||||
The number of non-default behaviors executed by
|
||||
the crypto/x509 package due to a non-default
|
||||
GODEBUG=x509sslcertoverrideplatform=... setting.
|
||||
|
||||
/godebug/non-default-behavior/x509usefallbackroots:events
|
||||
The number of non-default behaviors executed by the crypto/x509
|
||||
package due to a non-default GODEBUG=x509usefallbackroots=...
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue