1 package storer
2
3 import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "crypto/x509"
8 "crypto/x509/pkix"
9 "encoding/asn1"
10 "encoding/base64"
11 "errors"
12 "fmt"
13 "io"
14 "math/big"
15 "time"
16
17 "github.com/aws/aws-sdk-go-v2/service/s3"
18 "github.com/aws/aws-sdk-go-v2/service/s3/types"
19 smithyhttp "github.com/aws/smithy-go/transport/http"
20 "github.com/jmhodges/clock"
21 "github.com/prometheus/client_golang/prometheus"
22 "google.golang.org/protobuf/types/known/emptypb"
23
24 "github.com/letsencrypt/boulder/crl"
25 cspb "github.com/letsencrypt/boulder/crl/storer/proto"
26 "github.com/letsencrypt/boulder/issuance"
27 blog "github.com/letsencrypt/boulder/log"
28 )
29
30
31
32 type simpleS3 interface {
33 PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
34 GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
35 }
36
37 type crlStorer struct {
38 cspb.UnimplementedCRLStorerServer
39 s3Client simpleS3
40 s3Bucket string
41 issuers map[issuance.IssuerNameID]*issuance.Certificate
42 uploadCount *prometheus.CounterVec
43 sizeHistogram *prometheus.HistogramVec
44 latencyHistogram *prometheus.HistogramVec
45 log blog.Logger
46 clk clock.Clock
47 }
48
49 func New(
50 issuers []*issuance.Certificate,
51 s3Client simpleS3,
52 s3Bucket string,
53 stats prometheus.Registerer,
54 log blog.Logger,
55 clk clock.Clock,
56 ) (*crlStorer, error) {
57 issuersByNameID := make(map[issuance.IssuerNameID]*issuance.Certificate, len(issuers))
58 for _, issuer := range issuers {
59 issuersByNameID[issuer.NameID()] = issuer
60 }
61
62 uploadCount := prometheus.NewCounterVec(prometheus.CounterOpts{
63 Name: "crl_storer_uploads",
64 Help: "A counter of the number of CRLs uploaded by crl-storer",
65 }, []string{"issuer", "result"})
66 stats.MustRegister(uploadCount)
67
68 sizeHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{
69 Name: "crl_storer_sizes",
70 Help: "A histogram of the sizes (in bytes) of CRLs uploaded by crl-storer",
71 Buckets: []float64{0, 256, 1024, 4096, 16384, 65536},
72 }, []string{"issuer"})
73 stats.MustRegister(sizeHistogram)
74
75 latencyHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{
76 Name: "crl_storer_upload_times",
77 Help: "A histogram of the time (in seconds) it took crl-storer to upload CRLs",
78 Buckets: []float64{0.01, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000},
79 }, []string{"issuer"})
80 stats.MustRegister(latencyHistogram)
81
82 return &crlStorer{
83 issuers: issuersByNameID,
84 s3Client: s3Client,
85 s3Bucket: s3Bucket,
86 uploadCount: uploadCount,
87 sizeHistogram: sizeHistogram,
88 latencyHistogram: latencyHistogram,
89 log: log,
90 clk: clk,
91 }, nil
92 }
93
94
95
96
97
98
99
100 func (cs *crlStorer) UploadCRL(stream cspb.CRLStorer_UploadCRLServer) error {
101 var issuer *issuance.Certificate
102 var shardIdx int64
103 var crlNumber *big.Int
104 crlBytes := make([]byte, 0)
105
106
107 for {
108 in, err := stream.Recv()
109 if err != nil {
110 if err == io.EOF {
111 break
112 }
113 return err
114 }
115
116 switch payload := in.Payload.(type) {
117 case *cspb.UploadCRLRequest_Metadata:
118 if crlNumber != nil || issuer != nil {
119 return errors.New("got more than one metadata message")
120 }
121 if payload.Metadata.IssuerNameID == 0 || payload.Metadata.Number == 0 {
122 return errors.New("got incomplete metadata message")
123 }
124
125 shardIdx = payload.Metadata.ShardIdx
126 crlNumber = crl.Number(time.Unix(0, payload.Metadata.Number))
127
128 var ok bool
129 issuer, ok = cs.issuers[issuance.IssuerNameID(payload.Metadata.IssuerNameID)]
130 if !ok {
131 return fmt.Errorf("got unrecognized IssuerID: %d", payload.Metadata.IssuerNameID)
132 }
133
134 case *cspb.UploadCRLRequest_CrlChunk:
135 crlBytes = append(crlBytes, payload.CrlChunk...)
136 }
137 }
138
139
140 if issuer == nil || crlNumber == nil {
141 return errors.New("got no metadata message")
142 }
143
144 crlId := crl.Id(issuer.NameID(), int(shardIdx), crlNumber)
145
146 cs.sizeHistogram.WithLabelValues(issuer.Subject.CommonName).Observe(float64(len(crlBytes)))
147
148 crl, err := x509.ParseRevocationList(crlBytes)
149 if err != nil {
150 return fmt.Errorf("parsing CRL for %s: %w", crlId, err)
151 }
152
153 if crl.Number.Cmp(crlNumber) != 0 {
154 return errors.New("got mismatched CRL Number")
155 }
156
157 err = crl.CheckSignatureFrom(issuer.Certificate)
158 if err != nil {
159 return fmt.Errorf("validating signature for %s: %w", crlId, err)
160 }
161
162
163
164
165
166
167 filename := fmt.Sprintf("%d/%d.crl", issuer.NameID(), shardIdx)
168 prevObj, err := cs.s3Client.GetObject(stream.Context(), &s3.GetObjectInput{
169 Bucket: &cs.s3Bucket,
170 Key: &filename,
171 })
172 if err != nil {
173 var smithyErr *smithyhttp.ResponseError
174 if !errors.As(err, &smithyErr) || smithyErr.HTTPStatusCode() != 404 {
175 return fmt.Errorf("getting previous CRL for %s: %w", crlId, err)
176 }
177 cs.log.Infof("No previous CRL found for %s, proceeding", crlId)
178 } else {
179 prevBytes, err := io.ReadAll(prevObj.Body)
180 if err != nil {
181 return fmt.Errorf("downloading previous CRL for %s: %w", crlId, err)
182 }
183
184 prevCRL, err := x509.ParseRevocationList(prevBytes)
185 if err != nil {
186 return fmt.Errorf("parsing previous CRL for %s: %w", crlId, err)
187 }
188
189 idp := getIDPExt(crl.Extensions)
190 prevIdp := getIDPExt(prevCRL.Extensions)
191 if !bytes.Equal(idp, prevIdp) {
192 return fmt.Errorf("IDP does not match previous: %x != %x", idp, prevIdp)
193 }
194
195 if crl.Number.Cmp(prevCRL.Number) <= 0 {
196 return fmt.Errorf("crlNumber not strictly increasing: %d <= %d", crl.Number, prevCRL.Number)
197 }
198 }
199
200
201 start := cs.clk.Now()
202
203 checksum := sha256.Sum256(crlBytes)
204 checksumb64 := base64.StdEncoding.EncodeToString(checksum[:])
205 crlContentType := "application/pkix-crl"
206 _, err = cs.s3Client.PutObject(stream.Context(), &s3.PutObjectInput{
207 Bucket: &cs.s3Bucket,
208 Key: &filename,
209 Body: bytes.NewReader(crlBytes),
210 ChecksumAlgorithm: types.ChecksumAlgorithmSha256,
211 ChecksumSHA256: &checksumb64,
212 ContentType: &crlContentType,
213 Metadata: map[string]string{"crlNumber": crlNumber.String()},
214 })
215
216 latency := cs.clk.Now().Sub(start)
217 cs.latencyHistogram.WithLabelValues(issuer.Subject.CommonName).Observe(latency.Seconds())
218
219 if err != nil {
220 cs.uploadCount.WithLabelValues(issuer.Subject.CommonName, "failed").Inc()
221 cs.log.AuditErrf("CRL upload failed: id=[%s] err=[%s]", crlId, err)
222 return fmt.Errorf("uploading to S3: %w", err)
223 }
224
225 cs.uploadCount.WithLabelValues(issuer.Subject.CommonName, "success").Inc()
226 cs.log.AuditInfof(
227 "CRL uploaded: id=[%s] issuerCN=[%s] thisUpdate=[%s] nextUpdate=[%s] numEntries=[%d]",
228 crlId, issuer.Subject.CommonName, crl.ThisUpdate, crl.NextUpdate, len(crl.RevokedCertificateEntries),
229 )
230
231 return stream.SendAndClose(&emptypb.Empty{})
232 }
233
234
235 func getIDPExt(exts []pkix.Extension) []byte {
236 for _, ext := range exts {
237 if ext.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 28}) {
238 return ext.Value
239 }
240 }
241 return nil
242 }
243
View as plain text