grpc-go/security/advancedtls/crl_provider.go

237 lines
7.9 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"
"os"
"sync"
"time"
)
const defaultCRLRefreshDuration = 1 * time.Hour
// CRLProvider is the interface to be implemented to enable custom CRL provider
// behavior.
//
// The interface defines how gRPC gets CRLs from the provider during handshakes,
// but doesn't prescribe a specific way to load and store CRLs. Such
// implementations can be used in RevocationConfig of advancedtls.ClientOptions
// and/or advancedtls.ServerOptions.
// Please note that checking CRLs is being directly on the path of connection
// establishment, so implementations of the CRL function need to be fast, and
// slow things such as file IO should be done asynchronously.
// TODO(erm-g): Add link to related gRFC once it's ready.
// Please refer to https://github.com/grpc/proposal/ for more details.
type CRLProvider interface {
// CRL accepts x509 Cert and returns back related CRL struct. The CRL struct
// can be nil, can contain empty or non-empty list of revkoed certificates.
// Callers are expected to use the returned value as read-only.
CRL(cert *x509.Certificate) (*CRL, error)
}
// StaticCRLProvider implements CRLProvider interface by accepting raw content
// of CRL files at creation time and storing parsed CRL structs in-memory.
type StaticCRLProvider struct {
crls map[string]*CRL
}
// MakeStaticCRLProvider processes raw content of CRL files, adds parsed CRL
// structs into in-memory, and returns a new instance of the StaticCRLProvider.
func MakeStaticCRLProvider(rawCRLs [][]byte) *StaticCRLProvider {
p := StaticCRLProvider{}
p.crls = make(map[string]*CRL)
for idx, rawCRL := range rawCRLs {
cRL, err := NewCRL(rawCRL)
if err != nil {
grpclogLogger.Warningf("Can't parse raw CRL number %v from the slice: %v", idx, err)
continue
}
p.addCRL(cRL)
}
return &p
}
// AddCRL adds/updates provided CRL to in-memory storage.
func (p *StaticCRLProvider) addCRL(crl *CRL) {
key := crl.certList.Issuer.ToRDNSequence().String()
p.crls[key] = crl
}
// CRL returns CRL struct if it was previously loaded by calling AddCRL.
func (p *StaticCRLProvider) CRL(cert *x509.Certificate) (*CRL, error) {
return p.crls[cert.Issuer.ToRDNSequence().String()], nil
}
// Options represents a data structure holding a
// configuration for FileWatcherCRLProvider.
type Options struct {
CRLDirectory string // Path of the directory containing CRL files
RefreshDuration time.Duration // Time interval between CRLDirectory scans
crlReloadingFailedCallback func(err error) // Custom callback executed when a CRL file cant be processed
}
// FileWatcherCRLProvider implements the CRLProvider interface by periodically scanning
// CRLDirectory (see Options) and storing CRL structs in-memory
type FileWatcherCRLProvider struct {
crls map[string]*CRL
opts Options
mu sync.Mutex
done chan struct{}
}
// MakeFileWatcherCRLProvider returns a new instance of the
// FileWatcherCRLProvider. It uses Options to validate and apply configuration
// required for creating a new instance.
func MakeFileWatcherCRLProvider(o Options) (*FileWatcherCRLProvider, error) {
if err := o.validate(); err != nil {
return nil, err
}
done := make(chan struct{})
provider := &FileWatcherCRLProvider{
crls: make(map[string]*CRL),
opts: o,
done: done,
}
go provider.run()
return provider, nil
}
func (o *Options) validate() error {
// Checks relates to CRLDirectory.
if o.CRLDirectory == "" {
return fmt.Errorf("advancedtls: CRLDirectory needs to be specified")
}
fileInfo, err := os.Stat(o.CRLDirectory)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("advancedtls: CRLDirectory %v does not exist", o.CRLDirectory)
}
return err
}
if !fileInfo.IsDir() {
return fmt.Errorf("advancedtls: CRLDirectory %v is not a directory", o.CRLDirectory)
}
_, err = os.Open(o.CRLDirectory)
if err != nil {
if os.IsPermission(err) {
return fmt.Errorf("advancedtls: CRLDirectory %v is not readable", o.CRLDirectory)
}
return err
}
// Checks related to RefreshDuration.
if o.RefreshDuration <= 0 || o.RefreshDuration < time.Second {
o.RefreshDuration = defaultCRLRefreshDuration
grpclogLogger.Warningf("RefreshDuration must larger then 1 second: provided value %v, default value will be used %v", o.RefreshDuration, defaultCRLRefreshDuration)
}
return nil
}
// Start starts watching the directory for CRL files and updates the provider accordingly.
func (p *FileWatcherCRLProvider) run() {
ticker := time.NewTicker(p.opts.RefreshDuration)
defer ticker.Stop()
p.ScanCRLDirectory()
for {
select {
case <-p.done:
ticker.Stop()
return
case <-ticker.C:
p.ScanCRLDirectory()
}
}
}
// Close stops the background refresh of CRLDirectory of FileWatcherCRLProvider
func (p *FileWatcherCRLProvider) Close() {
close(p.done)
}
// ScanCRLDirectory starts the process of scanning Options.CRLDirectory and
// updating in-memory storage of CRL structs. Please note that the same method is
// called periodically by run goroutine.
// TODO(erm-g): Add link to related gRFC once it's ready.
// Please refer to https://github.com/grpc/proposal/ for edge case details.
func (p *FileWatcherCRLProvider) ScanCRLDirectory() {
dir, err := os.Open(p.opts.CRLDirectory)
if err != nil {
grpclogLogger.Errorf("Can't open CRLDirectory %v", p.opts.CRLDirectory, err)
if p.opts.crlReloadingFailedCallback != nil {
p.opts.crlReloadingFailedCallback(err)
}
}
defer dir.Close()
files, err := dir.ReadDir(0)
if err != nil {
grpclogLogger.Errorf("Can't access files under CRLDirectory %v", p.opts.CRLDirectory, err)
if p.opts.crlReloadingFailedCallback != nil {
p.opts.crlReloadingFailedCallback(err)
}
}
tempCRLs := make(map[string]*CRL)
successCounter := 0
failCounter := 0
for _, file := range files {
filePath := fmt.Sprintf("%s/%s", p.opts.CRLDirectory, file.Name())
crl, err := ReadCRLFile(filePath)
if err != nil {
failCounter++
grpclogLogger.Warningf("Can't add CRL from file %v under CRLDirectory %v", filePath, p.opts.CRLDirectory, err)
if p.opts.crlReloadingFailedCallback != nil {
p.opts.crlReloadingFailedCallback(err)
}
continue
}
tempCRLs[crl.certList.Issuer.ToRDNSequence().String()] = crl
successCounter++
}
// Only if all the files are processed successful we can swap maps (there
// might be deletions of entries in this case
if len(files) == successCounter {
p.mu.Lock()
defer p.mu.Unlock()
p.crls = make(map[string]*CRL)
for key, value := range tempCRLs {
p.crls[key] = value
}
grpclogLogger.Infof("Scan of CRLDirectory %v completed, %v files found and processed successfully, in-memory CRL storage flushed and repopulated", p.opts.CRLDirectory, len(files))
} else {
// Since some of the files failed we can only add/update entries in the map
p.mu.Lock()
defer p.mu.Unlock()
for key, value := range tempCRLs {
p.crls[key] = value
}
grpclogLogger.Infof("Scan of CRLDirectory %v completed, %v files found, %v files processing failed, %v entries of in-memory CRL storage added/updated", p.opts.CRLDirectory, len(files), failCounter, successCounter)
}
}
// CRL retrieves the CRL associated with the given certificate's issuer DN from
// in-memory if it was previously loaded during CRLDirectory scan.
func (p *FileWatcherCRLProvider) CRL(cert *x509.Certificate) (*CRL, error) {
p.mu.Lock()
defer p.mu.Unlock()
return p.crls[cert.Issuer.ToRDNSequence().String()], nil
}