forked from adamlaska/boulder
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfilter_source.go
More file actions
142 lines (127 loc) · 4.92 KB
/
filter_source.go
File metadata and controls
142 lines (127 loc) · 4.92 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
package responder
import (
"bytes"
"context"
"crypto"
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/issuance"
blog "github.com/letsencrypt/boulder/log"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/crypto/ocsp"
)
type responderID struct {
nameHash []byte
keyHash []byte
}
type filterSource struct {
wrapped Source
hashAlgorithm crypto.Hash
issuers map[issuance.IssuerNameID]responderID
serialPrefixes []string
counter *prometheus.CounterVec
log blog.Logger
}
// NewFilterSource returns a filterSource which performs various checks on the
// OCSP requests sent to the wrapped Source, and the OCSP responses returned
// by it.
func NewFilterSource(issuerCerts []*issuance.Certificate, serialPrefixes []string, wrapped Source, stats prometheus.Registerer, log blog.Logger) (*filterSource, error) {
if len(issuerCerts) < 1 {
return nil, errors.New("Filter must include at least 1 issuer cert")
}
issuersByNameId := make(map[issuance.IssuerNameID]responderID)
for _, issuerCert := range issuerCerts {
keyHash := issuerCert.KeyHash()
nameHash := issuerCert.NameHash()
rid := responderID{
keyHash: keyHash[:],
nameHash: nameHash[:],
}
issuersByNameId[issuerCert.NameID()] = rid
}
counter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "ocsp_filter_responses",
Help: "Count of OCSP requests/responses by action taken by the filter",
}, []string{"result"})
return &filterSource{
wrapped: wrapped,
hashAlgorithm: crypto.SHA1,
issuers: issuersByNameId,
serialPrefixes: serialPrefixes,
counter: counter,
log: log,
}, nil
}
// Response implements the Source interface. It checks the incoming request
// to ensure that we want to handle it, fetches the response from the wrapped
// Source, and checks that the response matches the request.
func (src *filterSource) Response(ctx context.Context, req *ocsp.Request) (*Response, error) {
iss, err := src.checkRequest(req)
if err != nil {
src.log.Debugf("Not responding to filtered OCSP request: %s", err.Error())
src.counter.WithLabelValues("request_filtered").Inc()
return nil, err
}
resp, err := src.wrapped.Response(ctx, req)
if err != nil {
src.counter.WithLabelValues("wrapped_error").Inc()
return nil, err
}
err = src.checkResponse(iss, resp)
if err != nil {
src.log.Warningf("OCSP Response not sent (issuer and serial mismatch) for CA=%s, Serial=%s", hex.EncodeToString(req.IssuerKeyHash), core.SerialToString(req.SerialNumber))
src.counter.WithLabelValues("response_filtered").Inc()
return nil, err
}
src.counter.WithLabelValues("success").Inc()
return resp, nil
}
// checkRequest returns a descriptive error if the request does not satisfy any of
// the requirements of an OCSP request, or nil if the request should be handled.
// If the request passes all checks, then checkRequest returns the unique id of
// the issuer cert specified in the request.
func (src *filterSource) checkRequest(req *ocsp.Request) (issuance.IssuerNameID, error) {
if req.HashAlgorithm != src.hashAlgorithm {
return 0, fmt.Errorf("unsupported issuer key/name hash algorithm %s: %w", req.HashAlgorithm, ErrNotFound)
}
if len(src.serialPrefixes) > 0 {
serialString := core.SerialToString(req.SerialNumber)
match := false
for _, prefix := range src.serialPrefixes {
if strings.HasPrefix(serialString, prefix) {
match = true
break
}
}
if !match {
return 0, fmt.Errorf("unrecognized serial prefix: %w", ErrNotFound)
}
}
for nameID, rid := range src.issuers {
if bytes.Equal(req.IssuerNameHash, rid.nameHash) && bytes.Equal(req.IssuerKeyHash, rid.keyHash) {
return nameID, nil
}
}
return 0, fmt.Errorf("unrecognized issuer key hash %s: %w", hex.EncodeToString(req.IssuerKeyHash), ErrNotFound)
}
// checkResponse returns nil if the ocsp response was generated by the same
// issuer as was identified in the request, or an error otherwise. This filters
// out, for example, responses which are for a serial that we issued, but from a
// different issuer than that contained in the request.
func (src *filterSource) checkResponse(reqIssuerID issuance.IssuerNameID, resp *Response) error {
respIssuerID := issuance.GetOCSPIssuerNameID(resp.Response)
if reqIssuerID != respIssuerID {
// This would be allowed if we used delegated responders, but we don't.
return fmt.Errorf("responder name does not match requested issuer name")
}
// In an ideal world, we'd also compare the Issuer Key Hash from the request's
// CertID (equivalent to looking up the key hash in src.issuers) against the
// Issuer Key Hash contained in the response's CertID. However, the Go OCSP
// library does not provide access to the response's CertID, so we can't.
// Specifically, we want to compare `src.issuers[reqIssuerID].keyHash` against
// something like resp.CertID.IssuerKeyHash, but the latter does not exist.
return nil
}