Source file src/crypto/x509/root_test.go

     1  // Copyright 2022 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  package x509
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"internal/testenv"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"slices"
    15  	"strings"
    16  	"testing"
    17  )
    18  
    19  func TestFallbackPanic(t *testing.T) {
    20  	defer func() {
    21  		if recover() == nil {
    22  			t.Fatal("Multiple calls to SetFallbackRoots should panic")
    23  		}
    24  	}()
    25  	SetFallbackRoots(nil)
    26  	SetFallbackRoots(nil)
    27  }
    28  
    29  func TestFallback(t *testing.T) {
    30  	// call systemRootsPool so that the sync.Once is triggered, and we can
    31  	// manipulate systemRoots without worrying about our working being overwritten
    32  	systemRootsPool()
    33  	if systemRoots != nil {
    34  		originalSystemRoots := *systemRoots
    35  		defer func() { systemRoots = &originalSystemRoots }()
    36  	}
    37  
    38  	tests := []struct {
    39  		name            string
    40  		systemRoots     *CertPool
    41  		systemPool      bool
    42  		poolContent     []*Certificate
    43  		forceFallback   bool
    44  		returnsFallback bool
    45  	}{
    46  		{
    47  			name:            "nil systemRoots",
    48  			returnsFallback: true,
    49  		},
    50  		{
    51  			name:            "empty systemRoots",
    52  			systemRoots:     NewCertPool(),
    53  			returnsFallback: true,
    54  		},
    55  		{
    56  			name:        "empty systemRoots system pool",
    57  			systemRoots: NewCertPool(),
    58  			systemPool:  true,
    59  		},
    60  		{
    61  			name:        "filled systemRoots system pool",
    62  			systemRoots: NewCertPool(),
    63  			poolContent: []*Certificate{{}},
    64  			systemPool:  true,
    65  		},
    66  		{
    67  			name:        "filled systemRoots",
    68  			systemRoots: NewCertPool(),
    69  			poolContent: []*Certificate{{}},
    70  		},
    71  		{
    72  			name:            "filled systemRoots, force fallback",
    73  			systemRoots:     NewCertPool(),
    74  			poolContent:     []*Certificate{{}},
    75  			forceFallback:   true,
    76  			returnsFallback: true,
    77  		},
    78  		{
    79  			name:            "filled systemRoot system pool, force fallback",
    80  			systemRoots:     NewCertPool(),
    81  			poolContent:     []*Certificate{{}},
    82  			systemPool:      true,
    83  			forceFallback:   true,
    84  			returnsFallback: true,
    85  		},
    86  	}
    87  
    88  	for _, tc := range tests {
    89  		t.Run(tc.name, func(t *testing.T) {
    90  			useFallbackRoots = false
    91  			fallbacksSet = false
    92  			systemRoots = tc.systemRoots
    93  
    94  			if systemRoots != nil {
    95  				systemRoots.systemPool = tc.systemPool
    96  			}
    97  			for _, c := range tc.poolContent {
    98  				systemRoots.AddCert(c)
    99  			}
   100  			if tc.forceFallback {
   101  				t.Setenv("GODEBUG", "x509usefallbackroots=1")
   102  			} else {
   103  				t.Setenv("GODEBUG", "x509usefallbackroots=0")
   104  			}
   105  
   106  			fallbackPool := NewCertPool()
   107  			SetFallbackRoots(fallbackPool)
   108  
   109  			systemPoolIsFallback := systemRoots == fallbackPool
   110  
   111  			if tc.returnsFallback && !systemPoolIsFallback {
   112  				t.Error("systemRoots was not set to fallback pool")
   113  			} else if !tc.returnsFallback && systemPoolIsFallback {
   114  				t.Error("systemRoots was set to fallback pool when it shouldn't have been")
   115  			}
   116  		})
   117  	}
   118  }
   119  
   120  const (
   121  	testDirCN   = "test-dir"
   122  	testFile    = "test-file.crt"
   123  	testFileCN  = "test-file"
   124  	testMissing = "missing"
   125  )
   126  
   127  func TestEnvVars(t *testing.T) {
   128  	tmpDir := t.TempDir()
   129  	testCert, err := os.ReadFile("testdata/test-dir.crt")
   130  	if err != nil {
   131  		t.Fatalf("failed to read test cert: %s", err)
   132  	}
   133  	if err := os.WriteFile(filepath.Join(tmpDir, testFile), testCert, 0644); err != nil {
   134  		t.Fatalf("failed to write test cert: %s", err)
   135  	}
   136  
   137  	testCases := []struct {
   138  		name    string
   139  		fileEnv string
   140  		dirEnv  string
   141  		files   []string
   142  		dirs    []string
   143  		cns     []string
   144  	}{
   145  		{
   146  			// Environment variables override the default locations preventing fall through.
   147  			name:    "override-defaults",
   148  			fileEnv: testMissing,
   149  			dirEnv:  testMissing,
   150  			files:   []string{testFile},
   151  			dirs:    []string{tmpDir},
   152  			cns:     nil,
   153  		},
   154  		{
   155  			// File environment overrides default file locations.
   156  			name:    "file",
   157  			fileEnv: testFile,
   158  			dirEnv:  "",
   159  			files:   nil,
   160  			dirs:    nil,
   161  			cns:     []string{testFileCN},
   162  		},
   163  		{
   164  			// Directory environment overrides default directory locations.
   165  			name:    "dir",
   166  			fileEnv: "",
   167  			dirEnv:  tmpDir,
   168  			files:   nil,
   169  			dirs:    nil,
   170  			cns:     []string{testDirCN},
   171  		},
   172  		{
   173  			// File & directory environment overrides both default locations.
   174  			name:    "file+dir",
   175  			fileEnv: testFile,
   176  			dirEnv:  tmpDir,
   177  			files:   nil,
   178  			dirs:    nil,
   179  			cns:     []string{testFileCN, testDirCN},
   180  		},
   181  		{
   182  			// Environment variable empty / unset uses default locations.
   183  			name:    "empty-fall-through",
   184  			fileEnv: "",
   185  			dirEnv:  "",
   186  			files:   []string{testFile},
   187  			dirs:    []string{tmpDir},
   188  			cns:     []string{testFileCN, testDirCN},
   189  		},
   190  	}
   191  
   192  	// Save old settings so we can restore before the test ends.
   193  	origCertFiles, origCertDirectories := certFiles, certDirectories
   194  	origFile, origDir := os.Getenv(certFileEnv), os.Getenv(certDirEnv)
   195  	defer func() {
   196  		certFiles = origCertFiles
   197  		certDirectories = origCertDirectories
   198  		os.Setenv(certFileEnv, origFile)
   199  		os.Setenv(certDirEnv, origDir)
   200  	}()
   201  
   202  	for _, tc := range testCases {
   203  		t.Run(tc.name, func(t *testing.T) {
   204  			if err := os.Setenv(certFileEnv, tc.fileEnv); err != nil {
   205  				t.Fatalf("setenv %q failed: %v", certFileEnv, err)
   206  			}
   207  			if err := os.Setenv(certDirEnv, tc.dirEnv); err != nil {
   208  				t.Fatalf("setenv %q failed: %v", certDirEnv, err)
   209  			}
   210  
   211  			certFiles, certDirectories = tc.files, tc.dirs
   212  
   213  			r, err := loadSystemRoots()
   214  			if err != nil {
   215  				t.Fatal("unexpected failure:", err)
   216  			}
   217  
   218  			if r == nil {
   219  				t.Fatal("nil roots")
   220  			}
   221  
   222  			wantSystemPool := (runtime.GOOS == "darwin" || runtime.GOOS == "windows") && tc.dirEnv == "" && tc.fileEnv == ""
   223  
   224  			if wantSystemPool {
   225  				if !r.systemPool {
   226  					t.Fatal("expected returned cert pool to be a system pool")
   227  				}
   228  				if r.len() != 0 {
   229  					t.Fatalf("expected empty system pool, pool has %d roots", r.len())
   230  				}
   231  				return
   232  			}
   233  
   234  			// Verify that the returned certs match, otherwise report where the mismatch is.
   235  			for i, cn := range tc.cns {
   236  				if i >= r.len() {
   237  					t.Errorf("missing cert %v @ %v", cn, i)
   238  				} else if r.mustCert(t, i).Subject.CommonName != cn {
   239  					fmt.Printf("%#v\n", r.mustCert(t, 0).Subject)
   240  					t.Errorf("unexpected cert common name %q, want %q", r.mustCert(t, i).Subject.CommonName, cn)
   241  				}
   242  			}
   243  			if r.len() > len(tc.cns) {
   244  				t.Errorf("got %v certs, which is more than %v wanted", r.len(), len(tc.cns))
   245  			}
   246  		})
   247  	}
   248  }
   249  
   250  // Ensure that "SSL_CERT_DIR" when used as the environment variable delimited by
   251  // colons on Unix-like systems, and semicolons on Windows, allows
   252  // loadSystemRoots to load all the roots from the respective directories.
   253  // See https://golang.org/issue/35325.
   254  func TestLoadSystemCertsLoadColonSeparatedDirs(t *testing.T) {
   255  	origFile, origDir := os.Getenv(certFileEnv), os.Getenv(certDirEnv)
   256  	origCertFiles := certFiles[:]
   257  
   258  	// To prevent any other certs from being loaded in
   259  	// through "SSL_CERT_FILE" or from known "certFiles",
   260  	// clear them all, and they'll be reverted on defer.
   261  	certFiles = certFiles[:0]
   262  	os.Setenv(certFileEnv, "")
   263  
   264  	defer func() {
   265  		certFiles = origCertFiles[:]
   266  		os.Setenv(certDirEnv, origDir)
   267  		os.Setenv(certFileEnv, origFile)
   268  	}()
   269  
   270  	tmpDir := t.TempDir()
   271  
   272  	rootPEMs := []string{
   273  		gtsRoot,
   274  		googleLeaf,
   275  	}
   276  
   277  	var certDirs []string
   278  	for i, certPEM := range rootPEMs {
   279  		certDir := filepath.Join(tmpDir, fmt.Sprintf("cert-%d", i))
   280  		if err := os.MkdirAll(certDir, 0755); err != nil {
   281  			t.Fatalf("failed to create certificate dir: %v", err)
   282  		}
   283  		certOutFile := filepath.Join(certDir, "cert.crt")
   284  		if err := os.WriteFile(certOutFile, []byte(certPEM), 0655); err != nil {
   285  			t.Fatalf("failed to write certificate to file: %v", err)
   286  		}
   287  		certDirs = append(certDirs, certDir)
   288  	}
   289  
   290  	// Sanity check: the number of certDirs should be equal to the number of roots.
   291  	if g, w := len(certDirs), len(rootPEMs); g != w {
   292  		t.Fatalf("failed sanity check: len(certsDir)=%d is not equal to len(rootsPEMS)=%d", g, w)
   293  	}
   294  
   295  	// Now finally concatenate them with a colon/semicolon.
   296  	concatCertDirs := strings.Join(certDirs, string(filepath.ListSeparator))
   297  	os.Setenv(certDirEnv, concatCertDirs)
   298  	gotPool, err := loadSystemRoots()
   299  	if err != nil {
   300  		t.Fatalf("failed to load system roots: %v", err)
   301  	}
   302  	subjects := gotPool.Subjects()
   303  	// We expect exactly len(rootPEMs) subjects back.
   304  	if g, w := len(subjects), len(rootPEMs); g != w {
   305  		t.Fatalf("invalid number of subjects: got %d want %d", g, w)
   306  	}
   307  
   308  	wantPool := NewCertPool()
   309  	for _, certPEM := range rootPEMs {
   310  		wantPool.AppendCertsFromPEM([]byte(certPEM))
   311  	}
   312  	strCertPool := func(p *CertPool) string {
   313  		return string(bytes.Join(p.Subjects(), []byte("\n")))
   314  	}
   315  
   316  	if !certPoolEqual(gotPool, wantPool) {
   317  		got, want := strCertPool(gotPool), strCertPool(wantPool)
   318  		t.Fatalf("mismatched certPools\nGot:\n%s\n\nWant:\n%s", got, want)
   319  	}
   320  }
   321  
   322  func TestReadUniqueDirectoryEntries(t *testing.T) {
   323  	testenv.MustHaveSymlink(t)
   324  	baseTmpDir := t.TempDir()
   325  	path := func(base string) string { return filepath.Join(baseTmpDir, base) }
   326  	if f, err := os.Create(path("file")); err != nil {
   327  		t.Fatal(err)
   328  	} else {
   329  		f.Close()
   330  	}
   331  	if err := os.Symlink("target-in", path("link-in")); err != nil {
   332  		t.Fatal(err)
   333  	}
   334  	if err := os.Symlink("../target-out", path("link-out")); err != nil {
   335  		t.Fatal(err)
   336  	}
   337  	got, err := readUniqueDirectoryEntries(baseTmpDir)
   338  	if err != nil {
   339  		t.Fatal(err)
   340  	}
   341  	gotNames := []string{}
   342  	for _, fi := range got {
   343  		gotNames = append(gotNames, fi.Name())
   344  	}
   345  	wantNames := []string{"file", "link-out"}
   346  	if !slices.Equal(gotNames, wantNames) {
   347  		t.Errorf("got %q; want %q", gotNames, wantNames)
   348  	}
   349  }
   350  
   351  func TestSSLCertEnvOverride(t *testing.T) {
   352  	testenv.SetGODEBUG(t, "x509sslcertoverrideplatform=0")
   353  	t.Setenv(certFileEnv, "/tmp/nope")
   354  	t.Setenv(certDirEnv, "/tmp/nope")
   355  
   356  	p, err := loadSystemRoots()
   357  	if err != nil {
   358  		t.Fatalf("unexpected failure: %s", err)
   359  	}
   360  
   361  	if runtime.GOOS == "windows" || runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
   362  		if !p.systemPool {
   363  			t.Fatal("x509sslcertoverrideplatform did not override SSL_CERT_{FILE,DIR}")
   364  		}
   365  	} else if p.systemPool {
   366  		t.Fatal("x509sslcertoverrideplatform caused a systemPool to be returned on OS other than windows or darwin")
   367  	}
   368  }
   369  

View as plain text