/* * * Copyright 2023 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package advancedtls import ( "crypto/x509" "fmt" "io" "os" "path/filepath" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "google.golang.org/grpc/security/advancedtls/testdata" ) // TestStaticCRLProvider tests how StaticCRLProvider handles the major four // cases for CRL checks. It loads the CRLs under crl directory, constructs // unrevoked, revoked leaf, and revoked intermediate chains, as well as a chain // without CRL for issuer, and checks that it’s correctly processed. func (s) TestStaticCRLProvider(t *testing.T) { rawCRLs := make([][]byte, 6) for i := 1; i <= 6; i++ { rawCRL, err := os.ReadFile(testdata.Path(fmt.Sprintf("crl/%d.crl", i))) if err != nil { t.Fatalf("readFile(%v) failed err = %v", fmt.Sprintf("crl/%d.crl", i), err) } rawCRLs = append(rawCRLs, rawCRL) } p := NewStaticCRLProvider(rawCRLs) // Each test data entry contains a description of a certificate chain, // certificate chain itself, and if CRL is not expected to be found. tests := []struct { desc string certs []*x509.Certificate expectNoCRL bool }{ { desc: "Unrevoked chain", certs: makeChain(t, testdata.Path("crl/unrevoked.pem")), }, { desc: "Revoked Intermediate chain", certs: makeChain(t, testdata.Path("crl/revokedInt.pem")), }, { desc: "Revoked leaf chain", certs: makeChain(t, testdata.Path("crl/revokedLeaf.pem")), }, { desc: "Chain with no CRL for issuer", certs: makeChain(t, testdata.Path("client_cert_1.pem")), expectNoCRL: true, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { for _, c := range tt.certs { crl, err := p.CRL(c) if err != nil { t.Fatalf("Expected error fetch from provider: %v", err) } if crl == nil && !tt.expectNoCRL { t.Fatalf("CRL is unexpectedly nil") } } }) } } // TestFileWatcherCRLProviderConfig checks creation of FileWatcherCRLProvider, // and the validation of FileWatcherOptions configuration. The configurations include empty // one, non existing CRLDirectory, invalid RefreshDuration, and the correct one. func (s) TestFileWatcherCRLProviderConfig(t *testing.T) { if _, err := NewFileWatcherCRLProvider(FileWatcherOptions{}); err == nil { t.Fatalf("Empty FileWatcherOptions should not be allowed") } if _, err := NewFileWatcherCRLProvider(FileWatcherOptions{CRLDirectory: "I_do_not_exist"}); err == nil { t.Fatalf("CRLDirectory must exist") } defaultProvider, err := NewFileWatcherCRLProvider(FileWatcherOptions{CRLDirectory: testdata.Path("crl")}) if err != nil { t.Fatal("Unexpected error:", err) } if defaultProvider.opts.RefreshDuration != defaultCRLRefreshDuration { t.Fatalf("RefreshDuration for defaultCRLRefreshDuration case is not properly updated by validate() func") } defaultProvider.Close() tooFastRefreshProvider, err := NewFileWatcherCRLProvider(FileWatcherOptions{ CRLDirectory: testdata.Path("crl"), RefreshDuration: 5 * time.Second, }) if err != nil { t.Fatal("Unexpected error:", err) } if tooFastRefreshProvider.opts.RefreshDuration != minCRLRefreshDuration { t.Fatalf("RefreshDuration for minCRLRefreshDuration case is not properly updated by validate() func") } tooFastRefreshProvider.Close() customCallback := func(err error) { fmt.Printf("Custom error message: %v", err) } regularProvider, err := NewFileWatcherCRLProvider(FileWatcherOptions{ CRLDirectory: testdata.Path("crl"), RefreshDuration: 2 * time.Hour, CRLReloadingFailedCallback: customCallback, }) if err != nil { t.Fatal("Unexpected error while creating regular FileWatcherCRLProvider:", err) } if regularProvider.opts.RefreshDuration != 2*time.Hour { t.Fatalf("Valid refreshDuration was incorrectly updated by validate() func") } regularProvider.Close() } // TestFileWatcherCRLProvider tests how FileWatcherCRLProvider handles the major // four cases for CRL checks. It scans the CRLs under crl directory to populate // the in-memory storage. Then we construct unrevoked, revoked leaf, and revoked // intermediate chains, as well as a chain without CRL for issuer, and check // that it’s correctly processed. Additionally, we also check if number of // invocations of custom callback is correct. func (s) TestFileWatcherCRLProvider(t *testing.T) { const nonCRLFilesUnderCRLDirectory = 17 nonCRLFilesSet := make(map[string]struct{}) customCallback := func(err error) { if strings.Contains(err.Error(), "BUILD") { return } nonCRLFilesSet[err.Error()] = struct{}{} } p, err := NewFileWatcherCRLProvider(FileWatcherOptions{ CRLDirectory: testdata.Path("crl"), RefreshDuration: 1 * time.Hour, CRLReloadingFailedCallback: customCallback, }) if err != nil { t.Fatal("Unexpected error while creating FileWatcherCRLProvider:", err) } // Each test data entry contains a description of a certificate chain, // certificate chain itself, and if CRL is not expected to be found. tests := []struct { desc string certs []*x509.Certificate expectNoCRL bool }{ { desc: "Unrevoked chain", certs: makeChain(t, testdata.Path("crl/unrevoked.pem")), }, { desc: "Revoked Intermediate chain", certs: makeChain(t, testdata.Path("crl/revokedInt.pem")), }, { desc: "Revoked leaf chain", certs: makeChain(t, testdata.Path("crl/revokedLeaf.pem")), }, { desc: "Chain with no CRL for issuer", certs: makeChain(t, testdata.Path("client_cert_1.pem")), expectNoCRL: true, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { for _, c := range tt.certs { crl, err := p.CRL(c) if err != nil { t.Fatalf("Expected error fetch from provider: %v", err) } if crl == nil && !tt.expectNoCRL { t.Fatalf("CRL is unexpectedly nil") } } }) } p.Close() if diff := cmp.Diff(len(nonCRLFilesSet), nonCRLFilesUnderCRLDirectory); diff != "" { t.Errorf("Unexpected number Number of callback executions\ndiff (-got +want):\n%s", diff) } } // TestFileWatcherCRLProviderDirectoryScan tests how FileWatcherCRLProvider // handles different contents of FileWatcherOptions.CRLDirectory. // We update the content with various (correct and incorrect) CRL files and // check if in-memory storage was properly updated. Please note that the same // instance of FileWatcherCRLProvider is used for the whole test so test cases // are not independent from each other. func (s) TestFileWatcherCRLProviderDirectoryScan(t *testing.T) { sourcePath := testdata.Path("crl") targetPath := createTmpDir(t) defer os.RemoveAll(targetPath) p, err := NewFileWatcherCRLProvider(FileWatcherOptions{ CRLDirectory: targetPath, RefreshDuration: 1 * time.Hour, }) if err != nil { t.Fatal("Unexpected error while creating FileWatcherCRLProvider:", err) } // Each test data entry contains a description of CRL directory content // (including the expected number of entries in the FileWatcherCRLProvider // map), the name of the files to be copied there before executing the test // case, and information regarding whether a specific certificate is expected // to be found in the map. tests := []struct { desc string crlFileNames []string certFileNames []struct { fileName string expected bool } }{ { desc: "Simple addition (1 map entry)", crlFileNames: []string{"1.crl"}, certFileNames: []struct { fileName string expected bool }{ {"crl/unrevoked.pem", true}, }, }, { desc: "Addition and deletion (2 map entries)", crlFileNames: []string{"3.crl", "5.crl"}, certFileNames: []struct { fileName string expected bool }{ {"crl/revokedInt.pem", true}, {"crl/revokedLeaf.pem", true}, {"crl/unrevoked.pem", false}, }, }, { desc: "Addition and a corrupt file (3 map entries)", crlFileNames: []string{"1.crl", "README.md"}, certFileNames: []struct { fileName string expected bool }{ {"crl/revokedInt.pem", true}, {"crl/revokedLeaf.pem", true}, {"crl/unrevoked.pem", true}, }}, { desc: "Full deletion (0 map entries)", crlFileNames: []string{}, certFileNames: []struct { fileName string expected bool }{ {"crl/revokedInt.pem", false}, {"crl/revokedLeaf.pem", false}, {"crl/unrevoked.pem", false}, }}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { copyFiles(sourcePath, targetPath, tt.crlFileNames, t) p.scanCRLDirectory() for _, certFileName := range tt.certFileNames { c := makeChain(t, testdata.Path(certFileName.fileName))[0] crl, err := p.CRL(c) if err != nil { t.Errorf("Cannot fetch CRL from provider: %v", err) } if crl == nil && certFileName.expected { t.Errorf("CRL is unexpectedly nil") } if crl != nil && !certFileName.expected { t.Errorf("CRL is unexpectedly not nil") } } }) } p.Close() } func copyFiles(sourcePath string, targetPath string, fileNames []string, t *testing.T) { t.Helper() targetDir, err := os.Open(targetPath) if err != nil { t.Fatalf("Can't open dir %v: %v", targetPath, err) } defer targetDir.Close() names, err := targetDir.Readdirnames(-1) if err != nil { t.Fatalf("Can't read dir %v: %v", targetPath, err) } for _, name := range names { err = os.RemoveAll(filepath.Join(targetPath, name)) if err != nil { t.Fatalf("Can't remove file %v: %v", name, err) } } for _, fileName := range fileNames { destinationPath := filepath.Join(targetPath, fileName) sourceFile, err := os.Open(filepath.Join(sourcePath, fileName)) if err != nil { t.Fatalf("Can't open file %v: %v", fileName, err) } defer sourceFile.Close() destinationFile, err := os.Create(destinationPath) if err != nil { t.Fatalf("Can't create file %v: %v", destinationFile, err) } defer destinationFile.Close() _, err = io.Copy(destinationFile, sourceFile) if err != nil { t.Fatalf("Can't copy file %v to %v: %v", sourceFile, destinationFile, err) } } } func createTmpDir(t *testing.T) string { t.Helper() // Create a temp directory. Passing an empty string for the first argument // uses the system temp directory. dir, err := os.MkdirTemp("", "filewatcher*") if err != nil { t.Fatalf("os.MkdirTemp() failed: %v", err) } t.Logf("Using tmpdir: %s", dir) return dir }