-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathcrl.go
More file actions
223 lines (194 loc) · 6.8 KB
/
crl.go
File metadata and controls
223 lines (194 loc) · 6.8 KB
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
package restful
import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
"time"
)
type crl struct {
mu sync.RWMutex
serials map[string]struct{}
nextUpdate time.Time
strictCheck bool
}
type clientOrServer interface {
setCRL(serials map[string]struct{}, nextUpdate time.Time, strict bool)
getCRL() *crl
}
var (
// ErrRevocationListReadError is when error happen during reading of revocation list
ErrRevocationListReadError = errors.New("error reading revocation list")
// ErrRevocationListOutOfDate is when the revocation list is out of date
ErrRevocationListOutOfDate = errors.New("revocation list out of date")
// ErrCertificateRevoked happens when the certificate revoked
ErrCertificateRevoked = errors.New("certificate revoked")
)
// CRLOptions defines the settings restful clients/servers can use for CRL verification
type CRLOptions struct {
// Ctx is a cancelable background context used for the regular CRL reading
Ctx context.Context
// CRLLocation is the file name or comma-separated HTTP distribution point URI list where the revocation list shall be from
CRLLocation string
// ReadInterval is the time interval the CRL information is retrieved from the distribution list or re-read from the file
ReadInterval time.Duration
// FileExistTimeout allows for the CRL file to not exist for an initial duration of time without reporting an error
FileExistTimeout time.Duration
// StatusChan is a channel where the CRL status can be provided.
// Every ReadInterval, an error is sent on the channel if getting the CRL is not successful or if the NextUpdate in the
// latest CRL is in the past, or nil is sent in case of no error.
StatusChan chan (error)
// StrictValityCheck enables strict NextUpdate checking.
// With this enabled, peer certificate checks will fail if the latest CRL file is outdated
StrictValityCheck bool
}
// set CRL for client or server.
// runs a periodic loop to re-read CRL
func setCRL(x clientOrServer, o CRLOptions) {
if o.Ctx == nil {
o.Ctx = context.Background()
}
fileExistDeadline := time.Now().Add(o.FileExistTimeout)
// initial read
crl, nextUpdate, lastModification, err := readCRL(o.CRLLocation, fileExistDeadline, time.Time{}, time.Time{})
if err == nil {
x.setCRL(crl, nextUpdate, o.StrictValityCheck)
} else {
// always initialize CRL pointer
x.setCRL(nil, time.Time{}, o.StrictValityCheck)
if o.StatusChan != nil {
o.StatusChan <- err
}
}
go func() {
ticker := time.NewTicker(o.ReadInterval)
for {
select {
case <-ticker.C:
crl, nextUpdate, lastModification, err = readCRL(o.CRLLocation, fileExistDeadline, nextUpdate, lastModification)
if err == nil && !lastModification.IsZero() {
x.setCRL(crl, nextUpdate, o.StrictValityCheck)
}
if o.StatusChan != nil {
o.StatusChan <- err
}
case <-o.Ctx.Done():
ticker.Stop()
return
}
}
}()
}
// readCRL reads the list of expired certificates from a location (file name or URL).
// It can send an error or nil the provided status channel when the status potentially changes.
// Will not send error if the file at the provided path doesn't exist before the provided deadline.
// If CRLLocation is a file, it is not re-read until it's last modification date is after lastKnownFileDate.
func readCRL(location string, deadline, nextUpdate, lastModified time.Time) (map[string]struct{}, time.Time, time.Time, error) {
crlBytes, newLastModified, err := getCRLBody(location, lastModified)
if err != nil {
if time.Now().Before(deadline) && errors.Is(err, os.ErrNotExist) {
return nil, time.Time{}, time.Time{}, nil
}
return nil, time.Time{}, time.Time{}, err
}
if crlBytes == nil {
// the file date has not changed since last read.
// may be past nextUpdate
err := checkNextUpdate(nextUpdate)
return nil, time.Time{}, time.Time{}, err
}
// Handle optional PEM decoding
if block, _ := pem.Decode(crlBytes); block != nil {
crlBytes = block.Bytes
}
revList, err := x509.ParseRevocationList(crlBytes)
if err != nil {
err = fmt.Errorf("%w: %s", ErrRevocationListReadError, err)
return nil, time.Time{}, newLastModified, err
}
nextUpdate = revList.NextUpdate
crl := make(map[string]struct{})
for _, rc := range revList.RevokedCertificateEntries {
crl[rc.SerialNumber.String()] = struct{}{}
}
err = checkNextUpdate(nextUpdate)
if err != nil {
return crl, nextUpdate, newLastModified, err
}
return crl, nextUpdate, newLastModified, nil
}
func checkNextUpdate(nextUpdate time.Time) error {
if nextUpdate.Before(time.Now()) {
return fmt.Errorf("%w: revocation list nextupdate is outdated: %s", ErrRevocationListOutOfDate, nextUpdate)
}
return nil
}
// getCRLbody returns the body and last modification time of the new CRL file,
// or nothing, if the file has not been modified since the given timestamp.
// If location is an URL, it will return the current time.
func getCRLBody(location string, lastModified time.Time) ([]byte, time.Time, error) {
var crlBytes []byte
var err error
if strings.HasPrefix(location, "http://") {
crlURIs := strings.Split(location, ",")
var resp *http.Response
for _, uri := range crlURIs {
uri = strings.Trim(uri, " ")
resp, err = http.Get(uri) // #nosec G107 - HTTP request made with variable uri
if err != nil {
continue
}
}
if err != nil {
return nil, time.Time{}, fmt.Errorf("%w: couldn't download CRL: %s", ErrRevocationListReadError, err)
}
crlBytes, err = io.ReadAll(resp.Body)
if err != nil {
return nil, time.Time{}, fmt.Errorf("%w: couldn't read CRL body: %s", ErrRevocationListReadError, err)
}
return crlBytes, time.Now(), nil
}
info, err := os.Stat(location)
if err != nil {
return nil, time.Time{}, fmt.Errorf("%w: couldn't read CRL file: %s", ErrRevocationListReadError, err)
}
if !info.ModTime().After(lastModified) {
return nil, time.Time{}, nil
}
crlBytes, err = os.ReadFile(location) // #nosec G304 - file path is provided in variable
if err != nil {
return nil, time.Time{}, fmt.Errorf("%w: couldn't read CRL file: %s", ErrRevocationListReadError, err)
}
return crlBytes, info.ModTime(), nil
}
func verifyPeerCert(crl *crl) func([][]byte, [][]*x509.Certificate) error {
// note: pass crl as pointer, and only reassign its value
return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(verifiedChains) == 0 {
return fmt.Errorf("no verified chains")
}
if crl == nil {
return nil
}
if crl.strictCheck && crl.nextUpdate.Before(time.Now()) {
return ErrRevocationListOutOfDate
}
// Parse leaf certificate
leaf := verifiedChains[0][0]
crl.mu.RLock()
defer crl.mu.RUnlock()
if len(crl.serials) > 0 {
// Check revocation
if _, ok := crl.serials[leaf.SerialNumber.String()]; ok {
return fmt.Errorf("%w: %X", ErrCertificateRevoked, leaf.SerialNumber)
}
}
return nil
}
}