...

Source file src/github.com/letsencrypt/boulder/ca/crl.go

Documentation: github.com/letsencrypt/boulder/ca

     1  package ca
     2  
     3  import (
     4  	"crypto/rand"
     5  	"crypto/sha256"
     6  	"crypto/x509"
     7  	"crypto/x509/pkix"
     8  	"encoding/asn1"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"strings"
    13  	"time"
    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.UnimplementedCRLGeneratorServer
    25  	issuers   map[issuance.IssuerNameID]*issuance.Issuer
    26  	lifetime  time.Duration
    27  	idpBase   string
    28  	maxLogLen int
    29  	log       blog.Logger
    30  }
    31  
    32  // NewCRLImpl returns a new object which fulfils the ca.proto CRLGenerator
    33  // interface. It uses the list of issuers to determine what issuers it can
    34  // issue CRLs from. lifetime sets the validity period (inclusive) of the
    35  // resulting CRLs. idpBase is the base URL from which IssuingDistributionPoint
    36  // URIs will constructed; it must use the http:// scheme.
    37  func NewCRLImpl(issuers []*issuance.Issuer, lifetime time.Duration, idpBase string, maxLogLen int, logger blog.Logger) (*crlImpl, error) {
    38  	issuersByNameID := make(map[issuance.IssuerNameID]*issuance.Issuer, len(issuers))
    39  	for _, issuer := range issuers {
    40  		issuersByNameID[issuer.Cert.NameID()] = issuer
    41  	}
    42  
    43  	if lifetime == 0 {
    44  		logger.Warningf("got zero for crl lifetime; setting to default 9 days")
    45  		lifetime = 9 * 24 * time.Hour
    46  	} else if lifetime >= 10*24*time.Hour {
    47  		return nil, fmt.Errorf("crl lifetime cannot be more than 10 days, got %q", lifetime)
    48  	} else if lifetime <= 0*time.Hour {
    49  		return nil, fmt.Errorf("crl lifetime must be positive, got %q", lifetime)
    50  	}
    51  
    52  	if !strings.HasPrefix(idpBase, "http://") {
    53  		return nil, fmt.Errorf("issuingDistributionPoint base URI must use http:// scheme, got %q", idpBase)
    54  	}
    55  	if strings.HasSuffix(idpBase, "/") {
    56  		return nil, fmt.Errorf("issuingDistributionPoint base URI must not end with a slash, got %q", idpBase)
    57  	}
    58  
    59  	return &crlImpl{
    60  		issuers:   issuersByNameID,
    61  		lifetime:  lifetime,
    62  		idpBase:   idpBase,
    63  		maxLogLen: maxLogLen,
    64  		log:       logger,
    65  	}, nil
    66  }
    67  
    68  func (ci *crlImpl) GenerateCRL(stream capb.CRLGenerator_GenerateCRLServer) error {
    69  	var issuer *issuance.Issuer
    70  	var template *x509.RevocationList
    71  	var shard int64
    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 template != nil {
    86  				return errors.New("got more than one metadata message")
    87  			}
    88  
    89  			template, err = ci.metadataToTemplate(payload.Metadata)
    90  			if err != nil {
    91  				return err
    92  			}
    93  
    94  			var ok bool
    95  			issuer, ok = ci.issuers[issuance.IssuerNameID(payload.Metadata.IssuerNameID)]
    96  			if !ok {
    97  				return fmt.Errorf("got unrecognized IssuerNameID: %d", payload.Metadata.IssuerNameID)
    98  			}
    99  
   100  			shard = payload.Metadata.ShardIdx
   101  
   102  		case *capb.GenerateCRLRequest_Entry:
   103  			rc, err := ci.entryToRevokedCertificate(payload.Entry)
   104  			if err != nil {
   105  				return err
   106  			}
   107  
   108  			rcs = append(rcs, *rc)
   109  
   110  		default:
   111  			return errors.New("got empty or malformed message in input stream")
   112  		}
   113  	}
   114  
   115  	if template == nil {
   116  		return errors.New("no crl metadata received")
   117  	}
   118  
   119  	// Add the Issuing Distribution Point extension.
   120  	idp, err := makeIDPExt(ci.idpBase, issuer.Cert.NameID(), shard)
   121  	if err != nil {
   122  		return fmt.Errorf("creating IDP extension: %w", err)
   123  	}
   124  	template.ExtraExtensions = append(template.ExtraExtensions, *idp)
   125  
   126  	// Compute a unique ID for this issuer-number-shard combo, to tie together all
   127  	// the audit log lines related to its issuance.
   128  	logID := blog.LogLineChecksum(fmt.Sprintf("%d", issuer.Cert.NameID()) + template.Number.String() + fmt.Sprintf("%d", shard))
   129  	ci.log.AuditInfof(
   130  		"Signing CRL: logID=[%s] issuer=[%s] number=[%s] shard=[%d] thisUpdate=[%s] nextUpdate=[%s] numEntries=[%d]",
   131  		logID, issuer.Cert.Subject.CommonName, template.Number.String(), shard, template.ThisUpdate, template.NextUpdate, len(rcs),
   132  	)
   133  
   134  	if len(rcs) > 0 {
   135  		builder := strings.Builder{}
   136  		for i := 0; i < len(rcs); i += 1 {
   137  			if builder.Len() == 0 {
   138  				fmt.Fprintf(&builder, "Signing CRL: logID=[%s] entries=[", logID)
   139  			}
   140  
   141  			fmt.Fprintf(&builder, "%x:%d,", rcs[i].SerialNumber.Bytes(), rcs[i].ReasonCode)
   142  
   143  			if builder.Len() >= ci.maxLogLen {
   144  				fmt.Fprint(&builder, "]")
   145  				ci.log.AuditInfo(builder.String())
   146  				builder = strings.Builder{}
   147  			}
   148  		}
   149  		fmt.Fprint(&builder, "]")
   150  		ci.log.AuditInfo(builder.String())
   151  	}
   152  
   153  	template.RevokedCertificateEntries = rcs
   154  
   155  	err = issuer.Linter.CheckCRL(template)
   156  	if err != nil {
   157  		return err
   158  	}
   159  
   160  	crlBytes, err := x509.CreateRevocationList(
   161  		rand.Reader,
   162  		template,
   163  		issuer.Cert.Certificate,
   164  		issuer.Signer,
   165  	)
   166  	if err != nil {
   167  		return fmt.Errorf("signing crl: %w", err)
   168  	}
   169  
   170  	hash := sha256.Sum256(crlBytes)
   171  	ci.log.AuditInfof(
   172  		"Signing CRL success: logID=[%s] size=[%d] hash=[%x]",
   173  		logID, len(crlBytes), hash,
   174  	)
   175  
   176  	for i := 0; i < len(crlBytes); i += 1000 {
   177  		j := i + 1000
   178  		if j > len(crlBytes) {
   179  			j = len(crlBytes)
   180  		}
   181  		err = stream.Send(&capb.GenerateCRLResponse{
   182  			Chunk: crlBytes[i:j],
   183  		})
   184  		if err != nil {
   185  			return err
   186  		}
   187  		if i%1000 == 0 {
   188  			ci.log.Debugf("Wrote %d bytes to output stream", i*1000)
   189  		}
   190  	}
   191  
   192  	return nil
   193  }
   194  
   195  func (ci *crlImpl) metadataToTemplate(meta *capb.CRLMetadata) (*x509.RevocationList, error) {
   196  	if meta.IssuerNameID == 0 || meta.ThisUpdateNS == 0 {
   197  		return nil, errors.New("got incomplete metadata message")
   198  	}
   199  	thisUpdate := time.Unix(0, meta.ThisUpdateNS)
   200  	number := bcrl.Number(thisUpdate)
   201  
   202  	return &x509.RevocationList{
   203  		Number:     number,
   204  		ThisUpdate: thisUpdate,
   205  		NextUpdate: thisUpdate.Add(-time.Second).Add(ci.lifetime),
   206  	}, nil
   207  }
   208  
   209  func (ci *crlImpl) entryToRevokedCertificate(entry *corepb.CRLEntry) (*x509.RevocationListEntry, error) {
   210  	serial, err := core.StringToSerial(entry.Serial)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	if entry.RevokedAtNS == 0 {
   216  		return nil, errors.New("got empty or zero revocation timestamp")
   217  	}
   218  	revokedAt := time.Unix(0, entry.RevokedAtNS)
   219  
   220  	return &x509.RevocationListEntry{
   221  		SerialNumber:   serial,
   222  		RevocationTime: revokedAt,
   223  		ReasonCode:     int(entry.Reason),
   224  	}, nil
   225  }
   226  
   227  // distributionPointName represents the ASN.1 DistributionPointName CHOICE as
   228  // defined in RFC 5280 Section 4.2.1.13. We only use one of the fields, so the
   229  // others are omitted.
   230  type distributionPointName struct {
   231  	// Technically, FullName is of type GeneralNames, which is of type SEQUENCE OF
   232  	// GeneralName. But GeneralName itself is of type CHOICE, and the ans1.Marhsal
   233  	// function doesn't support marshalling structs to CHOICEs, so we have to use
   234  	// asn1.RawValue and encode the GeneralName ourselves.
   235  	FullName []asn1.RawValue `asn1:"optional,tag:0"`
   236  }
   237  
   238  // issuingDistributionPoint represents the ASN.1 IssuingDistributionPoint
   239  // SEQUENCE as defined in RFC 5280 Section 5.2.5. We only use two of the fields,
   240  // so the others are omitted.
   241  type issuingDistributionPoint struct {
   242  	DistributionPoint     distributionPointName `asn1:"optional,tag:0"`
   243  	OnlyContainsUserCerts bool                  `asn1:"optional,tag:1"`
   244  }
   245  
   246  // makeIDPExt returns a critical IssuingDistributionPoint extension containing a
   247  // URI built from the base url, the issuer's NameID, and the shard number. It
   248  // also sets the OnlyContainsUserCerts boolean to true.
   249  func makeIDPExt(base string, issuer issuance.IssuerNameID, shardIdx int64) (*pkix.Extension, error) {
   250  	val := issuingDistributionPoint{
   251  		DistributionPoint: distributionPointName{
   252  			[]asn1.RawValue{ // GeneralNames
   253  				{ // GeneralName
   254  					Class: 2, // context-specific
   255  					Tag:   6, // uniformResourceIdentifier, IA5String
   256  					Bytes: []byte(fmt.Sprintf("%s/%d/%d.crl", base, issuer, shardIdx)),
   257  				},
   258  			},
   259  		},
   260  		OnlyContainsUserCerts: true,
   261  	}
   262  
   263  	valBytes, err := asn1.Marshal(val)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	return &pkix.Extension{
   269  		Id:       asn1.ObjectIdentifier{2, 5, 29, 28}, // id-ce-issuingDistributionPoint
   270  		Value:    valBytes,
   271  		Critical: true,
   272  	}, nil
   273  }
   274  

View as plain text