-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathctlog.go
More file actions
269 lines (243 loc) · 9.43 KB
/
ctlog.go
File metadata and controls
269 lines (243 loc) · 9.43 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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// Copyright 2016 Google LLC. All Rights Reserved.
//
// 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 tesseract
import (
"context"
"crypto"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/transparency-dev/tesseract/internal/ccadb"
"github.com/transparency-dev/tesseract/internal/ct"
"github.com/transparency-dev/tesseract/internal/x509util"
"github.com/transparency-dev/tesseract/storage"
"k8s.io/klog/v2"
)
// ChainValidationConfig contains parameters to configure chain validation.
type ChainValidationConfig struct {
// RootsPEMFile is the path to the file containing root certificates that
// are acceptable to the log. The certs are served through get-roots
// endpoint.
RootsPEMFile string
// RootsRemoteFetchURLs configures an endpoint to fetch additional roots from.
RootsRemoteFetchURLs []string
// RootsRemoteFetchInterval configures the frequency at which to fetch
// roots from RootsRemoteEndpoint.
RootsRemoteFetchInterval time.Duration
// RootsRemoteFetchBackup is a persistent backup storage for roots fetched
// remotely.
RootsRemoteFetchBackup storage.RootsStorage
// RejectRoots is a list of hex-encoded SHA-256 fingerprints of ASN.1 DER
// encoded root certificates that should never be trusted.
RejectRoots []string
// RejectExpired controls if true then the certificate validity period will be
// checked against the current time during the validation of submissions.
// This will cause expired certificates to be rejected.
RejectExpired bool
// RejectUnexpired controls if TesseraCT rejects certificates that are
// either currently valid or not yet valid.
// TODO(phboneff): evaluate whether we need to keep this one.
RejectUnexpired bool
// ExtKeyUsages lists Extended Key Usage values that newly submitted
// certificates MUST contain. By default all are accepted. The
// values specified must be ones known to the x509 package, comma separated.
ExtKeyUsages string
// RejectExtensions lists X.509 extension OIDs that newly submitted
// certificates MUST NOT contain. Empty by default. Values must be
// specificed in dotted string form (e.g. "2.3.4.5").
RejectExtensions string
// NotAfterStart defines the start of the range of acceptable NotAfter
// values, inclusive.
// Leaving this unset implies no lower bound to the range.
NotAfterStart *time.Time
// NotAfterLimit defines the end of the range of acceptable NotAfter values,
// exclusive.
// Leaving this unset implies no upper bound to the range.
NotAfterLimit *time.Time
// AcceptSHA1 specifies whether cert chains using SHA-1 based signing algorithms
// are allowed.
// CAUTION: This is a temporary solution and it will eventually be removed.
// DO NOT depend on it.
AcceptSHA1 bool
}
// systemTimeSource implements ct.TimeSource.
type systemTimeSource struct{}
// Now returns the true current local time.
func (s systemTimeSource) Now() time.Time {
return time.Now()
}
var sysTimeSource = systemTimeSource{}
// newChainValidator checks that a chain validation config is valid,
// parses it, and loads resources to validate chains.
func newChainValidator(ctx context.Context, cfg ChainValidationConfig) (ct.ChainValidator, error) {
// Load the trusted roots.
if cfg.RootsPEMFile == "" {
return nil, errors.New("empty rootsPemFile")
}
roots, err := x509util.NewPEMCertPool(cfg.RejectRoots)
if err != nil {
return nil, fmt.Errorf("failed to create roots pool: %v", err)
}
if err := roots.AppendCertsFromPEMFile(cfg.RootsPEMFile); err != nil {
return nil, fmt.Errorf("failed to read trusted roots from %q: %v", cfg.RootsPEMFile, err)
}
if cfg.RejectExpired && cfg.RejectUnexpired {
return nil, errors.New("configuration would reject all certificates")
}
// Validate the time interval.
if cfg.NotAfterStart != nil && cfg.NotAfterLimit != nil && (cfg.NotAfterLimit).Before(*cfg.NotAfterStart) {
return nil, fmt.Errorf("'Not After' limit %q before start %q", cfg.NotAfterLimit.Format(time.RFC3339), cfg.NotAfterStart.Format(time.RFC3339))
}
var extKeyUsages []x509.ExtKeyUsage
// Filter which extended key usages are allowed.
if cfg.ExtKeyUsages != "" {
lExtKeyUsages := strings.Split(cfg.ExtKeyUsages, ",")
extKeyUsages, err = ct.ParseExtKeyUsages(lExtKeyUsages)
if err != nil {
return nil, fmt.Errorf("failed to parse ExtKeyUsages: %v", err)
}
}
var rejectExtIds []asn1.ObjectIdentifier
// Filter which extensions are rejected.
if cfg.RejectExtensions != "" {
lRejectExtensions := strings.Split(cfg.RejectExtensions, ",")
rejectExtIds, err = ct.ParseOIDs(lRejectExtensions)
if err != nil {
return nil, fmt.Errorf("failed to parse RejectExtensions: %v", err)
}
}
if cfg.RootsRemoteFetchBackup != nil {
kvs, err := cfg.RootsRemoteFetchBackup.LoadAll(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load previously remotely fetched root from remote root backup storage: %v", err)
}
certs := make([][]byte, 0, len(kvs))
for _, kv := range kvs {
certs = append(certs, kv.V)
}
parsed, added := roots.AppendCertsFromPEMs(certs...)
klog.Infof("Fetched %d roots, parsed %d, and loaded %d new ones from remote root backup storage", len(certs), parsed, added)
}
if cfg.RootsRemoteFetchInterval > 0 && len(cfg.RootsRemoteFetchURLs) > 0 {
fetchAndAppendRemoteRoots := func(url string) {
rr, err := ccadb.Fetch(ctx, url, []string{ccadb.ColPEM})
if err != nil {
klog.Errorf("Couldn't fetch roots from %q: %s", url, err)
return
}
pems := make([][]byte, 0, len(rr))
for _, r := range rr {
if len(r) < 1 {
klog.Errorf("Couldn't parse root from %q: empty row", url)
continue
}
pems = append(pems, r[0])
if cfg.RootsRemoteFetchBackup != nil {
block, _ := pem.Decode(r[0])
if block == nil {
klog.Errorf("Failed to decode PEM block in data fetched from %q", url)
continue
}
sha := sha256.Sum256(block.Bytes)
key := []byte(hex.EncodeToString(sha[:]))
if err := cfg.RootsRemoteFetchBackup.AddIfNotExist(ctx, []storage.KV{{K: key, V: r[0]}}); err != nil {
klog.Errorf("Couldn't store roots %q: %v", string(key), err)
continue
}
}
}
parsed, added := roots.AppendCertsFromPEMs(pems...)
klog.Infof("Fetched %d roots, parsed %d, and loaded %d new ones from %q", len(pems), parsed, added, url)
}
for _, url := range cfg.RootsRemoteFetchURLs {
fetchAndAppendRemoteRoots(url)
}
go func() {
ticker := time.NewTicker(cfg.RootsRemoteFetchInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
for _, url := range cfg.RootsRemoteFetchURLs {
fetchAndAppendRemoteRoots(url)
}
}
}
}()
}
cv := ct.NewChainValidator(roots, cfg.RejectExpired, cfg.RejectUnexpired, cfg.NotAfterStart, cfg.NotAfterLimit, extKeyUsages, rejectExtIds, cfg.AcceptSHA1)
return cv, nil
}
// NotBeforeRL configures rate limits based on certificate not_before's age.
type NotBeforeRL struct {
AgeThreshold time.Duration
RateLimit float64
}
type LogHandlerOpts struct {
NotBeforeRL *NotBeforeRL
DedupRL float64
MaxCertChainBytes int64
}
// NewLogHandler creates a Tessera based CT log plugged into HTTP handlers.
//
// HTTP server handlers implement static-ct-api submission APIs:
// https://c2sp.org/static-ct-api#submission-apis.
// It populates the data served via monitoring APIs (https://c2sp.org/static-ct-api#submission-apis)
// but it _does not_ implement monitoring APIs itself. Monitoring APIs should
// be served independently, either through the storage's system serving
// infrastructure directly (GCS over HTTPS for instance), or with an
// independent serving stack of your choice.
func NewLogHandler(ctx context.Context, origin string, signer crypto.Signer, cfg ChainValidationConfig, cs storage.CreateStorage, httpDeadline time.Duration, maskInternalErrors bool, pathPrefix string, opts LogHandlerOpts) (http.Handler, error) {
cv, err := newChainValidator(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("newCertValidationOpts(): %v", err)
}
log, err := ct.NewLog(ctx, origin, signer, cv, cs, sysTimeSource)
if err != nil {
return nil, fmt.Errorf("newLog(): %v", err)
}
ctOpts := &ct.HandlerOptions{
Deadline: httpDeadline,
RequestLog: &ct.DefaultRequestLog{},
MaskInternalErrors: maskInternalErrors,
TimeSource: sysTimeSource,
PathPrefix: pathPrefix,
}
if opts.NotBeforeRL != nil {
ctOpts.RateLimits.NotBefore(opts.NotBeforeRL.AgeThreshold, opts.NotBeforeRL.RateLimit)
}
if opts.DedupRL >= 0 {
ctOpts.RateLimits.Dedup(opts.DedupRL)
}
handlers := ct.NewPathHandlers(ctx, ctOpts, log)
mux := http.NewServeMux()
// Register handlers for all the configured logs.
for path, handler := range handlers {
mux.Handle(path, http.MaxBytesHandler(handler, opts.MaxCertChainBytes))
}
// Health checking endpoint.
mux.HandleFunc("/healthz", func(resp http.ResponseWriter, req *http.Request) {
_, _ = fmt.Fprint(resp, "ok")
})
return mux, nil
}