boulder/ca/crl.go

204 lines
5.1 KiB
Go

package ca
import (
"crypto/sha256"
"crypto/x509"
"errors"
"fmt"
"io"
"strings"
"google.golang.org/grpc"
"github.com/prometheus/client_golang/prometheus"
capb "github.com/letsencrypt/boulder/ca/proto"
"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
bcrl "github.com/letsencrypt/boulder/crl"
"github.com/letsencrypt/boulder/issuance"
blog "github.com/letsencrypt/boulder/log"
)
type crlImpl struct {
capb.UnsafeCRLGeneratorServer
issuers map[issuance.NameID]*issuance.Issuer
profile *issuance.CRLProfile
maxLogLen int
log blog.Logger
metrics *caMetrics
}
var _ capb.CRLGeneratorServer = (*crlImpl)(nil)
// NewCRLImpl returns a new object which fulfils the ca.proto CRLGenerator
// interface. It uses the list of issuers to determine what issuers it can
// issue CRLs from. lifetime sets the validity period (inclusive) of the
// resulting CRLs.
func NewCRLImpl(
issuers []*issuance.Issuer,
profileConfig issuance.CRLProfileConfig,
maxLogLen int,
logger blog.Logger,
metrics *caMetrics,
) (*crlImpl, error) {
issuersByNameID := make(map[issuance.NameID]*issuance.Issuer, len(issuers))
for _, issuer := range issuers {
issuersByNameID[issuer.NameID()] = issuer
}
profile, err := issuance.NewCRLProfile(profileConfig)
if err != nil {
return nil, fmt.Errorf("loading CRL profile: %w", err)
}
return &crlImpl{
issuers: issuersByNameID,
profile: profile,
maxLogLen: maxLogLen,
log: logger,
metrics: metrics,
}, nil
}
func (ci *crlImpl) GenerateCRL(stream grpc.BidiStreamingServer[capb.GenerateCRLRequest, capb.GenerateCRLResponse]) error {
var issuer *issuance.Issuer
var req *issuance.CRLRequest
rcs := make([]x509.RevocationListEntry, 0)
for {
in, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
return err
}
switch payload := in.Payload.(type) {
case *capb.GenerateCRLRequest_Metadata:
if req != nil {
return errors.New("got more than one metadata message")
}
req, err = ci.metadataToRequest(payload.Metadata)
if err != nil {
return err
}
var ok bool
issuer, ok = ci.issuers[issuance.NameID(payload.Metadata.IssuerNameID)]
if !ok {
return fmt.Errorf("got unrecognized IssuerNameID: %d", payload.Metadata.IssuerNameID)
}
case *capb.GenerateCRLRequest_Entry:
rc, err := ci.entryToRevokedCertificate(payload.Entry)
if err != nil {
return err
}
rcs = append(rcs, *rc)
default:
return errors.New("got empty or malformed message in input stream")
}
}
if req == nil {
return errors.New("no crl metadata received")
}
// Compute a unique ID for this issuer-number-shard combo, to tie together all
// the audit log lines related to its issuance.
logID := blog.LogLineChecksum(fmt.Sprintf("%d", issuer.NameID()) + req.Number.String() + fmt.Sprintf("%d", req.Shard))
ci.log.AuditInfof(
"Signing CRL: logID=[%s] issuer=[%s] number=[%s] shard=[%d] thisUpdate=[%s] numEntries=[%d]",
logID, issuer.Cert.Subject.CommonName, req.Number.String(), req.Shard, req.ThisUpdate, len(rcs),
)
if len(rcs) > 0 {
builder := strings.Builder{}
for i := range len(rcs) {
if builder.Len() == 0 {
fmt.Fprintf(&builder, "Signing CRL: logID=[%s] entries=[", logID)
}
fmt.Fprintf(&builder, "%x:%d,", rcs[i].SerialNumber.Bytes(), rcs[i].ReasonCode)
if builder.Len() >= ci.maxLogLen {
fmt.Fprint(&builder, "]")
ci.log.AuditInfo(builder.String())
builder = strings.Builder{}
}
}
fmt.Fprint(&builder, "]")
ci.log.AuditInfo(builder.String())
}
req.Entries = rcs
crlBytes, err := issuer.IssueCRL(ci.profile, req)
if err != nil {
ci.metrics.noteSignError(err)
return fmt.Errorf("signing crl: %w", err)
}
ci.metrics.signatureCount.With(prometheus.Labels{"purpose": "crl", "issuer": issuer.Name()}).Inc()
hash := sha256.Sum256(crlBytes)
ci.log.AuditInfof(
"Signing CRL success: logID=[%s] size=[%d] hash=[%x]",
logID, len(crlBytes), hash,
)
for i := 0; i < len(crlBytes); i += 1000 {
j := i + 1000
if j > len(crlBytes) {
j = len(crlBytes)
}
err = stream.Send(&capb.GenerateCRLResponse{
Chunk: crlBytes[i:j],
})
if err != nil {
return err
}
if i%1000 == 0 {
ci.log.Debugf("Wrote %d bytes to output stream", i*1000)
}
}
return nil
}
func (ci *crlImpl) metadataToRequest(meta *capb.CRLMetadata) (*issuance.CRLRequest, error) {
if core.IsAnyNilOrZero(meta.IssuerNameID, meta.ThisUpdate, meta.ShardIdx) {
return nil, errors.New("got incomplete metadata message")
}
thisUpdate := meta.ThisUpdate.AsTime()
number := bcrl.Number(thisUpdate)
return &issuance.CRLRequest{
Number: number,
Shard: meta.ShardIdx,
ThisUpdate: thisUpdate,
}, nil
}
func (ci *crlImpl) entryToRevokedCertificate(entry *corepb.CRLEntry) (*x509.RevocationListEntry, error) {
serial, err := core.StringToSerial(entry.Serial)
if err != nil {
return nil, err
}
if core.IsAnyNilOrZero(entry.RevokedAt) {
return nil, errors.New("got empty or zero revocation timestamp")
}
revokedAt := entry.RevokedAt.AsTime()
return &x509.RevocationListEntry{
SerialNumber: serial,
RevocationTime: revokedAt,
ReasonCode: int(entry.Reason),
}, nil
}