Skip to content

Commit e8fdb70

Browse files
author
Raal Goff
committed
initial support for CRL
1 parent 949c29d commit e8fdb70

File tree

7 files changed

+213
-1
lines changed

7 files changed

+213
-1
lines changed

api/api.go

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type Authority interface {
5050
GetRoots() ([]*x509.Certificate, error)
5151
GetFederation() ([]*x509.Certificate, error)
5252
Version() authority.Version
53+
GenerateCertificateRevocationList(force bool) (string, error)
5354
}
5455

5556
// TimeDuration is an alias of provisioner.TimeDuration
@@ -258,6 +259,7 @@ func (h *caHandler) Route(r Router) {
258259
r.MethodFunc("POST", "/renew", h.Renew)
259260
r.MethodFunc("POST", "/rekey", h.Rekey)
260261
r.MethodFunc("POST", "/revoke", h.Revoke)
262+
r.MethodFunc("GET", "/crl", h.CRL)
261263
r.MethodFunc("GET", "/provisioners", h.Provisioners)
262264
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey)
263265
r.MethodFunc("GET", "/roots", h.Roots)

api/crl.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package api
2+
3+
import "net/http"
4+
5+
// CRL is an HTTP handler that returns the current CRL
6+
func (h *caHandler) CRL(w http.ResponseWriter, r *http.Request) {
7+
crl, err := h.Authority.GenerateCertificateRevocationList(false)
8+
9+
if err != nil {
10+
w.WriteHeader(500)
11+
return
12+
}
13+
14+
w.WriteHeader(200)
15+
_, err = w.Write([]byte(crl))
16+
}

authority/tls.go

+94
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55
"crypto"
66
"crypto/tls"
77
"crypto/x509"
8+
"crypto/x509/pkix"
89
"encoding/asn1"
910
"encoding/base64"
1011
"encoding/json"
1112
"encoding/pem"
1213
"fmt"
1314
"net"
15+
"math/big"
1416
"net/http"
1517
"strings"
1618
"time"
@@ -470,6 +472,9 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
470472

471473
// Save as revoked in the Db.
472474
err = a.revoke(revokedCert, rci)
475+
476+
// Generate a new CRL so CRL requesters will always get an up-to-date CRL whenever they request it
477+
_, _ = a.GenerateCertificateRevocationList(true)
473478
}
474479
switch err {
475480
case nil:
@@ -504,6 +509,95 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn
504509
return a.db.Revoke(rci)
505510
}
506511

