/* * * 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 const minCRLRefreshDuration = 1 * time.Minute // CRLProvider is the interface to be implemented to enable custom CRL provider // behavior, as defined in [gRFC A69]. // // 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 RevocationOptions of advancedtls.ClientOptions // and/or advancedtls.ServerOptions. // Please note that checking CRLs is 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. // // [gRFC A69]: https://github.com/grpc/proposal/pull/382 type CRLProvider interface { // CRL accepts x509 Cert and returns a related CRL struct, which can contain // either an empty or non-empty list of revoked certificates. If an error is // thrown or (nil, nil) is returned, it indicates that we can't load any // authoritative CRL files (which may not necessarily be a problem). It's not // considered invalid to have no CRLs if there are no revocations for an // issuer. In such cases, the status of the check CRL operation is marked as // RevocationUndetermined, as defined in [RFC5280 - Undetermined]. // // [RFC5280 - Undetermined]: https://datatracker.ietf.org/doc/html/rfc5280#section-6.3.3 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 } // NewStaticCRLProvider processes raw content of CRL files, adds parsed CRL // structs into in-memory, and returns a new instance of the StaticCRLProvider. func NewStaticCRLProvider(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 passed to NewStaticCRLProvider. func (p *StaticCRLProvider) CRL(cert *x509.Certificate) (*CRL, error) { return p.crls[cert.Issuer.ToRDNSequence().String()], nil } // FileWatcherOptions represents a data structure holding a configuration for // FileWatcherCRLProvider. type FileWatcherOptions struct { CRLDirectory string // Required: Path of the directory containing CRL files RefreshDuration time.Duration // Optional: Time interval (default 1 hour) between CRLDirectory scans, can't be smaller than 1 minute CRLReloadingFailedCallback func(err error) // Optional: Custom callback executed when a CRL file can’t be processed } // FileWatcherCRLProvider implements the CRLProvider interface by periodically // scanning CRLDirectory (see FileWatcherOptions) and storing CRL structs // in-memory. Users should call Close to stop the background refresh of // CRLDirectory. type FileWatcherCRLProvider struct { crls map[string]*CRL opts FileWatcherOptions mu sync.Mutex stop chan struct{} done chan struct{} } // NewFileWatcherCRLProvider returns a new instance of the // FileWatcherCRLProvider. It uses FileWatcherOptions to validate and apply // configuration required for creating a new instance. The initial scan of // CRLDirectory is performed inside this function. Users should call Close to // stop the background refresh of CRLDirectory. func NewFileWatcherCRLProvider(o FileWatcherOptions) (*FileWatcherCRLProvider, error) { if err := o.validate(); err != nil { return nil, err } provider := &FileWatcherCRLProvider{ crls: make(map[string]*CRL), opts: o, stop: make(chan struct{}), done: make(chan struct{}), } provider.scanCRLDirectory() go provider.run() return provider, nil } func (o *FileWatcherOptions) validate() error { // Checks relates to CRLDirectory. if o.CRLDirectory == "" { return fmt.Errorf("advancedtls: CRLDirectory needs to be specified") } if _, err := os.ReadDir(o.CRLDirectory); err != nil { return fmt.Errorf("advancedtls: CRLDirectory %v is not readable: %v", o.CRLDirectory, err) } // Checks related to RefreshDuration. if o.RefreshDuration == 0 { o.RefreshDuration = defaultCRLRefreshDuration } if o.RefreshDuration < minCRLRefreshDuration { grpclogLogger.Warningf("RefreshDuration must be at least 1 minute: provided value %v, minimum value %v will be used.", o.RefreshDuration, minCRLRefreshDuration) o.RefreshDuration = minCRLRefreshDuration } return nil } // Start starts watching the directory for CRL files and updates the provider accordingly. func (p *FileWatcherCRLProvider) run() { defer close(p.done) ticker := time.NewTicker(p.opts.RefreshDuration) defer ticker.Stop() for { select { case <-p.stop: grpclogLogger.Infof("Scanning of CRLDirectory %v stopped", p.opts.CRLDirectory) return case <-ticker.C: p.scanCRLDirectory() } } } // Close waits till the background refresh of CRLDirectory of // FileWatcherCRLProvider is done and then stops it. func (p *FileWatcherCRLProvider) Close() { close(p.stop) <-p.done } // scanCRLDirectory starts the process of scanning // FileWatcherOptions.CRLDirectory and updating in-memory storage of CRL // structs, as defined in [gRFC A69]. It's called periodically // (see FileWatcherOptions.RefreshDuration) by run goroutine. // // [gRFC A69]: https://github.com/grpc/proposal/pull/382 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 successfully 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 = tempCRLs 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 loaded during FileWatcherOptions.CRLDirectory scan before // the execution of this function. func (p *FileWatcherCRLProvider) CRL(cert *x509.Certificate) (*CRL, error) { p.mu.Lock() defer p.mu.Unlock() return p.crls[cert.Issuer.ToRDNSequence().String()], nil }