grpc-go/security/advancedtls/crl_provider_test.go

298 lines
9.0 KiB
Go
Raw 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"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/security/advancedtls/testdata"
)
const nonCRLFilesUnderCRLDirectory = 5
// 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 := MakeStaticCRLProvider(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 Options configuration. The configurations include empty
// one, non existing CRLDirectory, invalid RefreshDuration, and the correct one.
func (s) TestFileWatcherCRLProviderConfig(t *testing.T) {
if _, err := MakeFileWatcherCRLProvider(Options{}); err == nil {
t.Fatalf("Empty Options should not be allowed")
}
if _, err := MakeFileWatcherCRLProvider(Options{CRLDirectory: "I_do_not_exist"}); err == nil {
t.Fatalf("CRLDirectory must exist")
}
if defaultProvider, err := MakeFileWatcherCRLProvider(Options{CRLDirectory: testdata.Path("crl/provider")}); err == nil {
if defaultProvider.opts.RefreshDuration != defaultCRLRefreshDuration {
t.Fatalf("RefreshDuration is not properly updated by validate() func")
}
defaultProvider.Close()
} else {
t.Fatal("Unexpected error:", err)
}
customCallback := func(err error) {
fmt.Printf("Custom error message: %v", err)
}
regularProvider, err := MakeFileWatcherCRLProvider(Options{
CRLDirectory: testdata.Path("crl"),
RefreshDuration: 5 * time.Second,
crlReloadingFailedCallback: customCallback,
})
if err != nil {
t.Fatal("Unexpected error while creating regular FileWatcherCRLProvider:", err)
}
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) {
nonCRLFilesSet := make(map[string]struct{})
customCallback := func(err error) {
nonCRLFilesSet[err.Error()] = struct{}{}
}
p, err := MakeFileWatcherCRLProvider(Options{
CRLDirectory: testdata.Path("crl"),
RefreshDuration: 5 * time.Second,
crlReloadingFailedCallback: customCallback,
})
if err != nil {
t.Fatal("Unexpected error while creating FileWatcherCRLProvider:", err)
}
p.ScanCRLDirectory()
// 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")
}
}
})
}
if len(nonCRLFilesSet) < nonCRLFilesUnderCRLDirectory {
t.Fatalf("Number of callback executions: got %v, want %v", len(nonCRLFilesSet), nonCRLFilesUnderCRLDirectory)
}
p.Close()
}
// TestFileWatcherCRLProviderDirectoryScan tests how FileWatcherCRLProvider
// handles different contents of Options.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
// cases are not independent from each other.
func (s) TestFileWatcherCRLProviderDirectoryScan(t *testing.T) {
sourcePath := testdata.Path("crl")
targetPath := testdata.Path("crl/provider/filewatcher")
p, err := MakeFileWatcherCRLProvider(Options{
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, name
// of the files to be copied there before the test case execution, and
// expected number of entries in the FileWatcherCRLProvider map.
tests := []struct {
desc string
fileNames []string
expectedEntries int
}{
{
desc: "Empty dir",
fileNames: []string{},
expectedEntries: 0,
},
{
desc: "Simple addition",
fileNames: []string{"1.crl"},
expectedEntries: 1,
},
{
desc: "Addition and deletion",
fileNames: []string{"2.crl", "3.crl"},
expectedEntries: 2,
},
{
desc: "Addition and updating",
fileNames: []string{"2.crl", "3.crl", "4.crl"},
expectedEntries: 3,
},
{
desc: "Addition and a corrupt file",
fileNames: []string{"5.crl", "README.md"},
expectedEntries: 4,
},
{
desc: "Full deletion",
fileNames: []string{},
expectedEntries: 0,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
copyFiles(sourcePath, targetPath, tt.fileNames, t)
p.ScanCRLDirectory()
if diff := cmp.Diff(len(p.crls), tt.expectedEntries); diff != "" {
t.Errorf("Expected number of entries in the map do not match\ndiff (-got +want):\n%s", diff)
}
})
}
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(testdata.Path(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)
}
}
}