512+
// GenerateCertificateRevocationList returns a PEM representation of a signed CRL.
513+
// It will look for a valid generated CRL in the database, check if it has expired, and generate
514+
// a new CRL on demand if it has expired (or a CRL does not already exist).
515+
//
516+
// force set to true will force regeneration of the CRL regardless of whether it has actually expired
517+
func (a *Authority) GenerateCertificateRevocationList(force bool) (string, error) {
518+
519+
// check for an existing CRL in the database, and return that if its valid
520+
crlInfo, err := a.db.GetCRL()
521+
522+
if err != nil {
523+
return "", err
524+
}
525+
526+
if !force && crlInfo != nil && crlInfo.ExpiresAt.After(time.Now().UTC()) {
527+
return crlInfo.PEM, nil
528+
}
529+
530+
// some CAS may not implement the CRLGenerator interface, so check before we proceed
531+
caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator)
532+
533+
if !ok {
534+
return "", errors.Errorf("CRL Generator not implemented")
535+
}
536+
537+
revokedList, err := a.db.GetRevokedCertificates()
538+
539+
// Number is a monotonically increasing integer (essentially the CRL version number) that we need to
540+
// keep track of and increase every time we generate a new CRL
541+
var n int64 = 0
542+
var bn big.Int
543+
544+
if crlInfo != nil {
545+
n = crlInfo.Number + 1
546+
}
547+
bn.SetInt64(n)
548+
549+
// Convert our database db.RevokedCertificateInfo types into the pkix representation ready for the
550+
// CAS to sign it
551+
var revokedCertificates []pkix.RevokedCertificate
552+
553+
for _, revokedCert := range *revokedList {
554+
var sn big.Int
555+
sn.SetString(revokedCert.Serial, 10)
556+
revokedCertificates = append(revokedCertificates, pkix.RevokedCertificate{
557+
SerialNumber: &sn,
558+
RevocationTime: revokedCert.RevokedAt,
559+
Extensions: nil,
560+
})
561+
}
562+
563+
// Create a RevocationList representation ready for the CAS to sign
564+
// TODO: use a config value for the NextUpdate time duration
565+
// TODO: allow SignatureAlgorithm to be specified?
566+
revocationList := x509.RevocationList{
567+
SignatureAlgorithm: 0,
568+
RevokedCertificates: revokedCertificates,
569+
Number: &bn,
570+
ThisUpdate: time.Now().UTC(),
571+
NextUpdate: time.Now().UTC().Add(time.Minute * 10),
572+
ExtraExtensions: nil,
573+
}
574+
575+
certificateRevocationList, err := caCRLGenerator.CreateCertificateRevocationList(&revocationList)
576+
if err != nil {
577+
return "", err
578+
}
579+
580+
// Quick and dirty PEM encoding
581+
// TODO: clean this up
582+
pemCRL := fmt.Sprintf("-----BEGIN X509 CRL-----\n%s\n-----END X509 CRL-----\n", base64.StdEncoding.EncodeToString(certificateRevocationList))
583+
584+
// Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the
585+
// expiry time, and the byte-encoded CRL - then store it in the DB
586+
newCRLInfo := db.CertificateRevocationListInfo{
587+
Number: n,
588+
ExpiresAt: revocationList.NextUpdate,
589+
PEM: pemCRL,
590+
}
591+
592+
err = a.db.StoreCRL(&newCRLInfo)
593+
if err != nil {
594+
return "", err
595+
}
596+
597+
// Finally, return our CRL PEM
598+
return pemCRL, nil
599+
}
600+
507601
// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.
508602
func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
509603
fatal := func(err error) (*tls.Certificate, error) {

cas/apiv1/services.go

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ type CertificateAuthorityService interface {
1414
RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error)
1515
}
1616

17+
// CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService
18+
// that has a method to create a CRL
19+
type CertificateAuthorityCRLGenerator interface {
20+
CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error)
21+
}
22+
1723
// CertificateAuthorityGetter is an interface implemented by a
1824
// CertificateAuthorityService that has a method to get the root certificate.
1925
type CertificateAuthorityGetter interface {

cas/softcas/softcas.go

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package softcas
33
import (
44
"context"
55
"crypto"
6+
"crypto/rand"
67
"crypto/x509"
78
"time"
89

@@ -129,6 +130,17 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1
129130
}, nil
130131
}
131132

