Skip to content

Commit d417ce3

Browse files
author
Raal Goff
committed
implement changes from review
1 parent 668cb6f commit d417ce3

File tree

9 files changed

+221
-43
lines changed

9 files changed

+221
-43
lines changed

api/api.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ type Authority interface {
5050
GetRoots() ([]*x509.Certificate, error)
5151
GetFederation() ([]*x509.Certificate, error)
5252
Version() authority.Version
53-
GenerateCertificateRevocationList(force bool) ([]byte, error)
53+
GenerateCertificateRevocationList() error
54+
GetCertificateRevocationList() ([]byte, error)
5455
}
5556

5657
// TimeDuration is an alias of provisioner.TimeDuration

api/crl.go

+37-7
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,53 @@ package api
22

33
import (
44
"encoding/pem"
5+
"fmt"
6+
"github.com/pkg/errors"
57
"net/http"
68
)
79

8-
// CRL is an HTTP handler that returns the current CRL in PEM format
10+
// CRL is an HTTP handler that returns the current CRL in DER or PEM format
911
func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) {
10-
crlBytes, err := h.Authority.GenerateCertificateRevocationList(false)
12+
crlBytes, err := h.Authority.GetCertificateRevocationList()
13+
14+
_, formatAsPEM := r.URL.Query()["pem"]
1115

1216
if err != nil {
1317
w.WriteHeader(500)
18+
_, err = fmt.Fprintf(w, "%v\n", err)
19+
if err != nil {
20+
panic(errors.Wrap(err, "error writing http response"))
21+
}
1422
return
1523
}
1624

17-
pemBytes := pem.EncodeToMemory(&pem.Block{
18-
Type: "X509 CRL",
19-
Bytes: crlBytes,
20-
})
25+
if crlBytes == nil {
26+
w.WriteHeader(404)
27+
_, err = fmt.Fprintln(w, "No CRL available")
28+
if err != nil {
29+
panic(errors.Wrap(err, "error writing http response"))
30+
}
31+
return
32+
}
33+
34+
if formatAsPEM {
35+
pemBytes := pem.EncodeToMemory(&pem.Block{
36+
Type: "X509 CRL",
37+
Bytes: crlBytes,
38+
})
39+
w.Header().Add("Content-Type", "application/x-pem-file")
40+
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"")
41+
_, err = w.Write(pemBytes)
42+
} else {
43+
w.Header().Add("Content-Type", "application/pkix-crl")
44+
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"")
45+
_, err = w.Write(crlBytes)
46+
}
2147

2248
w.WriteHeader(200)
23-
_, err = w.Write(pemBytes)
49+
50+
if err != nil {
51+
panic(errors.Wrap(err, "error writing http response"))
52+
}
53+
2454
}

authority/authority.go

