forked from grpc/grpc-go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcrl_provider.go
239 lines (220 loc) · 8.52 KB
/
crl_provider.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
*
* 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
}