133+
// CreateCertificateRevocationList will create a new CRL based on the RevocationList passed to it
134+
func (c *SoftCAS) CreateCertificateRevocationList(crl *x509.RevocationList) ([]byte, error) {
135+
136+
revocationList, err := x509.CreateRevocationList(rand.Reader, crl, c.CertificateChain[0], c.Signer)
137+
if err != nil {
138+
return nil, err
139+
}
140+
141+
return revocationList, nil
142+
}
143+
132144
// CreateCertificateAuthority creates a root or an intermediate certificate.
133145
func (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) {
134146
switch {

db/db.go

+68-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
var (
1717
certsTable = []byte("x509_certs")
1818
revokedCertsTable = []byte("revoked_x509_certs")
19+
crlTable = []byte("x509_crl")
1920
revokedSSHCertsTable = []byte("revoked_ssh_certs")
2021
usedOTTTable = []byte("used_ott")
2122
sshCertsTable = []byte("ssh_certs")
@@ -24,6 +25,9 @@ var (
2425
sshHostPrincipalsTable = []byte("ssh_host_principals")
2526
)
2627

28+
var crlKey = []byte("crl") //TODO: at the moment we store a single CRL in the database, in a dedicated table.
29+
// is this acceptable? probably not....
30+
2731
// ErrAlreadyExists can be returned if the DB attempts to set a key that has
2832
// been previously set.
2933
var ErrAlreadyExists = errors.New("already exists")
@@ -47,6 +51,9 @@ type AuthDB interface {
4751
IsSSHRevoked(sn string) (bool, error)
4852
Revoke(rci *RevokedCertificateInfo) error
4953
RevokeSSH(rci *RevokedCertificateInfo) error
54+
GetRevokedCertificates() (*[]RevokedCertificateInfo, error)
55+
GetCRL() (*CertificateRevocationListInfo, error)
56+
StoreCRL(*CertificateRevocationListInfo) error
5057
GetCertificate(serialNumber string) (*x509.Certificate, error)
5158
StoreCertificate(crt *x509.Certificate) error
5259
UseToken(id, tok string) (bool, error)
@@ -82,7 +89,7 @@ func New(c *Config) (AuthDB, error) {
8289
tables := [][]byte{
8390
revokedCertsTable, certsTable, usedOTTTable,
8491
sshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable,
85-
revokedSSHCertsTable,
92+
revokedSSHCertsTable, crlTable,
8693
}
8794
for _, b := range tables {
8895
if err := db.CreateTable(b); err != nil {
@@ -107,6 +114,14 @@ type RevokedCertificateInfo struct {
107114
ACME bool
108115
}
109116

117+
// CertificateRevocationListInfo contains a CRL in PEM and associated metadata to allow a decision on whether
118+
// to regenerate the CRL or not easier
119+
type CertificateRevocationListInfo struct {
120+
Number int64
121+
ExpiresAt time.Time
122+
PEM string
123+
}
124+
110125
// IsRevoked returns whether or not a certificate with the given identifier
111126
// has been revoked.
112127
// 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 {
189204
}
190205
}
191206

207+
// GetRevokedCertificates gets a list of all revoked certificates.
208+
func (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) {
209+
entries, err := db.List(revokedCertsTable)
210+
if err != nil {
211+
return nil, err
212+
}
213+
var revokedCerts []RevokedCertificateInfo
214+
for _, e := range entries {
215+
var data RevokedCertificateInfo
216+
if err := json.Unmarshal(e.Value, &data); err != nil {
217+
return nil, err
218+
}
219+
revokedCerts = append(revokedCerts, data)
220+
221+
}
222+
return &revokedCerts, nil
223+
}
224+
225+
// StoreCRL stores a CRL in the DB
226+
func (db *DB) StoreCRL(crlInfo *CertificateRevocationListInfo) error {
227+
228+
crlInfoBytes, err := json.Marshal(crlInfo)
229+
if err != nil {
230+
return errors.Wrap(err, "json Marshal error")
231+
}
232+
233+
if err := db.Set(crlTable, crlKey, crlInfoBytes); err != nil {
234+
return errors.Wrap(err, "database Set error")
235+
}
236+
return nil
237+
}
238+
239+
// GetCRL gets the existing CRL from the database
240+
func (db *DB) GetCRL() (*CertificateRevocationListInfo, error) {
241+
crlInfoBytes, err := db.Get(crlTable, crlKey)
242+
243+
if database.IsErrNotFound(err) {
244+
return nil, nil
245+
}
246+
247+
if err != nil {
248+
return nil, errors.Wrap(err, "database Get error")
249+
}
250+
251+
var crlInfo CertificateRevocationListInfo
252+
err = json.Unmarshal(crlInfoBytes, &crlInfo)
253+
if err != nil {
254+
return nil, errors.Wrap(err, "json Unmarshal error")
255+
}
256+
return &crlInfo, err
257+
}
258+
192259
// GetCertificate retrieves a certificate by the serial number.
193260
func (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) {
194261
asn1Data, err := db.Get(certsTable, []byte(serialNumber))

db/simple.go

+15
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ func (s *SimpleDB) Revoke(rci *RevokedCertificateInfo) error {
4141
return ErrNotImplemented
4242
}
4343

44+
// GetRevokedCertificates returns a "NotImplemented" error.
45+
func (s *SimpleDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) {
46+
return nil, ErrNotImplemented
47+
}
48+
49+
// GetCRL returns a "NotImplemented" error.
50+
func (s *SimpleDB) GetCRL() (*CertificateRevocationListInfo, error) {
51+
return nil, ErrNotImplemented
52+
}
53+
54+
// StoreCRL returns a "NotImplemented" error.
55+
func (s *SimpleDB) StoreCRL(crlInfo *CertificateRevocationListInfo) error {
56+
return ErrNotImplemented
57+
}
58+
4459
// RevokeSSH returns a "NotImplemented" error.
4560
func (s *SimpleDB) RevokeSSH(rci *RevokedCertificateInfo) error {
4661
return ErrNotImplemented

0 commit comments

Comments
 (0)