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  }