+70
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ type Authority struct {
6565
sshCAUserFederatedCerts []ssh.PublicKey
6666
sshCAHostFederatedCerts []ssh.PublicKey
6767

68+
// CRL vars
69+
crlChannel chan int
70+
6871
// Do not re-initialize
6972
initOnce bool
7073
startTime time.Time
@@ -553,6 +556,16 @@ func (a *Authority) init() error {
553556
// not be repeated.
554557
a.initOnce = true
555558

559+
// Start the CRL generator
560+
if a.config.CRL != nil {
561+
if a.config.CRL.Generate && a.config.CRL.CacheDuration.Duration > time.Duration(0) {
562+
err := a.startCRLGenerator()
563+
if err != nil {
564+
return err
565+
}
566+
}
567+
}
568+
556569
return nil
557570
}
558571

@@ -646,3 +659,60 @@ func (a *Authority) requiresSCEPService() bool {
646659
func (a *Authority) GetSCEPService() *scep.Service {
647660
return a.scepService
648661
}
662+
663+
func (a *Authority) startCRLGenerator() error {
664+
665+
if a.config.CRL.CacheDuration.Duration > time.Duration(0) {
666+
// Check that there is a valid CRL in the DB right now. If it doesnt exist
667+
// or is expired, generated one now
668+
crlDB, ok := a.db.(db.CertificateRevocationListDB)
669+
if !ok {
670+
return errors.Errorf("CRL Generation requested, but database does not support CRL generation")
671+
}
672+
673+
crlInfo, err := crlDB.GetCRL()
674+
if err != nil {
675+
return errors.Wrap(err, "could not retrieve CRL from database")
676+
}
677+
678+
if crlInfo == nil {
679+
log.Println("No CRL exists in the DB, generating one now")
680+
err = a.GenerateCertificateRevocationList()
681+
if err != nil {
682+
return errors.Wrap(err, "could not generate a CRL")
683+
}
684+
}
685+
686+
if crlInfo.ExpiresAt.Before(time.Now().UTC()) {
687+
log.Printf("Existing CRL has expired (at %v), generating a new one", crlInfo.ExpiresAt)
688+
err = a.GenerateCertificateRevocationList()
689+
if err != nil {
690+
return errors.Wrap(err, "could not generate a CRL")
691+
}
692+
}
693+
694+
log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration)
695+
tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires
696+
if tickerDuration <= 0 {
697+
log.Printf("WARNING: Addition of jitter to CRL generation time %v creates a negative duration (%v). Using 1 minute cacheDuration", a.config.CRL.CacheDuration, tickerDuration)
698+
tickerDuration = time.Minute
699+
}
700+
crlTicker := time.NewTicker(tickerDuration)
701+
702+
go func() {
703+
for {
704+
select {
705+
case <-crlTicker.C:
706+
log.Println("Regenerating CRL")
707+
err := a.GenerateCertificateRevocationList()
708+
if err != nil {
709+
// TODO: log or panic here?
710+
panic(errors.Wrap(err, "authority.crlGenerator encountered an error"))
711+
}
712+
}
713+
}
714+
}()
715+
}
716+
717+
return nil
718+
}

authority/config/config.go

+7
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type Config struct {
6868
TLS *TLSOptions `json:"tls,omitempty"`
6969
Password string `json:"password,omitempty"`
7070
Templates *templates.Templates `json:"templates,omitempty"`
71+
CRL *CRLConfig `json:"crl,omitempty"`
7172
}
7273

7374
// ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer
@@ -99,6 +100,12 @@ type AuthConfig struct {
99100
EnableAdmin bool `json:"enableAdmin,omitempty"`
100101
}
101102

103+
// CRLConfig represents config options for CRL generation
104+
type CRLConfig struct {
105+
Generate bool `json:"generate,omitempty"`
106+
CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"`
107+
}
108+
102109
// init initializes the required fields in the AuthConfig if they are not
103110
// provided.
104111
func (c *AuthConfig) init() {

authority/tls.go

+66-26
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,16 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
408408
p provisioner.Interface
409409
err error
410410
)
411+
412+
// Attempt to get the certificate expiry using the serial number.
413+
cert, err := a.db.GetCertificate(revokeOpts.Serial)
414+
415+
// Revocation of a certificate not in the database may be requested, so fill in the expiry only
416+
// if we can
417+
if err == nil {
418+
rci.ExpiresAt = cert.NotAfter
419+
}
420+
411421
// If not mTLS nor ACME, then get the TokenID of the token.
412422
if !(revokeOpts.MTLS || revokeOpts.ACME) {
413423
token, err := jose.ParseSigned(revokeOpts.OTT)
@@ -474,7 +484,10 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
474484
err = a.revoke(revokedCert, rci)
475485

476486
// Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it
477-
_, _ = a.GenerateCertificateRevocationList(true)
487+
err = a.GenerateCertificateRevocationList()
488+
if err != nil {
489+
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
490+
}
478491
}
479492
switch err {
480493
case nil:
@@ -509,36 +522,64 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn
509522
return a.db.Revoke(rci)
510523
}
511524

512-
// GenerateCertificateRevocationList returns a DER representation of a signed CRL.
513-
// It will look for a valid generated CRL in the database, check if it has expired, and generate
514-
// a new CRL on demand if it has expired (or a CRL does not already exist).
515-
//
516-
// force set to true will force regeneration of the CRL regardless of whether it has actually expired
517-
func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error) {
525+
// GetCertificateRevocationList will return the currently generated CRL from the DB, or a not implemented
526+
// error if the underlying AuthDB does not support CRLs
527+
func (a *Authority) GetCertificateRevocationList() ([]byte, error) {
528+
if a.config.CRL == nil {
529+
return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList")
530+
}
518531

519-
// check for an existing CRL in the database, and return that if its valid
520-
crlInfo, err := a.db.GetCRL()
532+
crlDB, ok := a.db.(db.CertificateRevocationListDB)
533+
if !ok {
534+
return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList")
535+
}
521536

537+
crlInfo, err := crlDB.GetCRL()
522538
if err != nil {
523-
return nil, err
539+
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetCertificateRevocationList")
540+
524541
}
525542

526-
if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) {
527-
return crlInfo.DER, nil
543+
if crlInfo == nil {
544+
return nil, nil
545+
}
546+
547+
return crlInfo.DER, nil
548+
}
549+
550+
// GenerateCertificateRevocationList generates a DER representation of a signed CRL and stores it in the
551+
// database. Returns nil if CRL generation has been disabled in the config
552+
func (a *Authority) GenerateCertificateRevocationList() error {
553+
554+
if a.config.CRL == nil {
555+
// CRL is disabled
556+
return nil
557+
}
558+
559+
crlDB, ok := a.db.(db.CertificateRevocationListDB)
560+
if !ok {
561+
return errors.Errorf("Database does not support CRL generation")
528562
}
529563

530564
// some CAS may not implement the CRLGenerator interface, so check before we proceed
531565
caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator)
532-
533566
if !ok {
534-
return nil, errors.Errorf("CRL Generator not implemented")
567+
return errors.Errorf("CA does not support CRL Generation")
568+
}
569+
570+
crlInfo, err := crlDB.GetCRL()
571+
if err != nil {
572+
return errors.Wrap(err, "could not retrieve CRL from database")
535573
}
536574

537-
revokedList, err := a.db.GetRevokedCertificates()
575+
revokedList, err := crlDB.GetRevokedCertificates()
576+
if err != nil {
577+
return errors.Wrap(err, "could not retrieve revoked certificates list from database")
578+
}
538579

539580
// Number is a monotonically increasing integer (essentially the CRL version number) that we need to
540581
// keep track of and increase every time we generate a new CRL
541-
var n int64 = 0
582+
var n int64
542583
var bn big.Int
543584

544585
if crlInfo != nil {
@@ -561,37 +602,36 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error
561602
}
562603

563604
// Create a RevocationList representation ready for the CAS to sign
564-
// TODO: use a config value for the NextUpdate time duration
565605
// TODO: allow SignatureAlgorithm to be specified?
566606
revocationList := x509.RevocationList{
567607
SignatureAlgorithm: 0,
568608
RevokedCertificates: revokedCertificates,
569609
Number: &bn,
570610
ThisUpdate: time.Now().UTC(),
571-
NextUpdate: time.Now().UTC().Add(time.Minute * 10),
611+
NextUpdate: time.Now().UTC().Add(a.config.CRL.CacheDuration.Duration),
572612
ExtraExtensions: nil,
573613
}
574614

575-
certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList)
615+
certificateRevocationList, err := caCRLGenerator.CreateCRL(&casapi.CreateCRLRequest{RevocationList: &revocationList})
576616
if err != nil {
577-
return nil, err
617+
return errors.Wrap(err, "could not create CRL")
578618
}
579619

580620
// Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the
581-
// expiry time, and the DER-encoded CRL - then store it in the DB
621+
// expiry time, and the DER-encoded CRL
582622
newCRLInfo := db.CertificateRevocationListInfo{
583623
Number: n,
584624
ExpiresAt: revocationList.NextUpdate,
585-
DER: certificateRevocationList,
625+
DER: certificateRevocationList.CRL,
586626
}
587627

588-
err = a.db.StoreCRL(&newCRLInfo)
628+
// Store the CRL in the database ready for retrieval by api endpoints
629+
err = crlDB.StoreCRL(&newCRLInfo)
589630
if err != nil {
590-
return nil, err
631+
return errors.Wrap(err, "could not store CRL in database")
591632
}
592633

593-
// Finally, return our CRL in DER
594-
return certificateRevocationList, nil
634+
return nil
595635
}
596636

597637
// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.

cas/apiv1/requests.go

+10
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,13 @@ type CreateCertificateAuthorityResponse struct {
144144
PrivateKey crypto.PrivateKey
145145
Signer crypto.Signer
146146
}
147+
148+
// CreateCRLRequest is the request to create a Certificate Revocation List.
149+
type CreateCRLRequest struct {
150+
RevocationList *x509.RevocationList
151+
}
152+
153+
// CreateCRLResponse is the response to a Certificate Revocation List request.
154+
type CreateCRLResponse struct {
155+
CRL []byte //the CRL in DER format
156+
}

cas/apiv1/services.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type CertificateAuthorityService interface {
1717
// CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService
1818
// that has a method to create a CRL
1919
type CertificateAuthorityCRLGenerator interface {
20-
CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error)
20+
CreateCRL(req *CreateCRLRequest) (*CreateCRLResponse, error)
2121
}
2222

2323
// CertificateAuthorityGetter is an interface implemented by a

cas/softcas/softcas.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,15 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1
130130
}, nil
131131
}
132132

133-
// CreateCertificateRevocationList will create a new CRL based on the RevocationList passed to it
134-
func (c *SoftCAS) CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) {
133+
// CreateCRL will create a new CRL based on the RevocationList passed to it
134+
func (c *SoftCAS) CreateCRL(req *apiv1.CreateCRLRequest) (*apiv1.CreateCRLResponse, error) {
135135

136-
revocationList, err := x509.CreateRevocationList(rand.Reader, crl, c.CertificateChain[0], c.Signer)
136+
revocationListBytes, err := x509.CreateRevocationList(rand.Reader, req.RevocationList, c.CertificateChain[0], c.Signer)
137137
if err != nil {
138138
return nil, err
139139
}
140140

141-
return revocationList, nil
141+
return &apiv1.CreateCRLResponse{CRL: revocationListBytes}, nil
142142
}
143143

144144
// CreateCertificateAuthority creates a root or an intermediate certificate.

0 commit comments

Comments
 (0)