@@ -3,6 +3,8 @@ package ca
33import (
44 "crypto/rand"
55 "crypto/sha256"
6+ "crypto/x509/pkix"
7+ "encoding/asn1"
68 "errors"
79 "fmt"
810 "io"
@@ -22,11 +24,17 @@ type crlImpl struct {
2224 capb.UnimplementedCRLGeneratorServer
2325 issuers map [issuance.IssuerNameID ]* issuance.Issuer
2426 lifetime time.Duration
27+ idpBase string
2528 maxLogLen int
2629 log blog.Logger
2730}
2831
29- func NewCRLImpl (issuers []* issuance.Issuer , lifetime time.Duration , maxLogLen int , logger blog.Logger ) (* crlImpl , error ) {
32+ // NewCRLImpt 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 ) {
3038 issuersByNameID := make (map [issuance.IssuerNameID ]* issuance.Issuer , len (issuers ))
3139 for _ , issuer := range issuers {
3240 issuersByNameID [issuer .Cert .NameID ()] = issuer
@@ -41,9 +49,17 @@ func NewCRLImpl(issuers []*issuance.Issuer, lifetime time.Duration, maxLogLen in
4149 return nil , fmt .Errorf ("crl lifetime must be positive, got %q" , lifetime )
4250 }
4351
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+
4459 return & crlImpl {
4560 issuers : issuersByNameID ,
4661 lifetime : lifetime ,
62+ idpBase : idpBase ,
4763 maxLogLen : maxLogLen ,
4864 log : logger ,
4965 }, nil
@@ -100,6 +116,13 @@ func (ci *crlImpl) GenerateCRL(stream capb.CRLGenerator_GenerateCRLServer) error
100116 return errors .New ("no crl metadata received" )
101117 }
102118
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+
103126 // Compute a unique ID for this issuer-number-shard combo, to tie together all
104127 // the audit log lines related to its issuance.
105128 logID := blog .LogLineChecksum (fmt .Sprintf ("%d" , issuer .Cert .NameID ()) + template .Number .String () + fmt .Sprintf ("%d" , shard ))
@@ -133,7 +156,7 @@ func (ci *crlImpl) GenerateCRL(stream capb.CRLGenerator_GenerateCRLServer) error
133156
134157 template .RevokedCertificates = rcs
135158
136- err : = issuer .Linter .CheckCRL (template )
159+ err = issuer .Linter .CheckCRL (template )
137160 if err != nil {
138161 return err
139162 }
@@ -185,7 +208,6 @@ func (ci *crlImpl) metadataToTemplate(meta *capb.CRLMetadata) (*crl_x509.Revocat
185208 ThisUpdate : thisUpdate ,
186209 NextUpdate : thisUpdate .Add (- time .Second ).Add (ci .lifetime ),
187210 }, nil
188-
189211}
190212
191213func (ci * crlImpl ) entryToRevokedCertificate (entry * corepb.CRLEntry ) (* crl_x509.RevokedCertificate , error ) {
@@ -211,3 +233,51 @@ func (ci *crlImpl) entryToRevokedCertificate(entry *corepb.CRLEntry) (*crl_x509.
211233 ReasonCode : reason ,
212234 }, nil
213235}
236+
237+ // distributionPointName represents the ASN.1 DistributionPointName CHOICE as
238+ // defined in RFC 5280 Section 4.2.1.13. We only use one of the fields, so the
239+ // others are omitted.
240+ type distributionPointName struct {
241+ // Technically, FullName is of type GeneralNames, which is of type SEQUENCE OF
242+ // GeneralName. But GeneralName itself is of type CHOICE, and the ans1.Marhsal
243+ // function doesn't support marshalling structs to CHOICEs, so we have to use
244+ // asn1.RawValue and encode the GeneralName ourselves.
245+ FullName []asn1.RawValue `asn1:"optional,tag:0"`
246+ }
247+
248+ // issuingDistributionPoint represents the ASN.1 IssuingDistributionPoint
249+ // SEQUENCE as defined in RFC 5280 Section 5.2.5. We only use two of the fields,
250+ // so the others are omitted.
251+ type issuingDistributionPoint struct {
252+ DistributionPoint distributionPointName `asn1:"optional,tag:0"`
253+ OnlyContainsUserCerts bool `asn1:"optional,tag:1"`
254+ }
255+
256+ // makeIDPExt returns a critical IssuingDistributionPoint extension containing a
257+ // URI built from the base url, the issuer's NameID, and the shard number. It
258+ // also sets the OnlyContainsUserCerts boolean to true.
259+ func makeIDPExt (base string , issuer issuance.IssuerNameID , shardIdx int64 ) (* pkix.Extension , error ) {
260+ val := issuingDistributionPoint {
261+ DistributionPoint : distributionPointName {
262+ []asn1.RawValue { // GeneralNames
263+ { // GeneralName
264+ Class : 2 , // context-specific
265+ Tag : 6 , // uniformResourceIdentifier, IA5String
266+ Bytes : []byte (fmt .Sprintf ("%s/%d/%d.crl" , base , issuer , shardIdx )),
267+ },
268+ },
269+ },
270+ OnlyContainsUserCerts : true ,
271+ }
272+
273+ valBytes , err := asn1 .Marshal (val )
274+ if err != nil {
275+ return nil , err
276+ }
277+
278+ return & pkix.Extension {
279+ Id : asn1.ObjectIdentifier {2 , 5 , 29 , 28 }, // id-ce-issuingDistributionPoint
280+ Value : valBytes ,
281+ Critical : true ,
282+ }, nil
283+ }
0 commit comments