forked from TrueCloudLab/certificates
initial support for CRL
This commit is contained in:
parent
949c29d7db
commit
e8fdb703c9
7 changed files with 213 additions and 1 deletions
|
@ -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
16
api/crl.go
Normal 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))
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
69
db/db.go
69
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 {
|
||||
|
@ -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))
|
||||
|
|
15
db/simple.go
15
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
|
||||
|
|
Loading…
Reference in a new issue