1 package ctpolicy
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "time"
8
9 "github.com/letsencrypt/boulder/core"
10 "github.com/letsencrypt/boulder/ctpolicy/loglist"
11 berrors "github.com/letsencrypt/boulder/errors"
12 blog "github.com/letsencrypt/boulder/log"
13 pubpb "github.com/letsencrypt/boulder/publisher/proto"
14 "github.com/prometheus/client_golang/prometheus"
15 )
16
17 const (
18 succeeded = "succeeded"
19 failed = "failed"
20 )
21
22
23
24 type CTPolicy struct {
25 pub pubpb.PublisherClient
26 sctLogs loglist.List
27 infoLogs loglist.List
28 finalLogs loglist.List
29 stagger time.Duration
30 log blog.Logger
31 winnerCounter *prometheus.CounterVec
32 operatorGroupsGauge *prometheus.GaugeVec
33 shardExpiryGauge *prometheus.GaugeVec
34 }
35
36
37 func New(pub pubpb.PublisherClient, sctLogs loglist.List, infoLogs loglist.List, finalLogs loglist.List, stagger time.Duration, log blog.Logger, stats prometheus.Registerer) *CTPolicy {
38 winnerCounter := prometheus.NewCounterVec(
39 prometheus.CounterOpts{
40 Name: "sct_winner",
41 Help: "Counter of logs which are selected for sct submission, by log URL and result (succeeded or failed).",
42 },
43 []string{"url", "result"},
44 )
45 stats.MustRegister(winnerCounter)
46
47 operatorGroupsGauge := prometheus.NewGaugeVec(
48 prometheus.GaugeOpts{
49 Name: "ct_operator_group_size_gauge",
50 Help: "Gauge for CT operators group size, by operator and log source (capable of providing SCT, informational logs, logs we submit final certs to).",
51 },
52 []string{"operator", "source"},
53 )
54 stats.MustRegister(operatorGroupsGauge)
55
56 shardExpiryGauge := prometheus.NewGaugeVec(
57 prometheus.GaugeOpts{
58 Name: "ct_shard_expiration_seconds",
59 Help: "CT shard end_exclusive field expressed as Unix epoch time, by operator and logID.",
60 },
61 []string{"operator", "logID"},
62 )
63 stats.MustRegister(shardExpiryGauge)
64
65 for op, group := range sctLogs {
66 operatorGroupsGauge.WithLabelValues(op, "sctLogs").Set(float64(len(group)))
67
68 for _, log := range group {
69 if log.EndExclusive.IsZero() {
70
71 shardExpiryGauge.WithLabelValues(op, log.Name).Set(float64(0))
72 } else {
73 shardExpiryGauge.WithLabelValues(op, log.Name).Set(float64(log.EndExclusive.Unix()))
74 }
75 }
76 }
77
78 for op, group := range infoLogs {
79 operatorGroupsGauge.WithLabelValues(op, "infoLogs").Set(float64(len(group)))
80 }
81
82 for op, group := range finalLogs {
83 operatorGroupsGauge.WithLabelValues(op, "finalLogs").Set(float64(len(group)))
84 }
85
86 return &CTPolicy{
87 pub: pub,
88 sctLogs: sctLogs,
89 infoLogs: infoLogs,
90 finalLogs: finalLogs,
91 stagger: stagger,
92 log: log,
93 winnerCounter: winnerCounter,
94 operatorGroupsGauge: operatorGroupsGauge,
95 shardExpiryGauge: shardExpiryGauge,
96 }
97 }
98
99 type result struct {
100 sct []byte
101 url string
102 err error
103 }
104
105
106
107
108
109
110
111 func (ctp *CTPolicy) GetSCTs(ctx context.Context, cert core.CertDER, expiration time.Time) (core.SCTDERs, error) {
112
113
114 subCtx, cancel := context.WithCancel(ctx)
115 defer cancel()
116
117
118 getOne := func(i int, g string) ([]byte, string, error) {
119
120
121
122
123
124 select {
125 case <-subCtx.Done():
126 return nil, "", subCtx.Err()
127 case <-time.After(time.Duration(i-1) * ctp.stagger):
128 }
129
130
131
132 url, key, err := ctp.sctLogs.PickOne(g, expiration)
133 if err != nil {
134 return nil, "", fmt.Errorf("unable to get log info: %w", err)
135 }
136
137 sct, err := ctp.pub.SubmitToSingleCTWithResult(ctx, &pubpb.Request{
138 LogURL: url,
139 LogPublicKey: key,
140 Der: cert,
141 Precert: true,
142 })
143 if err != nil {
144 return nil, url, fmt.Errorf("ct submission to %q (%q) failed: %w", g, url, err)
145 }
146
147 return sct.Sct, url, nil
148 }
149
150
151
152
153 results := make(chan result, len(ctp.sctLogs))
154
155
156
157
158 for i, group := range ctp.sctLogs.Permute() {
159 go func(i int, g string) {
160 sctDER, url, err := getOne(i, g)
161 results <- result{sct: sctDER, url: url, err: err}
162 }(i, group)
163 }
164
165 go ctp.submitPrecertInformational(cert, expiration)
166
167
168
169
170 scts := make(core.SCTDERs, 0)
171 errs := make([]string, 0)
172 for i := 0; i < len(ctp.sctLogs); i++ {
173 res := <-results
174 if res.err != nil {
175 errs = append(errs, res.err.Error())
176 if res.url != "" {
177 ctp.winnerCounter.WithLabelValues(res.url, failed).Inc()
178 }
179 continue
180 }
181 scts = append(scts, res.sct)
182 ctp.winnerCounter.WithLabelValues(res.url, succeeded).Inc()
183 if len(scts) >= 2 {
184 return scts, nil
185 }
186 }
187
188
189
190 if ctx.Err() != nil {
191
192
193 return nil, berrors.MissingSCTsError("failed to get 2 SCTs before ctx finished: %s", ctx.Err())
194 }
195 return nil, berrors.MissingSCTsError("failed to get 2 SCTs, got %d error(s): %s", len(errs), strings.Join(errs, "; "))
196 }
197
198
199
200
201 func (ctp *CTPolicy) submitAllBestEffort(blob core.CertDER, isPrecert bool, expiry time.Time) {
202 logs := ctp.finalLogs
203 if isPrecert {
204 logs = ctp.infoLogs
205 }
206
207 for _, group := range logs {
208 for _, log := range group {
209 if log.StartInclusive.After(expiry) || log.EndExclusive.Equal(expiry) || log.EndExclusive.Before(expiry) {
210 continue
211 }
212
213 go func(log loglist.Log) {
214 _, err := ctp.pub.SubmitToSingleCTWithResult(
215 context.Background(),
216 &pubpb.Request{
217 LogURL: log.Url,
218 LogPublicKey: log.Key,
219 Der: blob,
220 Precert: isPrecert,
221 },
222 )
223 if err != nil {
224 ctp.log.Warningf("ct submission of cert to log %q failed: %s", log.Url, err)
225 }
226 }(log)
227 }
228 }
229
230 }
231
232
233
234 func (ctp *CTPolicy) submitPrecertInformational(cert core.CertDER, expiration time.Time) {
235 ctp.submitAllBestEffort(cert, true, expiration)
236 }
237
238
239
240 func (ctp *CTPolicy) SubmitFinalCert(cert core.CertDER, expiration time.Time) {
241 ctp.submitAllBestEffort(cert, false, expiration)
242 }
243
View as plain text