1 package publisher
2
3 import (
4 "context"
5 "crypto/ecdsa"
6 "crypto/rand"
7 "crypto/sha256"
8 "crypto/tls"
9 "crypto/x509"
10 "encoding/asn1"
11 "encoding/base64"
12 "encoding/json"
13 "errors"
14 "fmt"
15 "math/big"
16 "net/http"
17 "net/url"
18 "strings"
19 "sync"
20 "time"
21
22 ct "github.com/google/certificate-transparency-go"
23 ctClient "github.com/google/certificate-transparency-go/client"
24 "github.com/google/certificate-transparency-go/jsonclient"
25 cttls "github.com/google/certificate-transparency-go/tls"
26 "github.com/prometheus/client_golang/prometheus"
27
28 "github.com/letsencrypt/boulder/canceled"
29 "github.com/letsencrypt/boulder/issuance"
30 blog "github.com/letsencrypt/boulder/log"
31 "github.com/letsencrypt/boulder/metrics"
32 pubpb "github.com/letsencrypt/boulder/publisher/proto"
33 )
34
35
36 type Log struct {
37 logID string
38 uri string
39 client *ctClient.LogClient
40 }
41
42
43
44 type logCache struct {
45 sync.RWMutex
46 logs map[string]*Log
47 }
48
49
50
51 func (c *logCache) AddLog(uri, b64PK, userAgent string, logger blog.Logger) (*Log, error) {
52
53 c.RLock()
54 log, present := c.logs[b64PK]
55 c.RUnlock()
56
57
58 if present {
59 return log, nil
60 }
61
62
63 c.Lock()
64 defer c.Unlock()
65
66
67 log, err := NewLog(uri, b64PK, userAgent, logger)
68 if err != nil {
69 return nil, err
70 }
71 c.logs[b64PK] = log
72 return log, nil
73 }
74
75
76 func (c *logCache) Len() int {
77 c.RLock()
78 defer c.RUnlock()
79 return len(c.logs)
80 }
81
82 type logAdaptor struct {
83 blog.Logger
84 }
85
86 func (la logAdaptor) Printf(s string, args ...interface{}) {
87 la.Logger.Infof(s, args...)
88 }
89
90
91 func NewLog(uri, b64PK, userAgent string, logger blog.Logger) (*Log, error) {
92 url, err := url.Parse(uri)
93 if err != nil {
94 return nil, err
95 }
96 url.Path = strings.TrimSuffix(url.Path, "/")
97
98 pemPK := fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----",
99 b64PK)
100 opts := jsonclient.Options{
101 Logger: logAdaptor{logger},
102 PublicKey: pemPK,
103 UserAgent: userAgent,
104 }
105 httpClient := &http.Client{
106
107
108
109
110 Timeout: time.Minute*2 + time.Second*30,
111
112
113
114
115
116
117
118
119
120 Transport: &http.Transport{
121 MaxIdleConns: http.DefaultTransport.(*http.Transport).MaxIdleConns,
122 IdleConnTimeout: http.DefaultTransport.(*http.Transport).IdleConnTimeout,
123 TLSHandshakeTimeout: http.DefaultTransport.(*http.Transport).TLSHandshakeTimeout,
124
125
126
127
128
129
130
131
132
133
134
135 TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{},
136 },
137 }
138 client, err := ctClient.New(url.String(), httpClient, opts)
139 if err != nil {
140 return nil, fmt.Errorf("making CT client: %s", err)
141 }
142
143 return &Log{
144 logID: b64PK,
145 uri: url.String(),
146 client: client,
147 }, nil
148 }
149
150 type ctSubmissionRequest struct {
151 Chain []string `json:"chain"`
152 }
153
154 type pubMetrics struct {
155 submissionLatency *prometheus.HistogramVec
156 probeLatency *prometheus.HistogramVec
157 errorCount *prometheus.CounterVec
158 }
159
160 func initMetrics(stats prometheus.Registerer) *pubMetrics {
161 submissionLatency := prometheus.NewHistogramVec(
162 prometheus.HistogramOpts{
163 Name: "ct_submission_time_seconds",
164 Help: "Time taken to submit a certificate to a CT log",
165 Buckets: metrics.InternetFacingBuckets,
166 },
167 []string{"log", "status", "http_status"},
168 )
169 stats.MustRegister(submissionLatency)
170
171 probeLatency := prometheus.NewHistogramVec(
172 prometheus.HistogramOpts{
173 Name: "ct_probe_time_seconds",
174 Help: "Time taken to probe a CT log",
175 Buckets: metrics.InternetFacingBuckets,
176 },
177 []string{"log", "status"},
178 )
179 stats.MustRegister(probeLatency)
180
181 errorCount := prometheus.NewCounterVec(
182 prometheus.CounterOpts{
183 Name: "ct_errors_count",
184 Help: "Count of errors by type",
185 },
186 []string{"type"},
187 )
188 stats.MustRegister(errorCount)
189
190 return &pubMetrics{submissionLatency, probeLatency, errorCount}
191 }
192
193
194 type Impl struct {
195 pubpb.UnimplementedPublisherServer
196 log blog.Logger
197 userAgent string
198 issuerBundles map[issuance.IssuerNameID][]ct.ASN1Cert
199 ctLogsCache logCache
200 metrics *pubMetrics
201 }
202
203
204
205 func New(
206 bundles map[issuance.IssuerNameID][]ct.ASN1Cert,
207 userAgent string,
208 logger blog.Logger,
209 stats prometheus.Registerer,
210 ) *Impl {
211 return &Impl{
212 issuerBundles: bundles,
213 userAgent: userAgent,
214 ctLogsCache: logCache{
215 logs: make(map[string]*Log),
216 },
217 log: logger,
218 metrics: initMetrics(stats),
219 }
220 }
221
222
223
224 func (pub *Impl) SubmitToSingleCTWithResult(ctx context.Context, req *pubpb.Request) (*pubpb.Result, error) {
225 cert, err := x509.ParseCertificate(req.Der)
226 if err != nil {
227 pub.log.AuditErrf("Failed to parse certificate: %s", err)
228 return nil, err
229 }
230 chain := []ct.ASN1Cert{{Data: req.Der}}
231 id := issuance.GetIssuerNameID(cert)
232 issuerBundle, ok := pub.issuerBundles[id]
233 if !ok {
234 err := fmt.Errorf("No issuerBundle matching issuerNameID: %d", int64(id))
235 pub.log.AuditErrf("Failed to submit certificate to CT log: %s", err)
236 return nil, err
237 }
238 chain = append(chain, issuerBundle...)
239
240
241
242
243 ctLog, err := pub.ctLogsCache.AddLog(req.LogURL, req.LogPublicKey, pub.userAgent, pub.log)
244 if err != nil {
245 pub.log.AuditErrf("Making Log: %s", err)
246 return nil, err
247 }
248
249 isPrecert := req.Precert
250
251 sct, err := pub.singleLogSubmit(ctx, chain, isPrecert, ctLog)
252 if err != nil {
253 if canceled.Is(err) {
254 return nil, err
255 }
256 var body string
257 var rspErr jsonclient.RspError
258 if errors.As(err, &rspErr) && rspErr.StatusCode < 500 {
259 body = string(rspErr.Body)
260 }
261 pub.log.AuditErrf("Failed to submit certificate to CT log at %s: %s Body=%q",
262 ctLog.uri, err, body)
263 return nil, err
264 }
265
266 sctBytes, err := cttls.Marshal(*sct)
267 if err != nil {
268 return nil, err
269 }
270 return &pubpb.Result{Sct: sctBytes}, nil
271 }
272
273 func (pub *Impl) singleLogSubmit(
274 ctx context.Context,
275 chain []ct.ASN1Cert,
276 isPrecert bool,
277 ctLog *Log,
278 ) (*ct.SignedCertificateTimestamp, error) {
279 var submissionMethod func(context.Context, []ct.ASN1Cert) (*ct.SignedCertificateTimestamp, error)
280 submissionMethod = ctLog.client.AddChain
281 if isPrecert {
282 submissionMethod = ctLog.client.AddPreChain
283 }
284
285 start := time.Now()
286 sct, err := submissionMethod(ctx, chain)
287 took := time.Since(start).Seconds()
288 if err != nil {
289 status := "error"
290 if canceled.Is(err) {
291 status = "canceled"
292 }
293 httpStatus := ""
294 var rspError ctClient.RspError
295 if errors.As(err, &rspError) && rspError.StatusCode != 0 {
296 httpStatus = fmt.Sprintf("%d", rspError.StatusCode)
297 }
298 pub.metrics.submissionLatency.With(prometheus.Labels{
299 "log": ctLog.uri,
300 "status": status,
301 "http_status": httpStatus,
302 }).Observe(took)
303 if isPrecert {
304 pub.metrics.errorCount.WithLabelValues("precert").Inc()
305 } else {
306 pub.metrics.errorCount.WithLabelValues("final").Inc()
307 }
308 return nil, err
309 }
310 pub.metrics.submissionLatency.With(prometheus.Labels{
311 "log": ctLog.uri,
312 "status": "success",
313 "http_status": "",
314 }).Observe(took)
315
316 timestamp := time.Unix(int64(sct.Timestamp)/1000, 0)
317 if time.Until(timestamp) > time.Minute {
318 return nil, fmt.Errorf("SCT Timestamp was too far in the future (%s)", timestamp)
319 }
320
321
322 if isPrecert && time.Until(timestamp) < -10*time.Minute {
323 return nil, fmt.Errorf("SCT Timestamp was too far in the past (%s)", timestamp)
324 }
325
326 return sct, nil
327 }
328
329
330
331 func CreateTestingSignedSCT(req []string, k *ecdsa.PrivateKey, precert bool, timestamp time.Time) []byte {
332 chain := make([]ct.ASN1Cert, len(req))
333 for i, str := range req {
334 b, err := base64.StdEncoding.DecodeString(str)
335 if err != nil {
336 panic("cannot decode chain")
337 }
338 chain[i] = ct.ASN1Cert{Data: b}
339 }
340
341
342 etype := ct.X509LogEntryType
343 if precert {
344 etype = ct.PrecertLogEntryType
345 }
346 leaf, err := ct.MerkleTreeLeafFromRawChain(chain, etype, 0)
347 if err != nil {
348 panic(fmt.Sprintf("failed to create leaf: %s", err))
349 }
350
351
352 rawKey, _ := x509.MarshalPKIXPublicKey(&k.PublicKey)
353 logID := sha256.Sum256(rawKey)
354 timestampMillis := uint64(timestamp.UnixNano()) / 1e6
355 serialized, _ := ct.SerializeSCTSignatureInput(ct.SignedCertificateTimestamp{
356 SCTVersion: ct.V1,
357 LogID: ct.LogID{KeyID: logID},
358 Timestamp: timestampMillis,
359 }, ct.LogEntry{Leaf: *leaf})
360 hashed := sha256.Sum256(serialized)
361 var ecdsaSig struct {
362 R, S *big.Int
363 }
364 ecdsaSig.R, ecdsaSig.S, _ = ecdsa.Sign(rand.Reader, k, hashed[:])
365 sig, _ := asn1.Marshal(ecdsaSig)
366
367
368
369
370 var jsonSCTObj struct {
371 SCTVersion ct.Version `json:"sct_version"`
372 ID string `json:"id"`
373 Timestamp uint64 `json:"timestamp"`
374 Extensions string `json:"extensions"`
375 Signature string `json:"signature"`
376 }
377 jsonSCTObj.SCTVersion = ct.V1
378 jsonSCTObj.ID = base64.StdEncoding.EncodeToString(logID[:])
379 jsonSCTObj.Timestamp = timestampMillis
380 ds := ct.DigitallySigned{
381 Algorithm: cttls.SignatureAndHashAlgorithm{
382 Hash: cttls.SHA256,
383 Signature: cttls.ECDSA,
384 },
385 Signature: sig,
386 }
387 jsonSCTObj.Signature, _ = ds.Base64String()
388
389 jsonSCT, _ := json.Marshal(jsonSCTObj)
390 return jsonSCT
391 }
392
393
394
395
396 func GetCTBundleForChain(chain []*issuance.Certificate) []ct.ASN1Cert {
397 var ctBundle []ct.ASN1Cert
398 for _, cert := range chain {
399 ctBundle = append(ctBundle, ct.ASN1Cert{Data: cert.Raw})
400 }
401 return ctBundle
402 }
403
View as plain text