grpc-go/security/advancedtls/crl_provider_test.go

357 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
*
* 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 its 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 its 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
}