github.com/letsencrypt/boulder@v0.20251208.0/ca/crl.go (about) 1 package ca 2 3 import ( 4 "crypto/sha256" 5 "crypto/x509" 6 "errors" 7 "fmt" 8 "io" 9 "strings" 10 11 "google.golang.org/grpc" 12 13 "github.com/prometheus/client_golang/prometheus" 14 15 capb "github.com/letsencrypt/boulder/ca/proto" 16 "github.com/letsencrypt/boulder/core" 17 corepb "github.com/letsencrypt/boulder/core/proto" 18 bcrl "github.com/letsencrypt/boulder/crl" 19 "github.com/letsencrypt/boulder/issuance" 20 blog "github.com/letsencrypt/boulder/log" 21 ) 22 23 type crlImpl struct { 24 capb.UnsafeCRLGeneratorServer 25 issuers map[issuance.NameID]*issuance.Issuer 26 profile *issuance.CRLProfile 27 maxLogLen int 28 log blog.Logger 29 metrics *caMetrics 30 } 31 32 var _ capb.CRLGeneratorServer = (*crlImpl)(nil) 33 34 // NewCRLImpl returns a new object which fulfils the ca.proto CRLGenerator 35 // interface. It uses the list of issuers to determine what issuers it can 36 // issue CRLs from. lifetime sets the validity period (inclusive) of the 37 // resulting CRLs. 38 func NewCRLImpl( 39 issuers []*issuance.Issuer, 40 profileConfig issuance.CRLProfileConfig, 41 maxLogLen int, 42 logger blog.Logger, 43 metrics *caMetrics, 44 ) (*crlImpl, error) { 45 issuerMap := make(map[issuance.NameID]*issuance.Issuer) 46 for _, issuer := range issuers { 47 nameID := issuer.NameID() 48 _, found := issuerMap[nameID] 49 if found { 50 return nil, fmt.Errorf("got two issuers with same NameID (%q)", issuer.Name()) 51 } 52 issuerMap[nameID] = issuer 53 } 54 55 profile, err := issuance.NewCRLProfile(profileConfig) 56 if err != nil { 57 return nil, fmt.Errorf("loading CRL profile: %w", err) 58 } 59 60 return &crlImpl{ 61 issuers: issuerMap, 62 profile: profile, 63 maxLogLen: maxLogLen, 64 log: logger, 65 metrics: metrics, 66 }, nil 67 } 68 69 func (ci *crlImpl) GenerateCRL(stream grpc.BidiStreamingServer[capb.GenerateCRLRequest, capb.GenerateCRLResponse]) error { 70 var issuer *issuance.Issuer 71 var req *issuance.CRLRequest 72 rcs := make([]x509.RevocationListEntry, 0) 73 74 for { 75 in, err := stream.Recv() 76 if err != nil { 77 if err == io.EOF { 78 break 79 } 80 return err 81 } 82 83 switch payload := in.Payload.(type) { 84 case *capb.GenerateCRLRequest_Metadata: 85 if req != nil { 86 return errors.New("got more than one metadata message") 87 } 88 89 req, err = ci.metadataToRequest(payload.Metadata) 90 if err != nil { 91 return err 92 } 93 94 var ok bool 95 issuer, ok = ci.issuers[issuance.NameID(payload.Metadata.IssuerNameID)] 96 if !ok { 97 return fmt.Errorf("got unrecognized IssuerNameID: %d", payload.Metadata.IssuerNameID) 98 } 99 100 case *capb.GenerateCRLRequest_Entry: 101 rc, err := ci.entryToRevokedCertificate(payload.Entry) 102 if err != nil { 103 return err 104 } 105 106 rcs = append(rcs, *rc) 107 108 default: 109 return errors.New("got empty or malformed message in input stream") 110 } 111 } 112 113 if req == nil { 114 return errors.New("no crl metadata received") 115 } 116 117 // Compute a unique ID for this issuer-number-shard combo, to tie together all 118 // the audit log lines related to its issuance. 119 logID := blog.LogLineChecksum(fmt.Sprintf("%d", issuer.NameID()) + req.Number.String() + fmt.Sprintf("%d", req.Shard)) 120 ci.log.AuditInfof( 121 "Signing CRL: logID=[%s] issuer=[%s] number=[%s] shard=[%d] thisUpdate=[%s] numEntries=[%d]", 122 logID, issuer.Cert.Subject.CommonName, req.Number.String(), req.Shard, req.ThisUpdate, len(rcs), 123 ) 124 125 if len(rcs) > 0 { 126 builder := strings.Builder{} 127 for i := range len(rcs) { 128 if builder.Len() == 0 { 129 fmt.Fprintf(&builder, "Signing CRL: logID=[%s] entries=[", logID) 130 } 131 132 fmt.Fprintf(&builder, "%x:%d,", rcs[i].SerialNumber.Bytes(), rcs[i].ReasonCode) 133 134 if builder.Len() >= ci.maxLogLen { 135 fmt.Fprint(&builder, "]") 136 ci.log.AuditInfo(builder.String()) 137 builder = strings.Builder{} 138 } 139 } 140 fmt.Fprint(&builder, "]") 141 ci.log.AuditInfo(builder.String()) 142 } 143 144 req.Entries = rcs 145 146 crlBytes, err := issuer.IssueCRL(ci.profile, req) 147 if err != nil { 148 ci.metrics.noteSignError(err) 149 return fmt.Errorf("signing crl: %w", err) 150 } 151 ci.metrics.signatureCount.With(prometheus.Labels{"purpose": "crl", "issuer": issuer.Name()}).Inc() 152 153 hash := sha256.Sum256(crlBytes) 154 ci.log.AuditInfof( 155 "Signing CRL success: logID=[%s] size=[%d] hash=[%x]", 156 logID, len(crlBytes), hash, 157 ) 158 159 for i := 0; i < len(crlBytes); i += 1000 { 160 j := min(i+1000, len(crlBytes)) 161 err = stream.Send(&capb.GenerateCRLResponse{ 162 Chunk: crlBytes[i:j], 163 }) 164 if err != nil { 165 return err 166 } 167 if i%1000 == 0 { 168 ci.log.Debugf("Wrote %d bytes to output stream", i*1000) 169 } 170 } 171 172 return nil 173 } 174 175 func (ci *crlImpl) metadataToRequest(meta *capb.CRLMetadata) (*issuance.CRLRequest, error) { 176 if core.IsAnyNilOrZero(meta.IssuerNameID, meta.ThisUpdate, meta.ShardIdx) { 177 return nil, errors.New("got incomplete metadata message") 178 } 179 thisUpdate := meta.ThisUpdate.AsTime() 180 number := bcrl.Number(thisUpdate) 181 182 return &issuance.CRLRequest{ 183 Number: number, 184 Shard: meta.ShardIdx, 185 ThisUpdate: thisUpdate, 186 }, nil 187 } 188 189 func (ci *crlImpl) entryToRevokedCertificate(entry *corepb.CRLEntry) (*x509.RevocationListEntry, error) { 190 serial, err := core.StringToSerial(entry.Serial) 191 if err != nil { 192 return nil, err 193 } 194 195 if core.IsAnyNilOrZero(entry.RevokedAt) { 196 return nil, errors.New("got empty or zero revocation timestamp") 197 } 198 revokedAt := entry.RevokedAt.AsTime() 199 200 return &x509.RevocationListEntry{ 201 SerialNumber: serial, 202 RevocationTime: revokedAt, 203 ReasonCode: int(entry.Reason), 204 }, nil 205 }