initial support for CRL

This commit is contained in:
Raal Goff 2021-10-30 15:52:50 +08:00
parent 949c29d7db
commit e8fdb703c9
7 changed files with 213 additions and 1 deletions

View file

@ -50,6 +50,7 @@ type Authority interface {
GetRoots() ([]*x509.Certificate, error)
GetFederation() ([]*x509.Certificate, error)
Version() authority.Version
GenerateCertificateRevocationList(force bool) (string, error)
}
// TimeDuration is an alias of provisioner.TimeDuration
@ -258,6 +259,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)

16
api/crl.go Normal file
View file

@ -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))
}

View file

@ -5,12 +5,14 @@ import (
"crypto"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"net"
"math/big"
"net/http"
"strings"
"time"
@ -470,6 +472,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:
@ -504,6 +509,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) {

View file

@ -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 {

View file

@ -3,6 +3,7 @@ package softcas
import (
"context"
"crypto"
"crypto/rand"
"crypto/x509"
"time"
@ -129,6 +130,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 {

View file

@ -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 {
@ -107,6 +114,14 @@ type RevokedCertificateInfo struct {
ACME 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
@ -189,6 +204,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))

View file

@ -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