From 56926b9012b4e89377f923f0391db489a01280d4 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Sat, 30 Oct 2021 15:52:50 +0800 Subject: [PATCH 1/6] initial support for CRL --- api/api.go | 2 + api/crl.go | 16 +++++++ authority/tls.go | 95 ++++++++++++++++++++++++++++++++++++++++++ cas/apiv1/services.go | 6 +++ cas/softcas/softcas.go | 12 ++++++ db/db.go | 69 +++++++++++++++++++++++++++++- db/simple.go | 15 +++++++ 7 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 api/crl.go diff --git a/api/api.go b/api/api.go index 30ba03f9..d8cfa15f 100644 --- a/api/api.go +++ b/api/api.go @@ -46,6 +46,7 @@ type Authority interface { GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version + GenerateCertificateRevocationList(force bool) (string, error) } // TimeDuration is an alias of provisioner.TimeDuration @@ -254,6 +255,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("POST", "/renew", h.Renew) r.MethodFunc("POST", "/rekey", h.Rekey) r.MethodFunc("POST", "/revoke", h.Revoke) + r.MethodFunc("GET", "/crl", h.CRL) r.MethodFunc("GET", "/provisioners", h.Provisioners) r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey) r.MethodFunc("GET", "/roots", h.Roots) diff --git a/api/crl.go b/api/crl.go new file mode 100644 index 00000000..50b29d6c --- /dev/null +++ b/api/crl.go @@ -0,0 +1,16 @@ +package api + +import "net/http" + +// CRL is an HTTP handler that returns the current CRL +func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { + crl, err := h.Authority.GenerateCertificateRevocationList(false) + + if err != nil { + w.WriteHeader(500) + return + } + + w.WriteHeader(200) + _, err = w.Write([]byte(crl)) +} diff --git a/authority/tls.go b/authority/tls.go index 839866a2..0bd4a7e7 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -5,9 +5,12 @@ import ( "crypto" "crypto/tls" "crypto/x509" + "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "encoding/pem" + "fmt" + "math/big" "net/http" "time" @@ -426,6 +429,9 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error // Save as revoked in the Db. err = a.revoke(revokedCert, rci) + + // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it + _, _ = a.GenerateCertificateRevocationList(true) } switch err { case nil: @@ -458,6 +464,95 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn return a.db.Revoke(rci) } +// GenerateCertificateRevocationList returns a PEM representation of a signed CRL. +// It will look for a valid generated CRL in the database, check if it has expired, and generate +// a new CRL on demand if it has expired (or a CRL does not already exist). +// +// force set to true will force regeneration of the CRL regardless of whether it has actually expired +func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error) { + + // check for an existing CRL in the database, and return that if its valid + crlInfo, err := a.db.GetCRL() + + if err != nil { + return "", err + } + + if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) { + return crlInfo.PEM, nil + } + + // some CAS may not implement the CRLGenerator interface, so check before we proceed + caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator) + + if !ok { + return "", errors.Errorf("CRL Generator not implemented") + } + + revokedList, err := a.db.GetRevokedCertificates() + + // Number is a monotonically increasing integer (essentially the CRL version number) that we need to + // keep track of and increase every time we generate a new CRL + var n int64 = 0 + var bn big.Int + + if crlInfo != nil { + n = crlInfo.Number + 1 + } + bn.SetInt64(n) + + // Convert our database db.RevokedCertificateInfo types into the pkix representation ready for the + // CAS to sign it + var revokedCertificates []pkix.RevokedCertificate + + for _, revokedCert := range *revokedList { + var sn big.Int + sn.SetString(revokedCert.Serial, 10) + revokedCertificates = append(revokedCertificates, pkix.RevokedCertificate{ + SerialNumber: &sn, + RevocationTime: revokedCert.RevokedAt, + Extensions: nil, + }) + } + + // Create a RevocationList representation ready for the CAS to sign + // TODO: use a config value for the NextUpdate time duration + // TODO: allow SignatureAlgorithm to be specified? + revocationList := x509.RevocationList{ + SignatureAlgorithm: 0, + RevokedCertificates: revokedCertificates, + Number: &bn, + ThisUpdate: time.Now().UTC(), + NextUpdate: time.Now().UTC().Add(time.Minute * 10), + ExtraExtensions: nil, + } + + certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList) + if err != nil { + return "", err + } + + // Quick and dirty PEM encoding + // TODO: clean this up + pemCRL := fmt.Sprintf("-----BEGIN X509 CRL-----\n%s\n-----END X509 CRL-----\n", base64.StdEncoding.EncodeToString(certificateRevocationList)) + + // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the + // expiry time, and the byte-encoded CRL - then store it in the DB + newCRLInfo := db.CertificateRevocationListInfo{ + Number: n, + ExpiresAt: revocationList.NextUpdate, + PEM: pemCRL, + } + + err = a.db.StoreCRL(&newCRLInfo) + if err != nil { + return "", err + } + + // Finally, return our CRL PEM + return pemCRL, nil +} + // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) { fatal := func(err error) (*tls.Certificate, error) { diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index cf9a5470..143c1f17 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -14,6 +14,12 @@ type CertificateAuthorityService interface { RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) } +// CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService +// that has a method to create a CRL +type CertificateAuthorityCRLGenerator interface { + CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) +} + // CertificateAuthorityGetter is an interface implemented by a // CertificateAuthorityService that has a method to get the root certificate. type CertificateAuthorityGetter interface { diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 8e67d016..24e7dd54 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -3,6 +3,7 @@ package softcas import ( "context" "crypto" + "crypto/rand" "crypto/x509" "time" @@ -112,6 +113,17 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 }, nil } +// CreateCertificateRevocationList will create a new CRL based on the RevocationList passed to it +func (c *SoftCAS) CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) { + + revocationList, err := x509.CreateRevocationList(rand.Reader, crl, c.CertificateChain[0], c.Signer) + if err != nil { + return nil, err + } + + return revocationList, nil +} + // CreateCertificateAuthority creates a root or an intermediate certificate. func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) { switch { diff --git a/db/db.go b/db/db.go index 2643e577..5826d7b9 100644 --- a/db/db.go +++ b/db/db.go @@ -16,6 +16,7 @@ import ( var ( certsTable = []byte("x509_certs") revokedCertsTable = []byte("revoked_x509_certs") + crlTable = []byte("x509_crl") revokedSSHCertsTable = []byte("revoked_ssh_certs") usedOTTTable = []byte("used_ott") sshCertsTable = []byte("ssh_certs") @@ -24,6 +25,9 @@ var ( sshHostPrincipalsTable = []byte("ssh_host_principals") ) +var crlKey = []byte("crl") //TODO: at the moment we store a single CRL in the database, in a dedicated table. +// is this acceptable? probably not.... + // ErrAlreadyExists can be returned if the DB attempts to set a key that has // been previously set. var ErrAlreadyExists = errors.New("already exists") @@ -47,6 +51,9 @@ type AuthDB interface { IsSSHRevoked(sn string) (bool, error) Revoke(rci *RevokedCertificateInfo) error RevokeSSH(rci *RevokedCertificateInfo) error + GetRevokedCertificates() (*[]RevokedCertificateInfo, error) + GetCRL() (*CertificateRevocationListInfo, error) + StoreCRL(*CertificateRevocationListInfo) error GetCertificate(serialNumber string) (*x509.Certificate, error) StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) @@ -82,7 +89,7 @@ func New(c *Config) (AuthDB, error) { tables := [][]byte{ revokedCertsTable, certsTable, usedOTTTable, sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable, - revokedSSHCertsTable, + revokedSSHCertsTable, crlTable, } for _, b := range tables { if err := db.CreateTable(b); err != nil { @@ -106,6 +113,14 @@ type RevokedCertificateInfo struct { MTLS bool } +// CertificateRevocationListInfo contains a CRL in PEM and associated metadata to allow a decision on whether +// to regenerate the CRL or not easier +type CertificateRevocationListInfo struct { + Number int64 + ExpiresAt time.Time + PEM string +} + // IsRevoked returns whether or not a certificate with the given identifier // has been revoked. // In the case of an X509 Certificate the `id` should be the Serial Number of @@ -188,6 +203,58 @@ func (db *DB) RevokeSSH(rci *RevokedCertificateInfo) error { } } +// GetRevokedCertificates gets a list of all revoked certificates. +func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { + entries, err := db.List(revokedCertsTable) + if err != nil { + return nil, err + } + var revokedCerts []RevokedCertificateInfo + for _, e := range entries { + var data RevokedCertificateInfo + if err := json.Unmarshal(e.Value, &data); err != nil { + return nil, err + } + revokedCerts = append(revokedCerts, data) + + } + return &revokedCerts, nil +} + +// StoreCRL stores a CRL in the DB +func (db *DB) StoreCRL(crlInfo *CertificateRevocationListInfo) error { + + crlInfoBytes, err := json.Marshal(crlInfo) + if err != nil { + return errors.Wrap(err, "json Marshal error") + } + + if err := db.Set(crlTable, crlKey, crlInfoBytes); err != nil { + return errors.Wrap(err, "database Set error") + } + return nil +} + +// GetCRL gets the existing CRL from the database +func (db *DB) GetCRL() (*CertificateRevocationListInfo, error) { + crlInfoBytes, err := db.Get(crlTable, crlKey) + + if database.IsErrNotFound(err) { + return nil, nil + } + + if err != nil { + return nil, errors.Wrap(err, "database Get error") + } + + var crlInfo CertificateRevocationListInfo + err = json.Unmarshal(crlInfoBytes, &crlInfo) + if err != nil { + return nil, errors.Wrap(err, "json Unmarshal error") + } + return &crlInfo, err +} + // GetCertificate retrieves a certificate by the serial number. func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) { asn1Data, err := db.Get(certsTable, []byte(serialNumber)) diff --git a/db/simple.go b/db/simple.go index 0e5426ec..c34cdb5d 100644 --- a/db/simple.go +++ b/db/simple.go @@ -41,6 +41,21 @@ func (s *SimpleDB) Revoke(rci *RevokedCertificateInfo) error { return ErrNotImplemented } +// GetRevokedCertificates returns a "NotImplemented" error. +func (s *SimpleDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { + return nil, ErrNotImplemented +} + +// GetCRL returns a "NotImplemented" error. +func (s *SimpleDB) GetCRL() (*CertificateRevocationListInfo, error) { + return nil, ErrNotImplemented +} + +// StoreCRL returns a "NotImplemented" error. +func (s *SimpleDB) StoreCRL(crlInfo *CertificateRevocationListInfo) error { + return ErrNotImplemented +} + // RevokeSSH returns a "NotImplemented" error. func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error { return ErrNotImplemented From 8545adea92c039de2aa4d202661febfaec5f498f Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 2 Nov 2021 13:26:07 +0800 Subject: [PATCH 2/6] change GenerateCertificateRevocationList to return DER, store DER in db instead of PEM, nicer PEM encoding of CRL, add Mock stubs --- api/api.go | 2 +- api/api_test.go | 4 ++++ api/crl.go | 16 ++++++++++++---- authority/tls.go | 21 ++++++++------------- db/db.go | 18 +++++++++++++++--- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/api/api.go b/api/api.go index d8cfa15f..75a0397b 100644 --- a/api/api.go +++ b/api/api.go @@ -46,7 +46,7 @@ type Authority interface { GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version - GenerateCertificateRevocationList(force bool) (string, error) + GenerateCertificateRevocationList(force bool) ([]byte, error) } // TimeDuration is an alias of provisioner.TimeDuration diff --git a/api/api_test.go b/api/api_test.go index 89596165..aef6db77 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -580,6 +580,10 @@ type mockAuthority struct { version func() authority.Version } +func (m *mockAuthority) GenerateCertificateRevocationList(force bool) ([]byte, error) { + panic("implement me") +} + // TODO: remove once Authorize is deprecated. func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) { return m.AuthorizeSign(ott) diff --git a/api/crl.go b/api/crl.go index 50b29d6c..46758777 100644 --- a/api/crl.go +++ b/api/crl.go @@ -1,16 +1,24 @@ package api -import "net/http" +import ( + "encoding/pem" + "net/http" +) -// CRL is an HTTP handler that returns the current CRL +// CRL is an HTTP handler that returns the current CRL in PEM format func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { - crl, err := h.Authority.GenerateCertificateRevocationList(false) + crlBytes, err := h.Authority.GenerateCertificateRevocationList(false) if err != nil { w.WriteHeader(500) return } + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: crlBytes, + }) + w.WriteHeader(200) - _, err = w.Write([]byte(crl)) + _, err = w.Write(pemBytes) } diff --git a/authority/tls.go b/authority/tls.go index 0bd4a7e7..b46cb40d 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -9,7 +9,6 @@ import ( "encoding/asn1" "encoding/base64" "encoding/pem" - "fmt" "math/big" "net/http" "time" @@ -469,24 +468,24 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn // a new CRL on demand if it has expired (or a CRL does not already exist). // // force set to true will force regeneration of the CRL regardless of whether it has actually expired -func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error) { +func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error) { // check for an existing CRL in the database, and return that if its valid crlInfo, err := a.db.GetCRL() if err != nil { - return "", err + return nil, err } if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) { - return crlInfo.PEM, nil + return crlInfo.DER, nil } // some CAS may not implement the CRLGenerator interface, so check before we proceed caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator) if !ok { - return "", errors.Errorf("CRL Generator not implemented") + return nil, errors.Errorf("CRL Generator not implemented") } revokedList, err := a.db.GetRevokedCertificates() @@ -529,28 +528,24 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList) if err != nil { - return "", err + return nil, err } - // Quick and dirty PEM encoding - // TODO: clean this up - pemCRL := fmt.Sprintf("-----BEGIN X509 CRL-----\n%s\n-----END X509 CRL-----\n", base64.StdEncoding.EncodeToString(certificateRevocationList)) - // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the // expiry time, and the byte-encoded CRL - then store it in the DB newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, - PEM: pemCRL, + DER: certificateRevocationList, } err = a.db.StoreCRL(&newCRLInfo) if err != nil { - return "", err + return nil, err } // Finally, return our CRL PEM - return pemCRL, nil + return certificateRevocationList, nil } // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. diff --git a/db/db.go b/db/db.go index 5826d7b9..72c4ec71 100644 --- a/db/db.go +++ b/db/db.go @@ -113,12 +113,12 @@ type RevokedCertificateInfo struct { MTLS bool } -// CertificateRevocationListInfo contains a CRL in PEM and associated metadata to allow a decision on whether -// to regenerate the CRL or not easier +// CertificateRevocationListInfo contains a CRL in DER format and associated +// metadata to allow a decision on whether to regenerate the CRL or not easier type CertificateRevocationListInfo struct { Number int64 ExpiresAt time.Time - PEM string + DER []byte } // IsRevoked returns whether or not a certificate with the given identifier @@ -378,6 +378,18 @@ type MockAuthDB struct { MShutdown func() error } +func (m *MockAuthDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { + panic("implement me") +} + +func (m *MockAuthDB) GetCRL() (*CertificateRevocationListInfo, error) { + panic("implement me") +} + +func (m *MockAuthDB) StoreCRL(info *CertificateRevocationListInfo) error { + panic("implement me") +} + // IsRevoked mock. func (m *MockAuthDB) IsRevoked(sn string) (bool, error) { if m.MIsRevoked != nil { From 26cb52a573bef0cc1d3c509d355ba1b2c1afb0cc Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 2 Nov 2021 16:39:29 +0800 Subject: [PATCH 3/6] missed some mentions of PEM when changing the returned format to DER regarding CRL generation --- authority/tls.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index b46cb40d..82179f13 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -463,7 +463,7 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn return a.db.Revoke(rci) } -// GenerateCertificateRevocationList returns a PEM representation of a signed CRL. +// GenerateCertificateRevocationList returns a DER representation of a signed CRL. // It will look for a valid generated CRL in the database, check if it has expired, and generate // a new CRL on demand if it has expired (or a CRL does not already exist). // @@ -532,7 +532,7 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error } // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the - // expiry time, and the byte-encoded CRL - then store it in the DB + // expiry time, and the DER-encoded CRL - then store it in the DB newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, @@ -544,7 +544,7 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error return nil, err } - // Finally, return our CRL PEM + // Finally, return our CRL in DER return certificateRevocationList, nil } From 222b52db130543bc6b6497fecddc8c7e7f084a69 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Thu, 4 Nov 2021 14:05:07 +0800 Subject: [PATCH 4/6] implement changes from review --- api/api.go | 3 +- api/crl.go | 44 +++++++++++++++--- authority/authority.go | 70 +++++++++++++++++++++++++++++ authority/config/config.go | 7 +++ authority/tls.go | 92 +++++++++++++++++++++++++++----------- cas/apiv1/requests.go | 10 +++++ cas/apiv1/services.go | 2 +- cas/softcas/softcas.go | 8 ++-- db/db.go | 28 ++++++++++-- 9 files changed, 221 insertions(+), 43 deletions(-) diff --git a/api/api.go b/api/api.go index 75a0397b..fa7602e1 100644 --- a/api/api.go +++ b/api/api.go @@ -46,7 +46,8 @@ type Authority interface { GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version - GenerateCertificateRevocationList(force bool) ([]byte, error) + GenerateCertificateRevocationList() error + GetCertificateRevocationList() ([]byte, error) } // TimeDuration is an alias of provisioner.TimeDuration diff --git a/api/crl.go b/api/crl.go index 46758777..4d20cfeb 100644 --- a/api/crl.go +++ b/api/crl.go @@ -2,23 +2,53 @@ package api import ( "encoding/pem" + "fmt" + "github.com/pkg/errors" "net/http" ) -// CRL is an HTTP handler that returns the current CRL in PEM format +// CRL is an HTTP handler that returns the current CRL in DER or PEM format func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { - crlBytes, err := h.Authority.GenerateCertificateRevocationList(false) + crlBytes, err := h.Authority.GetCertificateRevocationList() + + _, formatAsPEM := r.URL.Query()["pem"] if err != nil { w.WriteHeader(500) + _, err = fmt.Fprintf(w, "%v\n", err) + if err != nil { + panic(errors.Wrap(err, "error writing http response")) + } return } - pemBytes := pem.EncodeToMemory(&pem.Block{ - Type: "X509 CRL", - Bytes: crlBytes, - }) + if crlBytes == nil { + w.WriteHeader(404) + _, err = fmt.Fprintln(w, "No CRL available") + if err != nil { + panic(errors.Wrap(err, "error writing http response")) + } + return + } + + if formatAsPEM { + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: crlBytes, + }) + w.Header().Add("Content-Type", "application/x-pem-file") + w.Header().Add("Content-Disposition", "attachment; filename=\"crl.pem\"") + _, err = w.Write(pemBytes) + } else { + w.Header().Add("Content-Type", "application/pkix-crl") + w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"") + _, err = w.Write(crlBytes) + } w.WriteHeader(200) - _, err = w.Write(pemBytes) + + if err != nil { + panic(errors.Wrap(err, "error writing http response")) + } + } diff --git a/authority/authority.go b/authority/authority.go index aa8698d7..0b885170 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -64,6 +64,9 @@ type Authority struct { sshCAUserFederatedCerts []ssh.PublicKey sshCAHostFederatedCerts []ssh.PublicKey + // CRL vars + crlChannel chan int + // Do not re-initialize initOnce bool startTime time.Time @@ -550,6 +553,16 @@ func (a *Authority) init() error { // not be repeated. a.initOnce = true + // Start the CRL generator + if a.config.CRL != nil { + if a.config.CRL.Generate && a.config.CRL.CacheDuration.Duration > time.Duration(0) { + err := a.startCRLGenerator() + if err != nil { + return err + } + } + } + return nil } @@ -615,3 +628,60 @@ func (a *Authority) requiresSCEPService() bool { func (a *Authority) GetSCEPService() *scep.Service { return a.scepService } + +func (a *Authority) startCRLGenerator() error { + + if a.config.CRL.CacheDuration.Duration > time.Duration(0) { + // Check that there is a valid CRL in the DB right now. If it doesnt exist + // or is expired, generated one now + crlDB, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return errors.Errorf("CRL Generation requested, but database does not support CRL generation") + } + + crlInfo, err := crlDB.GetCRL() + if err != nil { + return errors.Wrap(err, "could not retrieve CRL from database") + } + + if crlInfo == nil { + log.Println("No CRL exists in the DB, generating one now") + err = a.GenerateCertificateRevocationList() + if err != nil { + return errors.Wrap(err, "could not generate a CRL") + } + } + + if crlInfo.ExpiresAt.Before(time.Now().UTC()) { + log.Printf("Existing CRL has expired (at %v), generating a new one", crlInfo.ExpiresAt) + err = a.GenerateCertificateRevocationList() + if err != nil { + return errors.Wrap(err, "could not generate a CRL") + } + } + + log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) + tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires + if tickerDuration <= 0 { + 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) + tickerDuration = time.Minute + } + crlTicker := time.NewTicker(tickerDuration) + + go func() { + for { + select { + case <-crlTicker.C: + log.Println("Regenerating CRL") + err := a.GenerateCertificateRevocationList() + if err != nil { + // TODO: log or panic here? + panic(errors.Wrap(err, "authority.crlGenerator encountered an error")) + } + } + } + }() + } + + return nil +} diff --git a/authority/config/config.go b/authority/config/config.go index 75c32994..9177dc22 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -64,6 +64,7 @@ type Config struct { TLS *TLSOptions `json:"tls,omitempty"` Password string `json:"password,omitempty"` Templates *templates.Templates `json:"templates,omitempty"` + CRL *CRLConfig `json:"crl,omitempty"` } // ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer @@ -95,6 +96,12 @@ type AuthConfig struct { EnableAdmin bool `json:"enableAdmin,omitempty"` } +// CRLConfig represents config options for CRL generation +type CRLConfig struct { + Generate bool `json:"generate,omitempty"` + CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"` +} + // init initializes the required fields in the AuthConfig if they are not // provided. func (c *AuthConfig) init() { diff --git a/authority/tls.go b/authority/tls.go index 82179f13..8f113342 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -364,6 +364,16 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error p provisioner.Interface err error ) + + // Attempt to get the certificate expiry using the serial number. + cert, err := a.db.GetCertificate(revokeOpts.Serial) + + // Revocation of a certificate not in the database may be requested, so fill in the expiry only + // if we can + if err == nil { + rci.ExpiresAt = cert.NotAfter + } + // If not mTLS then get the TokenID of the token. if !revokeOpts.MTLS { token, err := jose.ParseSigned(revokeOpts.OTT) @@ -430,7 +440,10 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error err = a.revoke(revokedCert, rci) // Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it - _, _ = a.GenerateCertificateRevocationList(true) + err = a.GenerateCertificateRevocationList() + if err != nil { + return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) + } } switch err { case nil: @@ -463,36 +476,64 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn return a.db.Revoke(rci) } -// GenerateCertificateRevocationList returns a DER representation of a signed CRL. -// It will look for a valid generated CRL in the database, check if it has expired, and generate -// a new CRL on demand if it has expired (or a CRL does not already exist). -// -// force set to true will force regeneration of the CRL regardless of whether it has actually expired -func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error) { +// GetCertificateRevocationList will return the currently generated CRL from the DB, or a not implemented +// error if the underlying AuthDB does not support CRLs +func (a *Authority) GetCertificateRevocationList() ([]byte, error) { + if a.config.CRL == nil { + return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") + } - // check for an existing CRL in the database, and return that if its valid - crlInfo, err := a.db.GetCRL() + crlDB, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList") + } + crlInfo, err := crlDB.GetCRL() if err != nil { - return nil, err + return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetCertificateRevocationList") + } - if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) { - return crlInfo.DER, nil + if crlInfo == nil { + return nil, nil + } + + return crlInfo.DER, nil +} + +// GenerateCertificateRevocationList generates a DER representation of a signed CRL and stores it in the +// database. Returns nil if CRL generation has been disabled in the config +func (a *Authority) GenerateCertificateRevocationList() error { + + if a.config.CRL == nil { + // CRL is disabled + return nil + } + + crlDB, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return errors.Errorf("Database does not support CRL generation") } // some CAS may not implement the CRLGenerator interface, so check before we proceed caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator) - if !ok { - return nil, errors.Errorf("CRL Generator not implemented") + return errors.Errorf("CA does not support CRL Generation") + } + + crlInfo, err := crlDB.GetCRL() + if err != nil { + return errors.Wrap(err, "could not retrieve CRL from database") } - revokedList, err := a.db.GetRevokedCertificates() + revokedList, err := crlDB.GetRevokedCertificates() + if err != nil { + return errors.Wrap(err, "could not retrieve revoked certificates list from database") + } // Number is a monotonically increasing integer (essentially the CRL version number) that we need to // keep track of and increase every time we generate a new CRL - var n int64 = 0 + var n int64 var bn big.Int if crlInfo != nil { @@ -515,37 +556,36 @@ func (a *Authority) GenerateCertificateRevocationList(force bool) ([]byte, error } // Create a RevocationList representation ready for the CAS to sign - // TODO: use a config value for the NextUpdate time duration // TODO: allow SignatureAlgorithm to be specified? revocationList := x509.RevocationList{ SignatureAlgorithm: 0, RevokedCertificates: revokedCertificates, Number: &bn, ThisUpdate: time.Now().UTC(), - NextUpdate: time.Now().UTC().Add(time.Minute * 10), + NextUpdate: time.Now().UTC().Add(a.config.CRL.CacheDuration.Duration), ExtraExtensions: nil, } - certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList) + certificateRevocationList, err := caCRLGenerator.CreateCRL(&casapi.CreateCRLRequest{RevocationList: &revocationList}) if err != nil { - return nil, err + return errors.Wrap(err, "could not create CRL") } // Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the - // expiry time, and the DER-encoded CRL - then store it in the DB + // expiry time, and the DER-encoded CRL newCRLInfo := db.CertificateRevocationListInfo{ Number: n, ExpiresAt: revocationList.NextUpdate, - DER: certificateRevocationList, + DER: certificateRevocationList.CRL, } - err = a.db.StoreCRL(&newCRLInfo) + // Store the CRL in the database ready for retrieval by api endpoints + err = crlDB.StoreCRL(&newCRLInfo) if err != nil { - return nil, err + return errors.Wrap(err, "could not store CRL in database") } - // Finally, return our CRL in DER - return certificateRevocationList, nil + return nil } // GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server. diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index bf745c17..630cecce 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -144,3 +144,13 @@ type CreateCertificateAuthorityResponse struct { PrivateKey crypto.PrivateKey Signer crypto.Signer } + +// CreateCRLRequest is the request to create a Certificate Revocation List. +type CreateCRLRequest struct { + RevocationList *x509.RevocationList +} + +// CreateCRLResponse is the response to a Certificate Revocation List request. +type CreateCRLResponse struct { + CRL []byte //the CRL in DER format +} diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 143c1f17..ce7d0364 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -17,7 +17,7 @@ type CertificateAuthorityService interface { // CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService // that has a method to create a CRL type CertificateAuthorityCRLGenerator interface { - CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) + CreateCRL(req *CreateCRLRequest) (*CreateCRLResponse, error) } // CertificateAuthorityGetter is an interface implemented by a diff --git a/cas/softcas/softcas.go b/cas/softcas/softcas.go index 24e7dd54..23e48d45 100644 --- a/cas/softcas/softcas.go +++ b/cas/softcas/softcas.go @@ -113,15 +113,15 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 }, nil } -// CreateCertificateRevocationList will create a new CRL based on the RevocationList passed to it -func (c *SoftCAS) CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) { +// CreateCRL will create a new CRL based on the RevocationList passed to it +func (c *SoftCAS) CreateCRL(req *apiv1.CreateCRLRequest) (*apiv1.CreateCRLResponse, error) { - revocationList, err := x509.CreateRevocationList(rand.Reader, crl, c.CertificateChain[0], c.Signer) + revocationListBytes, err := x509.CreateRevocationList(rand.Reader, req.RevocationList, c.CertificateChain[0], c.Signer) if err != nil { return nil, err } - return revocationList, nil + return &apiv1.CreateCRLResponse{CRL: revocationListBytes}, nil } // CreateCertificateAuthority creates a root or an intermediate certificate. diff --git a/db/db.go b/db/db.go index 72c4ec71..21cea901 100644 --- a/db/db.go +++ b/db/db.go @@ -51,9 +51,6 @@ type AuthDB interface { IsSSHRevoked(sn string) (bool, error) Revoke(rci *RevokedCertificateInfo) error RevokeSSH(rci *RevokedCertificateInfo) error - GetRevokedCertificates() (*[]RevokedCertificateInfo, error) - GetCRL() (*CertificateRevocationListInfo, error) - StoreCRL(*CertificateRevocationListInfo) error GetCertificate(serialNumber string) (*x509.Certificate, error) StoreCertificate(crt *x509.Certificate) error UseToken(id, tok string) (bool, error) @@ -63,6 +60,13 @@ type AuthDB interface { Shutdown() error } +// CertificateRevocationListDB is an interface to indicate whether the DB supports CRL generation +type CertificateRevocationListDB interface { + GetRevokedCertificates() (*[]RevokedCertificateInfo, error) + GetCRL() (*CertificateRevocationListInfo, error) + StoreCRL(*CertificateRevocationListInfo) error +} + // DB is a wrapper over the nosql.DB interface. type DB struct { nosql.DB @@ -109,6 +113,7 @@ type RevokedCertificateInfo struct { ReasonCode int Reason string RevokedAt time.Time + ExpiresAt time.Time TokenID string MTLS bool } @@ -215,7 +220,22 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { if err := json.Unmarshal(e.Value, &data); err != nil { return nil, err } - revokedCerts = append(revokedCerts, data) + + if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(time.Now().UTC()) { + revokedCerts = append(revokedCerts, data) + } else if data.ExpiresAt.IsZero() { + cert, err := db.GetCertificate(data.Serial) + if err != nil { + revokedCerts = append(revokedCerts, data) // a revoked certificate may not be in the database, + // so its expiry date is undiscoverable and will need + // to be added to the crl always + continue + } + + if cert.NotAfter.After(time.Now().UTC()) { + revokedCerts = append(revokedCerts, data) + } + } } return &revokedCerts, nil From 45975b061c36b8cd3525d651635fa39c7a96a110 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 29 Mar 2022 08:51:39 +0800 Subject: [PATCH 5/6] requested changes --- api/crl.go | 2 -- authority/authority.go | 77 ++++++++++++++++++------------------------ authority/tls.go | 4 +-- 3 files changed, 34 insertions(+), 49 deletions(-) diff --git a/api/crl.go b/api/crl.go index 4d20cfeb..c90a77f9 100644 --- a/api/crl.go +++ b/api/crl.go @@ -45,8 +45,6 @@ func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { _, err = w.Write(crlBytes) } - w.WriteHeader(200) - if err != nil { panic(errors.Wrap(err, "error writing http response")) } diff --git a/authority/authority.go b/authority/authority.go index 0b885170..5f52d04e 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "crypto/x509" "encoding/hex" + "fmt" "log" "strings" "sync" @@ -631,57 +632,43 @@ func (a *Authority) GetSCEPService() *scep.Service { func (a *Authority) startCRLGenerator() error { - if a.config.CRL.CacheDuration.Duration > time.Duration(0) { - // Check that there is a valid CRL in the DB right now. If it doesnt exist - // or is expired, generated one now - crlDB, ok := a.db.(db.CertificateRevocationListDB) - if !ok { - return errors.Errorf("CRL Generation requested, but database does not support CRL generation") - } + if a.config.CRL.CacheDuration.Duration <= 0 { + return nil + } - crlInfo, err := crlDB.GetCRL() - if err != nil { - return errors.Wrap(err, "could not retrieve CRL from database") - } + // Check that there is a valid CRL in the DB right now. If it doesn't exist + // or is expired, generate one now + _, ok := a.db.(db.CertificateRevocationListDB) + if !ok { + return errors.Errorf("CRL Generation requested, but database does not support CRL generation") + } - if crlInfo == nil { - log.Println("No CRL exists in the DB, generating one now") - err = a.GenerateCertificateRevocationList() - if err != nil { - return errors.Wrap(err, "could not generate a CRL") - } - } + // Always create a new CRL on startup in case the CA has been down and the time to next expected CRL + // update is less than the cache duration. + err := a.GenerateCertificateRevocationList() + if err != nil { + return errors.Wrap(err, "could not generate a CRL") + } - if crlInfo.ExpiresAt.Before(time.Now().UTC()) { - log.Printf("Existing CRL has expired (at %v), generating a new one", crlInfo.ExpiresAt) - err = a.GenerateCertificateRevocationList() - if err != nil { - return errors.Wrap(err, "could not generate a CRL") - } - } + log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) + tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires + if tickerDuration <= 0 { + panic(fmt.Sprintf("ERROR: Addition of jitter to CRL generation time %v creates a negative duration (%v). Use a CRL generation time of longer than 1 minute.", a.config.CRL.CacheDuration, tickerDuration)) + } + crlTicker := time.NewTicker(tickerDuration) - log.Printf("CRL will be auto-generated every %v", a.config.CRL.CacheDuration) - tickerDuration := a.config.CRL.CacheDuration.Duration - time.Minute // generate the new CRL 1 minute before it expires - if tickerDuration <= 0 { - 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) - tickerDuration = time.Minute - } - crlTicker := time.NewTicker(tickerDuration) - - go func() { - for { - select { - case <-crlTicker.C: - log.Println("Regenerating CRL") - err := a.GenerateCertificateRevocationList() - if err != nil { - // TODO: log or panic here? - panic(errors.Wrap(err, "authority.crlGenerator encountered an error")) - } + go func() { + for { + select { + case <-crlTicker.C: + log.Println("Regenerating CRL") + err := a.GenerateCertificateRevocationList() + if err != nil { + log.Printf("ERROR: authority.crlGenerator encountered an error when regenerating the CRL: %v", err) } } - }() - } + } + }() return nil } diff --git a/authority/tls.go b/authority/tls.go index 8f113342..ba1c4939 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -480,12 +480,12 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn // error if the underlying AuthDB does not support CRLs func (a *Authority) GetCertificateRevocationList() ([]byte, error) { if a.config.CRL == nil { - return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") + return nil, errs.Wrap(http.StatusNotFound, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList") } crlDB, ok := a.db.(db.CertificateRevocationListDB) if !ok { - return nil, errs.Wrap(http.StatusInternalServerError, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList") + return nil, errs.Wrap(http.StatusNotImplemented, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList") } crlInfo, err := crlDB.GetCRL() From 8520c861d554b31eb21e92603b690c06790ff2d4 Mon Sep 17 00:00:00 2001 From: Raal Goff Date: Tue, 5 Apr 2022 11:19:13 +0800 Subject: [PATCH 6/6] implemented some requested changes --- api/api.go | 1 - api/api_test.go | 2 +- api/crl.go | 18 +++++++++--------- authority/authority.go | 15 ++++++++++++--- authority/tls.go | 16 +++++++++------- db/db.go | 6 ++++-- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/api/api.go b/api/api.go index fa7602e1..f20df474 100644 --- a/api/api.go +++ b/api/api.go @@ -46,7 +46,6 @@ type Authority interface { GetRoots() (federation []*x509.Certificate, err error) GetFederation() ([]*x509.Certificate, error) Version() authority.Version - GenerateCertificateRevocationList() error GetCertificateRevocationList() ([]byte, error) } diff --git a/api/api_test.go b/api/api_test.go index aef6db77..7ce54d73 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -580,7 +580,7 @@ type mockAuthority struct { version func() authority.Version } -func (m *mockAuthority) GenerateCertificateRevocationList(force bool) ([]byte, error) { +func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) { panic("implement me") } diff --git a/api/crl.go b/api/crl.go index c90a77f9..45024470 100644 --- a/api/crl.go +++ b/api/crl.go @@ -4,6 +4,7 @@ import ( "encoding/pem" "fmt" "github.com/pkg/errors" + "github.com/smallstep/certificates/errs" "net/http" ) @@ -14,17 +15,16 @@ func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) { _, formatAsPEM := r.URL.Query()["pem"] if err != nil { - w.WriteHeader(500) - _, err = fmt.Fprintf(w, "%v\n", err) - if err != nil { - panic(errors.Wrap(err, "error writing http response")) + + caErr, isCaErr := err.(*errs.Error) + + if isCaErr { + http.Error(w, caErr.Msg, caErr.Status) + return } - return - } - if crlBytes == nil { - w.WriteHeader(404) - _, err = fmt.Fprintln(w, "No CRL available") + w.WriteHeader(500) + _, err = fmt.Fprintf(w, "%v\n", err) if err != nil { panic(errors.Wrap(err, "error writing http response")) } diff --git a/authority/authority.go b/authority/authority.go index 5f52d04e..7414a0d4 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -66,7 +66,7 @@ type Authority struct { sshCAHostFederatedCerts []ssh.PublicKey // CRL vars - crlChannel chan int + crlTicker *time.Ticker // Do not re-initialize initOnce bool @@ -586,6 +586,10 @@ func (a *Authority) IsAdminAPIEnabled() bool { // Shutdown safely shuts down any clients, databases, etc. held by the Authority. func (a *Authority) Shutdown() error { + if a.crlTicker != nil { + a.crlTicker.Stop() + } + if err := a.keyManager.Close(); err != nil { log.Printf("error closing the key manager: %v", err) } @@ -594,6 +598,11 @@ func (a *Authority) Shutdown() error { // CloseForReload closes internal services, to allow a safe reload. func (a *Authority) CloseForReload() { + + if a.crlTicker != nil { + a.crlTicker.Stop() + } + if err := a.keyManager.Close(); err != nil { log.Printf("error closing the key manager: %v", err) } @@ -655,12 +664,12 @@ func (a *Authority) startCRLGenerator() error { if tickerDuration <= 0 { panic(fmt.Sprintf("ERROR: Addition of jitter to CRL generation time %v creates a negative duration (%v). Use a CRL generation time of longer than 1 minute.", a.config.CRL.CacheDuration, tickerDuration)) } - crlTicker := time.NewTicker(tickerDuration) + a.crlTicker = time.NewTicker(tickerDuration) go func() { for { select { - case <-crlTicker.C: + case <-a.crlTicker.C: log.Println("Regenerating CRL") err := a.GenerateCertificateRevocationList() if err != nil { diff --git a/authority/tls.go b/authority/tls.go index ba1c4939..44da6b3a 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -365,13 +365,15 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error err error ) - // Attempt to get the certificate expiry using the serial number. - cert, err := a.db.GetCertificate(revokeOpts.Serial) - - // Revocation of a certificate not in the database may be requested, so fill in the expiry only - // if we can - if err == nil { - rci.ExpiresAt = cert.NotAfter + if revokeOpts.Crt == nil { + // Attempt to get the certificate expiry using the serial number. + cert, err := a.db.GetCertificate(revokeOpts.Serial) + + // Revocation of a certificate not in the database may be requested, so fill in the expiry only + // if we can + if err == nil { + rci.ExpiresAt = cert.NotAfter + } } // If not mTLS then get the TokenID of the token. diff --git a/db/db.go b/db/db.go index 21cea901..883f54ed 100644 --- a/db/db.go +++ b/db/db.go @@ -215,13 +215,15 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { return nil, err } var revokedCerts []RevokedCertificateInfo + now := time.Now().UTC() + for _, e := range entries { var data RevokedCertificateInfo if err := json.Unmarshal(e.Value, &data); err != nil { return nil, err } - if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(time.Now().UTC()) { + if !data.ExpiresAt.IsZero() && data.ExpiresAt.After(now) { revokedCerts = append(revokedCerts, data) } else if data.ExpiresAt.IsZero() { cert, err := db.GetCertificate(data.Serial) @@ -232,7 +234,7 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) { continue } - if cert.NotAfter.After(time.Now().UTC()) { + if cert.NotAfter.After(now) { revokedCerts = append(revokedCerts, data) } }