forked from TrueCloudLab/certificates
implement changes from review
This commit is contained in:
parent
668cb6f39c
commit
d417ce3232
9 changed files with 224 additions and 46 deletions
|
@ -50,7 +50,8 @@ type Authority interface {
|
|||
GetRoots() ([]*x509.Certificate, 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
|
||||
|
|
36
api/crl.go
36
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
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -65,6 +65,9 @@ type Authority struct {
|
|||
sshCAUserFederatedCerts []ssh.PublicKey
|
||||
sshCAHostFederatedCerts []ssh.PublicKey
|
||||
|
||||
// CRL vars
|
||||
crlChannel chan int
|
||||
|
||||
// Do not re-initialize
|
||||
initOnce bool
|
||||
startTime time.Time
|
||||
|
@ -553,6 +556,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
|
||||
}
|
||||
|
||||
|
@ -646,3 +659,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
|
||||
}
|
||||
|
|
|
@ -68,6 +68,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
|
||||
|
@ -99,6 +100,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() {
|
||||
|
|
|
@ -408,6 +408,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 nor ACME, then get the TokenID of the token.
|
||||
if !(revokeOpts.MTLS || revokeOpts.ACME) {
|
||||
token, err := jose.ParseSigned(revokeOpts.OTT)
|
||||
|
@ -474,7 +484,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:
|
||||
|
@ -509,36 +522,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) {
|
||||
|
||||
// check for an existing CRL in the database, and return that if its valid
|
||||
crlInfo, err := a.db.GetCRL()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 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")
|
||||
}
|
||||
|
||||
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, errs.Wrap(http.StatusInternalServerError, err, "authority.GetCertificateRevocationList")
|
||||
|
||||
}
|
||||
|
||||
if crlInfo == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) {
|
||||
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")
|
||||
}
|
||||
|
||||
revokedList, err := a.db.GetRevokedCertificates()
|
||||
crlInfo, err := crlDB.GetCRL()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve CRL from database")
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -561,37 +602,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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -130,15 +130,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.
|
||||
|
|
26
db/db.go
26
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
|
||||
ACME bool
|
||||
|
@ -216,7 +221,22 @@ func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) {
|
|||
if err := json.Unmarshal(e.Value, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue