mirror of https://github.com/grpc/grpc-go.git
237 lines
7.9 KiB
Go
237 lines
7.9 KiB
Go
/*
|
||
*
|
||
* 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 can’t 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
|
||
}
|