forked from TrueCloudLab/certificates
initial support for CRL
This commit is contained in:
parent
24a6900de2
commit
56926b9012
7 changed files with 214 additions and 1 deletions
|
@ -46,6 +46,7 @@ type Authority interface {
|
||||||
GetRoots() (federation []*x509.Certificate, err error)
|
GetRoots() (federation []*x509.Certificate, err error)
|
||||||
GetFederation() ([]*x509.Certificate, error)
|
GetFederation() ([]*x509.Certificate, error)
|
||||||
Version() authority.Version
|
Version() authority.Version
|
||||||
|
GenerateCertificateRevocationList(force bool) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimeDuration is an alias of provisioner.TimeDuration
|
// 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", "/renew", h.Renew)
|
||||||
r.MethodFunc("POST", "/rekey", h.Rekey)
|
r.MethodFunc("POST", "/rekey", h.Rekey)
|
||||||
r.MethodFunc("POST", "/revoke", h.Revoke)
|
r.MethodFunc("POST", "/revoke", h.Revoke)
|
||||||
|
r.MethodFunc("GET", "/crl", h.CRL)
|
||||||
r.MethodFunc("GET", "/provisioners", h.Provisioners)
|
r.MethodFunc("GET", "/provisioners", h.Provisioners)
|
||||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey)
|
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey)
|
||||||
r.MethodFunc("GET", "/roots", h.Roots)
|
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,9 +5,12 @@ import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -426,6 +429,9 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
||||||
|
|
||||||
// Save as revoked in the Db.
|
// Save as revoked in the Db.
|
||||||
err = a.revoke(revokedCert, rci)
|
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 {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
|
@ -458,6 +464,95 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn
|
||||||
return a.db.Revoke(rci)
|
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.
|
// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.
|
||||||
func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
|
func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
|
||||||
fatal := func(err error) (*tls.Certificate, error) {
|
fatal := func(err error) (*tls.Certificate, error) {
|
||||||
|
|
|
@ -14,6 +14,12 @@ type CertificateAuthorityService interface {
|
||||||
RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error)
|
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
|
// CertificateAuthorityGetter is an interface implemented by a
|
||||||
// CertificateAuthorityService that has a method to get the root certificate.
|
// CertificateAuthorityService that has a method to get the root certificate.
|
||||||
type CertificateAuthorityGetter interface {
|
type CertificateAuthorityGetter interface {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package softcas
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -112,6 +113,17 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1
|
||||||
}, nil
|
}, 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.
|
// CreateCertificateAuthority creates a root or an intermediate certificate.
|
||||||
func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) {
|
func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) {
|
||||||
switch {
|
switch {
|
||||||
|
|
69
db/db.go
69
db/db.go
|
@ -16,6 +16,7 @@ import (
|
||||||
var (
|
var (
|
||||||
certsTable = []byte("x509_certs")
|
certsTable = []byte("x509_certs")
|
||||||
revokedCertsTable = []byte("revoked_x509_certs")
|
revokedCertsTable = []byte("revoked_x509_certs")
|
||||||
|
crlTable = []byte("x509_crl")
|
||||||
revokedSSHCertsTable = []byte("revoked_ssh_certs")
|
revokedSSHCertsTable = []byte("revoked_ssh_certs")
|
||||||
usedOTTTable = []byte("used_ott")
|
usedOTTTable = []byte("used_ott")
|
||||||
sshCertsTable = []byte("ssh_certs")
|
sshCertsTable = []byte("ssh_certs")
|
||||||
|
@ -24,6 +25,9 @@ var (
|
||||||
sshHostPrincipalsTable = []byte("ssh_host_principals")
|
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
|
// ErrAlreadyExists can be returned if the DB attempts to set a key that has
|
||||||
// been previously set.
|
// been previously set.
|
||||||
var ErrAlreadyExists = errors.New("already exists")
|
var ErrAlreadyExists = errors.New("already exists")
|
||||||
|
@ -47,6 +51,9 @@ type AuthDB interface {
|
||||||
IsSSHRevoked(sn string) (bool, error)
|
IsSSHRevoked(sn string) (bool, error)
|
||||||
Revoke(rci *RevokedCertificateInfo) error
|
Revoke(rci *RevokedCertificateInfo) error
|
||||||
RevokeSSH(rci *RevokedCertificateInfo) error
|
RevokeSSH(rci *RevokedCertificateInfo) error
|
||||||
|
GetRevokedCertificates() (*[]RevokedCertificateInfo, error)
|
||||||
|
GetCRL() (*CertificateRevocationListInfo, error)
|
||||||
|
StoreCRL(*CertificateRevocationListInfo) error
|
||||||
GetCertificate(serialNumber string) (*x509.Certificate, error)
|
GetCertificate(serialNumber string) (*x509.Certificate, error)
|
||||||
StoreCertificate(crt *x509.Certificate) error
|
StoreCertificate(crt *x509.Certificate) error
|
||||||
UseToken(id, tok string) (bool, error)
|
UseToken(id, tok string) (bool, error)
|
||||||
|
@ -82,7 +89,7 @@ func New(c *Config) (AuthDB, error) {
|
||||||
tables := [][]byte{
|
tables := [][]byte{
|
||||||
revokedCertsTable, certsTable, usedOTTTable,
|
revokedCertsTable, certsTable, usedOTTTable,
|
||||||
sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable,
|
sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable,
|
||||||
revokedSSHCertsTable,
|
revokedSSHCertsTable, crlTable,
|
||||||
}
|
}
|
||||||
for _, b := range tables {
|
for _, b := range tables {
|
||||||
if err := db.CreateTable(b); err != nil {
|
if err := db.CreateTable(b); err != nil {
|
||||||
|
@ -106,6 +113,14 @@ type RevokedCertificateInfo struct {
|
||||||
MTLS bool
|
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
|
// IsRevoked returns whether or not a certificate with the given identifier
|
||||||
// has been revoked.
|
// has been revoked.
|
||||||
// In the case of an X509 Certificate the `id` should be the Serial Number of
|
// 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.
|
// GetCertificate retrieves a certificate by the serial number.
|
||||||
func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) {
|
func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) {
|
||||||
asn1Data, err := db.Get(certsTable, []byte(serialNumber))
|
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
|
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.
|
// RevokeSSH returns a "NotImplemented" error.
|
||||||
func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error {
|
func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error {
|
||||||
return ErrNotImplemented
|
return ErrNotImplemented
|
||||||
|
|
Loading…
Reference in a new issue