forked from TrueCloudLab/certificates
commit
3e0b603eb4
14 changed files with 744 additions and 62 deletions
|
@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||||
X.509 certificates.
|
X.509 certificates.
|
||||||
- Added provisioner webhooks for augmenting template data and authorizing certificate requests before signing.
|
- Added provisioner webhooks for augmenting template data and authorizing certificate requests before signing.
|
||||||
- Added automatic migration of provisioners when enabling remote managment.
|
- Added automatic migration of provisioners when enabling remote managment.
|
||||||
|
- Added experimental support for CRLs.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- MySQL DSN parsing issues fixed with upgrade to [smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0).
|
- MySQL DSN parsing issues fixed with upgrade to [smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0).
|
||||||
|
|
|
@ -49,6 +49,7 @@ type Authority interface {
|
||||||
GetRoots() ([]*x509.Certificate, error)
|
GetRoots() ([]*x509.Certificate, error)
|
||||||
GetFederation() ([]*x509.Certificate, error)
|
GetFederation() ([]*x509.Certificate, error)
|
||||||
Version() authority.Version
|
Version() authority.Version
|
||||||
|
GetCertificateRevocationList() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustAuthority will be replaced on unit tests.
|
// mustAuthority will be replaced on unit tests.
|
||||||
|
@ -267,6 +268,7 @@ func Route(r Router) {
|
||||||
r.MethodFunc("POST", "/renew", Renew)
|
r.MethodFunc("POST", "/renew", Renew)
|
||||||
r.MethodFunc("POST", "/rekey", Rekey)
|
r.MethodFunc("POST", "/rekey", Rekey)
|
||||||
r.MethodFunc("POST", "/revoke", Revoke)
|
r.MethodFunc("POST", "/revoke", Revoke)
|
||||||
|
r.MethodFunc("GET", "/crl", CRL)
|
||||||
r.MethodFunc("GET", "/provisioners", Provisioners)
|
r.MethodFunc("GET", "/provisioners", Provisioners)
|
||||||
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
|
r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", ProvisionerKey)
|
||||||
r.MethodFunc("GET", "/roots", Roots)
|
r.MethodFunc("GET", "/roots", Roots)
|
||||||
|
|
|
@ -199,6 +199,7 @@ type mockAuthority struct {
|
||||||
getEncryptedKey func(kid string) (string, error)
|
getEncryptedKey func(kid string) (string, error)
|
||||||
getRoots func() ([]*x509.Certificate, error)
|
getRoots func() ([]*x509.Certificate, error)
|
||||||
getFederation func() ([]*x509.Certificate, error)
|
getFederation func() ([]*x509.Certificate, error)
|
||||||
|
getCRL func() ([]byte, error)
|
||||||
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
signSSH func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||||
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
signSSHAddUser func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||||
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
|
renewSSH func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||||
|
@ -212,6 +213,14 @@ type mockAuthority struct {
|
||||||
version func() authority.Version
|
version func() authority.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAuthority) GetCertificateRevocationList() ([]byte, error) {
|
||||||
|
if m.getCRL != nil {
|
||||||
|
return m.getCRL()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.ret1.([]byte), m.err
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove once Authorize is deprecated.
|
// TODO: remove once Authorize is deprecated.
|
||||||
func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
func (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
|
||||||
if m.authorize != nil {
|
if m.authorize != nil {
|
||||||
|
@ -772,6 +781,45 @@ func (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) (
|
||||||
return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err
|
return m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_CRLGeneration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
statusCode int
|
||||||
|
expected []byte
|
||||||
|
}{
|
||||||
|
{"empty", nil, http.StatusOK, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/crl", nil)
|
||||||
|
req = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mockMustAuthority(t, &mockAuthority{ret1: tt.expected, err: tt.err})
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
CRL(w, req)
|
||||||
|
res := w.Result()
|
||||||
|
|
||||||
|
if res.StatusCode != tt.statusCode {
|
||||||
|
t.Errorf("caHandler.CRL StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("caHandler.Root unexpected error = %v", err)
|
||||||
|
}
|
||||||
|
if tt.statusCode == 200 {
|
||||||
|
if !bytes.Equal(bytes.TrimSpace(body), tt.expected) {
|
||||||
|
t.Errorf("caHandler.Root CRL = %s, wants %s", body, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_caHandler_Route(t *testing.T) {
|
func Test_caHandler_Route(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Authority Authority
|
Authority Authority
|
||||||
|
|
32
api/crl.go
Normal file
32
api/crl.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/pem"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/api/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CRL is an HTTP handler that returns the current CRL in DER or PEM format
|
||||||
|
func CRL(w http.ResponseWriter, r *http.Request) {
|
||||||
|
crlBytes, err := mustAuthority(r.Context()).GetCertificateRevocationList()
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, formatAsPEM := r.URL.Query()["pem"]
|
||||||
|
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\"")
|
||||||
|
w.Write(pemBytes)
|
||||||
|
} else {
|
||||||
|
w.Header().Add("Content-Type", "application/pkix-crl")
|
||||||
|
w.Header().Add("Content-Disposition", "attachment; filename=\"crl.der\"")
|
||||||
|
w.Write(crlBytes)
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,6 +73,11 @@ type Authority struct {
|
||||||
sshCAUserFederatedCerts []ssh.PublicKey
|
sshCAUserFederatedCerts []ssh.PublicKey
|
||||||
sshCAHostFederatedCerts []ssh.PublicKey
|
sshCAHostFederatedCerts []ssh.PublicKey
|
||||||
|
|
||||||
|
// CRL vars
|
||||||
|
crlTicker *time.Ticker
|
||||||
|
crlStopper chan struct{}
|
||||||
|
crlMutex sync.Mutex
|
||||||
|
|
||||||
// If true, do not re-initialize
|
// If true, do not re-initialize
|
||||||
initOnce bool
|
initOnce bool
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
|
@ -711,6 +716,18 @@ func (a *Authority) init() error {
|
||||||
a.templates.Data["Step"] = tmplVars
|
a.templates.Data["Step"] = tmplVars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the CRL generator, we can assume the configuration is validated.
|
||||||
|
if a.config.CRL.IsEnabled() {
|
||||||
|
// Default cache duration to the default one
|
||||||
|
if v := a.config.CRL.CacheDuration; v == nil || v.Duration <= 0 {
|
||||||
|
a.config.CRL.CacheDuration = config.DefaultCRLCacheDuration
|
||||||
|
}
|
||||||
|
// Start CRL generator
|
||||||
|
if err := a.startCRLGenerator(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// JWT numeric dates are seconds.
|
// JWT numeric dates are seconds.
|
||||||
a.startTime = time.Now().Truncate(time.Second)
|
a.startTime = time.Now().Truncate(time.Second)
|
||||||
// Set flag indicating that initialization has been completed, and should
|
// Set flag indicating that initialization has been completed, and should
|
||||||
|
@ -777,6 +794,11 @@ func (a *Authority) IsAdminAPIEnabled() bool {
|
||||||
|
|
||||||
// Shutdown safely shuts down any clients, databases, etc. held by the Authority.
|
// Shutdown safely shuts down any clients, databases, etc. held by the Authority.
|
||||||
func (a *Authority) Shutdown() error {
|
func (a *Authority) Shutdown() error {
|
||||||
|
if a.crlTicker != nil {
|
||||||
|
a.crlTicker.Stop()
|
||||||
|
close(a.crlStopper)
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.keyManager.Close(); err != nil {
|
if err := a.keyManager.Close(); err != nil {
|
||||||
log.Printf("error closing the key manager: %v", err)
|
log.Printf("error closing the key manager: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -785,6 +807,11 @@ func (a *Authority) Shutdown() error {
|
||||||
|
|
||||||
// CloseForReload closes internal services, to allow a safe reload.
|
// CloseForReload closes internal services, to allow a safe reload.
|
||||||
func (a *Authority) CloseForReload() {
|
func (a *Authority) CloseForReload() {
|
||||||
|
if a.crlTicker != nil {
|
||||||
|
a.crlTicker.Stop()
|
||||||
|
close(a.crlStopper)
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.keyManager.Close(); err != nil {
|
if err := a.keyManager.Close(); err != nil {
|
||||||
log.Printf("error closing the key manager: %v", err)
|
log.Printf("error closing the key manager: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -825,11 +852,49 @@ func (a *Authority) requiresSCEPService() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSCEPService returns the configured SCEP Service
|
// GetSCEPService returns the configured SCEP Service.
|
||||||
// TODO: this function is intended to exist temporarily
|
//
|
||||||
// in order to make SCEP work more easily. It can be
|
// TODO: this function is intended to exist temporarily in order to make SCEP
|
||||||
// made more correct by using the right interfaces/abstractions
|
// work more easily. It can be made more correct by using the right
|
||||||
// after it works as expected.
|
// interfaces/abstractions after it works as expected.
|
||||||
func (a *Authority) GetSCEPService() *scep.Service {
|
func (a *Authority) GetSCEPService() *scep.Service {
|
||||||
return a.scepService
|
return a.scepService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Authority) startCRLGenerator() error {
|
||||||
|
if !a.config.CRL.IsEnabled() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that there is a valid CRL in the DB right now. If it doesn't exist
|
||||||
|
// or is expired, generate one now
|
||||||
|
_, ok := a.db.(db.CertificateRevocationListDB)
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("CRL Generation requested, but database does not support CRL generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always create a new CRL on startup in case the CA has been down and the
|
||||||
|
// time to next expected CRL update is less than the cache duration.
|
||||||
|
if err := a.GenerateCertificateRevocationList(); err != nil {
|
||||||
|
return errors.Wrap(err, "could not generate a CRL")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.crlStopper = make(chan struct{}, 1)
|
||||||
|
a.crlTicker = time.NewTicker(a.config.CRL.TickerDuration())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-a.crlTicker.C:
|
||||||
|
log.Println("Regenerating CRL")
|
||||||
|
if err := a.GenerateCertificateRevocationList(); err != nil {
|
||||||
|
log.Printf("error regenerating the CRL: %v", err)
|
||||||
|
}
|
||||||
|
case <-a.crlStopper:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -35,8 +35,13 @@ var (
|
||||||
// DefaultEnableSSHCA enable SSH CA features per provisioner or globally
|
// DefaultEnableSSHCA enable SSH CA features per provisioner or globally
|
||||||
// for all provisioners.
|
// for all provisioners.
|
||||||
DefaultEnableSSHCA = false
|
DefaultEnableSSHCA = false
|
||||||
// GlobalProvisionerClaims default claims for the Authority. Can be overridden
|
// DefaultCRLCacheDuration is the default cache duration for the CRL.
|
||||||
// by provisioner specific claims.
|
DefaultCRLCacheDuration = &provisioner.Duration{Duration: 24 * time.Hour}
|
||||||
|
// DefaultCRLExpiredDuration is the default duration in which expired
|
||||||
|
// certificates will remain in the CRL after expiration.
|
||||||
|
DefaultCRLExpiredDuration = time.Hour
|
||||||
|
// GlobalProvisionerClaims is the default duration that expired certificates
|
||||||
|
// remain in the CRL after expiration.
|
||||||
GlobalProvisionerClaims = provisioner.Claims{
|
GlobalProvisionerClaims = provisioner.Claims{
|
||||||
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
|
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
|
||||||
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||||
|
@ -72,12 +77,62 @@ type Config struct {
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Templates *templates.Templates `json:"templates,omitempty"`
|
Templates *templates.Templates `json:"templates,omitempty"`
|
||||||
CommonName string `json:"commonName,omitempty"`
|
CommonName string `json:"commonName,omitempty"`
|
||||||
|
CRL *CRLConfig `json:"crl,omitempty"`
|
||||||
SkipValidation bool `json:"-"`
|
SkipValidation bool `json:"-"`
|
||||||
|
|
||||||
// Keeps record of the filename the Config is read from
|
// Keeps record of the filename the Config is read from
|
||||||
loadedFromFilepath string
|
loadedFromFilepath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CRLConfig represents config options for CRL generation
|
||||||
|
type CRLConfig struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
GenerateOnRevoke bool `json:"generateOnRevoke,omitempty"`
|
||||||
|
CacheDuration *provisioner.Duration `json:"cacheDuration,omitempty"`
|
||||||
|
RenewPeriod *provisioner.Duration `json:"renewPeriod,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled returns if the CRL is enabled.
|
||||||
|
func (c *CRLConfig) IsEnabled() bool {
|
||||||
|
return c != nil && c.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the CRL configuration.
|
||||||
|
func (c *CRLConfig) Validate() error {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.CacheDuration != nil && c.CacheDuration.Duration < 0 {
|
||||||
|
return errors.New("crl.cacheDuration must be greater than or equal to 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.RenewPeriod != nil && c.RenewPeriod.Duration < 0 {
|
||||||
|
return errors.New("crl.renewPeriod must be greater than or equal to 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.RenewPeriod != nil && c.CacheDuration != nil &&
|
||||||
|
c.RenewPeriod.Duration > c.CacheDuration.Duration {
|
||||||
|
return errors.New("crl.cacheDuration must be greater than or equal to crl.renewPeriod")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TickerDuration the renewal ticker duration. This is set by renewPeriod, of it
|
||||||
|
// is not set is ~2/3 of cacheDuration.
|
||||||
|
func (c *CRLConfig) TickerDuration() time.Duration {
|
||||||
|
if !c.IsEnabled() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.RenewPeriod != nil && c.RenewPeriod.Duration > 0 {
|
||||||
|
return c.RenewPeriod.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
return (c.CacheDuration.Duration / 3) * 2
|
||||||
|
}
|
||||||
|
|
||||||
// ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer
|
// ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer
|
||||||
// x509 Certificate blocks.
|
// x509 Certificate blocks.
|
||||||
type ASN1DN struct {
|
type ASN1DN struct {
|
||||||
|
@ -190,6 +245,9 @@ func (c *Config) Init() {
|
||||||
if c.CommonName == "" {
|
if c.CommonName == "" {
|
||||||
c.CommonName = "Step Online CA"
|
c.CommonName = "Step Online CA"
|
||||||
}
|
}
|
||||||
|
if c.CRL != nil && c.CRL.Enabled && c.CRL.CacheDuration == nil {
|
||||||
|
c.CRL.CacheDuration = DefaultCRLCacheDuration
|
||||||
|
}
|
||||||
c.AuthorityConfig.init()
|
c.AuthorityConfig.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,6 +358,11 @@ func (c *Config) Validate() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate crl config: nil is ok
|
||||||
|
if err := c.CRL.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.AuthorityConfig.Validate(c.GetAudiences())
|
return c.AuthorityConfig.Validate(c.GetAudiences())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
241
authority/tls.go
241
authority/tls.go
|
@ -5,11 +5,13 @@ import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -29,6 +31,7 @@ import (
|
||||||
"github.com/smallstep/certificates/db"
|
"github.com/smallstep/certificates/db"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
"github.com/smallstep/certificates/webhook"
|
"github.com/smallstep/certificates/webhook"
|
||||||
|
"github.com/smallstep/nosql/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetTLSOptions returns the tls options configured.
|
// GetTLSOptions returns the tls options configured.
|
||||||
|
@ -36,8 +39,11 @@ func (a *Authority) GetTLSOptions() *config.TLSOptions {
|
||||||
return a.config.TLS
|
return a.config.TLS
|
||||||
}
|
}
|
||||||
|
|
||||||
var oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
|
var (
|
||||||
var oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14}
|
oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
|
||||||
|
oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14}
|
||||||
|
oidExtensionIssuingDistributionPoint = asn1.ObjectIdentifier{2, 5, 29, 28}
|
||||||
|
)
|
||||||
|
|
||||||
func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc {
|
func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc {
|
||||||
return func(crt *x509.Certificate, opts provisioner.SignOptions) error {
|
return func(crt *x509.Certificate, opts provisioner.SignOptions) error {
|
||||||
|
@ -512,16 +518,23 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
||||||
RevokedAt: time.Now().UTC(),
|
RevokedAt: time.Now().UTC(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// For X509 CRLs attempt to get the expiration date of the certificate.
|
||||||
p provisioner.Interface
|
if provisioner.MethodFromContext(ctx) == provisioner.RevokeMethod {
|
||||||
err error
|
if revokeOpts.Crt == nil {
|
||||||
)
|
cert, err := a.db.GetCertificate(revokeOpts.Serial)
|
||||||
|
if err == nil {
|
||||||
|
rci.ExpiresAt = cert.NotAfter
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rci.ExpiresAt = revokeOpts.Crt.NotAfter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If not mTLS nor ACME, then get the TokenID of the token.
|
// If not mTLS nor ACME, then get the TokenID of the token.
|
||||||
if !(revokeOpts.MTLS || revokeOpts.ACME) {
|
if !(revokeOpts.MTLS || revokeOpts.ACME) {
|
||||||
token, err := jose.ParseSigned(revokeOpts.OTT)
|
token, err := jose.ParseSigned(revokeOpts.OTT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.Wrap(http.StatusUnauthorized, err,
|
return errs.Wrap(http.StatusUnauthorized, err, "authority.Revoke; error parsing token", opts...)
|
||||||
"authority.Revoke; error parsing token", opts...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get claims w/out verification.
|
// Get claims w/out verification.
|
||||||
|
@ -531,28 +544,43 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will also validate the audiences for JWK provisioners.
|
// This method will also validate the audiences for JWK provisioners.
|
||||||
p, err = a.LoadProvisionerByToken(token, &claims.Claims)
|
p, err := a.LoadProvisionerByToken(token, &claims.Claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rci.ProvisionerID = p.GetID()
|
rci.ProvisionerID = p.GetID()
|
||||||
rci.TokenID, err = p.GetTokenID(revokeOpts.OTT)
|
rci.TokenID, err = p.GetTokenID(revokeOpts.OTT)
|
||||||
if err != nil && !errors.Is(err, provisioner.ErrAllowTokenReuse) {
|
if err != nil && !errors.Is(err, provisioner.ErrAllowTokenReuse) {
|
||||||
return errs.Wrap(http.StatusInternalServerError, err,
|
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke; could not get ID for token")
|
||||||
"authority.Revoke; could not get ID for token")
|
|
||||||
}
|
}
|
||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
errs.WithKeyVal("provisionerID", rci.ProvisionerID),
|
errs.WithKeyVal("provisionerID", rci.ProvisionerID),
|
||||||
errs.WithKeyVal("tokenID", rci.TokenID),
|
errs.WithKeyVal("tokenID", rci.TokenID),
|
||||||
)
|
)
|
||||||
} else if p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil {
|
} else if p, err := a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil {
|
||||||
// Load the Certificate provisioner if one exists.
|
// Load the Certificate provisioner if one exists.
|
||||||
rci.ProvisionerID = p.GetID()
|
rci.ProvisionerID = p.GetID()
|
||||||
opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID))
|
opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
failRevoke := func(err error) error {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, db.ErrNotImplemented):
|
||||||
|
return errs.NotImplemented("authority.Revoke; no persistence layer configured", opts...)
|
||||||
|
case errors.Is(err, db.ErrAlreadyExists):
|
||||||
|
return errs.ApplyOptions(
|
||||||
|
errs.BadRequest("certificate with serial number '%s' is already revoked", rci.Serial),
|
||||||
|
opts...,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {
|
if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {
|
||||||
err = a.revokeSSH(nil, rci)
|
if err := a.revokeSSH(nil, rci); err != nil {
|
||||||
|
return failRevoke(err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Revoke an X.509 certificate using CAS. If the certificate is not
|
// Revoke an X.509 certificate using CAS. If the certificate is not
|
||||||
// provided we will try to read it from the db. If the read fails we
|
// provided we will try to read it from the db. If the read fails we
|
||||||
|
@ -567,7 +595,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
||||||
|
|
||||||
// CAS operation, note that SoftCAS (default) is a noop.
|
// CAS operation, note that SoftCAS (default) is a noop.
|
||||||
// The revoke happens when this is stored in the db.
|
// The revoke happens when this is stored in the db.
|
||||||
_, err = a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{
|
_, err := a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{
|
||||||
Certificate: revokedCert,
|
Certificate: revokedCert,
|
||||||
SerialNumber: rci.Serial,
|
SerialNumber: rci.Serial,
|
||||||
Reason: rci.Reason,
|
Reason: rci.Reason,
|
||||||
|
@ -579,21 +607,20 @@ 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)
|
if err := a.revoke(revokedCert, rci); err != nil {
|
||||||
}
|
return failRevoke(err)
|
||||||
switch {
|
}
|
||||||
case err == nil:
|
|
||||||
return nil
|
// Generate a new CRL so CRL requesters will always get an up-to-date
|
||||||
case errors.Is(err, db.ErrNotImplemented):
|
// CRL whenever they request it.
|
||||||
return errs.NotImplemented("authority.Revoke; no persistence layer configured", opts...)
|
if a.config.CRL.IsEnabled() && a.config.CRL.GenerateOnRevoke {
|
||||||
case errors.Is(err, db.ErrAlreadyExists):
|
if err := a.GenerateCertificateRevocationList(); err != nil {
|
||||||
return errs.ApplyOptions(
|
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
|
||||||
errs.BadRequest("certificate with serial number '%s' is already revoked", rci.Serial),
|
}
|
||||||
opts...,
|
}
|
||||||
)
|
|
||||||
default:
|
|
||||||
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authority) revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error {
|
func (a *Authority) revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error {
|
||||||
|
@ -614,6 +641,137 @@ func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateIn
|
||||||
return a.db.RevokeSSH(rci)
|
return a.db.RevokeSSH(rci)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.IsEnabled() {
|
||||||
|
return nil, errs.Wrap(http.StatusNotFound, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList")
|
||||||
|
}
|
||||||
|
|
||||||
|
crlDB, ok := a.db.(db.CertificateRevocationListDB)
|
||||||
|
if !ok {
|
||||||
|
return nil, errs.Wrap(http.StatusNotImplemented, 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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.IsEnabled() {
|
||||||
|
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 errors.Errorf("CA does not support CRL Generation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// use a mutex to ensure only one CRL is generated at a time to avoid
|
||||||
|
// concurrency issues
|
||||||
|
a.crlMutex.Lock()
|
||||||
|
defer a.crlMutex.Unlock()
|
||||||
|
|
||||||
|
crlInfo, err := crlDB.GetCRL()
|
||||||
|
if err != nil && !database.IsErrNotFound(err) {
|
||||||
|
return errors.Wrap(err, "could not retrieve CRL from database")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().Truncate(time.Second).UTC()
|
||||||
|
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 bn big.Int
|
||||||
|
if crlInfo != nil {
|
||||||
|
bn.SetInt64(crlInfo.Number + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert our database db.RevokedCertificateInfo types into the pkix
|
||||||
|
// representation ready for the CAS to sign it
|
||||||
|
var revokedCertificates []pkix.RevokedCertificate
|
||||||
|
skipExpiredTime := now.Add(-config.DefaultCRLExpiredDuration)
|
||||||
|
for _, revokedCert := range *revokedList {
|
||||||
|
// skip expired certificates
|
||||||
|
if !revokedCert.ExpiresAt.IsZero() && revokedCert.ExpiresAt.Before(skipExpiredTime) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var sn big.Int
|
||||||
|
sn.SetString(revokedCert.Serial, 10)
|
||||||
|
revokedCertificates = append(revokedCertificates, pkix.RevokedCertificate{
|
||||||
|
SerialNumber: &sn,
|
||||||
|
RevocationTime: revokedCert.RevokedAt,
|
||||||
|
Extensions: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateDuration time.Duration
|
||||||
|
if a.config.CRL.CacheDuration != nil {
|
||||||
|
updateDuration = a.config.CRL.CacheDuration.Duration
|
||||||
|
} else if crlInfo != nil {
|
||||||
|
updateDuration = crlInfo.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a RevocationList representation ready for the CAS to sign
|
||||||
|
// TODO: allow SignatureAlgorithm to be specified?
|
||||||
|
revocationList := x509.RevocationList{
|
||||||
|
SignatureAlgorithm: 0,
|
||||||
|
RevokedCertificates: revokedCertificates,
|
||||||
|
Number: &bn,
|
||||||
|
ThisUpdate: now,
|
||||||
|
NextUpdate: now.Add(updateDuration),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add distribution point.
|
||||||
|
//
|
||||||
|
// Note that this is currently using the port 443 by default.
|
||||||
|
fullName := a.config.Audience("/1.0/crl")[0]
|
||||||
|
if b, err := marshalDistributionPoint(fullName, false); err == nil {
|
||||||
|
revocationList.ExtraExtensions = []pkix.Extension{
|
||||||
|
{Id: oidExtensionIssuingDistributionPoint, Value: b},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
certificateRevocationList, err := caCRLGenerator.CreateCRL(&casapi.CreateCRLRequest{RevocationList: &revocationList})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not create CRL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the
|
||||||
|
// expiry time, duration, and the DER-encoded CRL
|
||||||
|
newCRLInfo := db.CertificateRevocationListInfo{
|
||||||
|
Number: bn.Int64(),
|
||||||
|
ExpiresAt: revocationList.NextUpdate,
|
||||||
|
DER: certificateRevocationList.CRL,
|
||||||
|
Duration: updateDuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the CRL in the database ready for retrieval by api endpoints
|
||||||
|
err = crlDB.StoreCRL(&newCRLInfo)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not store CRL in database")
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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) {
|
||||||
|
@ -707,6 +865,33 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
|
||||||
return &tlsCrt, nil
|
return &tlsCrt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 5280, 5.2.5
|
||||||
|
type distributionPoint struct {
|
||||||
|
DistributionPoint distributionPointName `asn1:"optional,tag:0"`
|
||||||
|
OnlyContainsUserCerts bool `asn1:"optional,tag:1"`
|
||||||
|
OnlyContainsCACerts bool `asn1:"optional,tag:2"`
|
||||||
|
OnlySomeReasons asn1.BitString `asn1:"optional,tag:3"`
|
||||||
|
IndirectCRL bool `asn1:"optional,tag:4"`
|
||||||
|
OnlyContainsAttributeCerts bool `asn1:"optional,tag:5"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type distributionPointName struct {
|
||||||
|
FullName []asn1.RawValue `asn1:"optional,tag:0"`
|
||||||
|
RelativeName pkix.RDNSequence `asn1:"optional,tag:1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalDistributionPoint(fullName string, isCA bool) ([]byte, error) {
|
||||||
|
return asn1.Marshal(distributionPoint{
|
||||||
|
DistributionPoint: distributionPointName{
|
||||||
|
FullName: []asn1.RawValue{
|
||||||
|
{Class: 2, Tag: 6, Bytes: []byte(fullName)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OnlyContainsUserCerts: !isCA,
|
||||||
|
OnlyContainsCACerts: isCA,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// templatingError tries to extract more information about the cause of
|
// templatingError tries to extract more information about the cause of
|
||||||
// an error related to (most probably) malformed template data and adds
|
// an error related to (most probably) malformed template data and adds
|
||||||
// this to the error message.
|
// this to the error message.
|
||||||
|
|
|
@ -26,11 +26,13 @@ import (
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/api/render"
|
"github.com/smallstep/certificates/api/render"
|
||||||
|
"github.com/smallstep/certificates/authority/config"
|
||||||
"github.com/smallstep/certificates/authority/policy"
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
"github.com/smallstep/certificates/authority/provisioner"
|
"github.com/smallstep/certificates/authority/provisioner"
|
||||||
"github.com/smallstep/certificates/cas/softcas"
|
"github.com/smallstep/certificates/cas/softcas"
|
||||||
"github.com/smallstep/certificates/db"
|
"github.com/smallstep/certificates/db"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
|
"github.com/smallstep/nosql/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -1498,7 +1500,7 @@ func TestAuthority_Revoke(t *testing.T) {
|
||||||
return true, nil
|
return true, nil
|
||||||
},
|
},
|
||||||
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
||||||
return nil, nil
|
return nil, errors.New("not found")
|
||||||
},
|
},
|
||||||
Err: errors.New("force"),
|
Err: errors.New("force"),
|
||||||
}))
|
}))
|
||||||
|
@ -1538,7 +1540,7 @@ func TestAuthority_Revoke(t *testing.T) {
|
||||||
return true, nil
|
return true, nil
|
||||||
},
|
},
|
||||||
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
||||||
return nil, nil
|
return nil, errors.New("not found")
|
||||||
},
|
},
|
||||||
Err: db.ErrAlreadyExists,
|
Err: db.ErrAlreadyExists,
|
||||||
}))
|
}))
|
||||||
|
@ -1807,3 +1809,148 @@ func TestAuthority_constraints(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthority_CRL(t *testing.T) {
|
||||||
|
reasonCode := 2
|
||||||
|
reason := "bob was let go"
|
||||||
|
validIssuer := "step-cli"
|
||||||
|
validAudience := testAudiences.Revoke
|
||||||
|
now := time.Now().UTC()
|
||||||
|
//
|
||||||
|
jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
//
|
||||||
|
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
||||||
|
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
crlCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)
|
||||||
|
|
||||||
|
var crlStore db.CertificateRevocationListInfo
|
||||||
|
var revokedList []db.RevokedCertificateInfo
|
||||||
|
|
||||||
|
type test struct {
|
||||||
|
auth *Authority
|
||||||
|
ctx context.Context
|
||||||
|
expected []string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
tests := map[string]func() test{
|
||||||
|
"fail/empty-crl": func() test {
|
||||||
|
a := testAuthority(t, WithDatabase(&db.MockAuthDB{
|
||||||
|
MUseToken: func(id, tok string) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
},
|
||||||
|
MStoreCRL: func(i *db.CertificateRevocationListInfo) error {
|
||||||
|
crlStore = *i
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MGetCRL: func() (*db.CertificateRevocationListInfo, error) {
|
||||||
|
return nil, database.ErrNotFound
|
||||||
|
},
|
||||||
|
MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {
|
||||||
|
return &revokedList, nil
|
||||||
|
},
|
||||||
|
MRevoke: func(rci *db.RevokedCertificateInfo) error {
|
||||||
|
revokedList = append(revokedList, *rci)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
a.config.CRL = &config.CRLConfig{
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return test{
|
||||||
|
auth: a,
|
||||||
|
ctx: crlCtx,
|
||||||
|
expected: nil,
|
||||||
|
err: database.ErrNotFound,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/crl-full": func() test {
|
||||||
|
a := testAuthority(t, WithDatabase(&db.MockAuthDB{
|
||||||
|
MUseToken: func(id, tok string) (bool, error) {
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
},
|
||||||
|
MStoreCRL: func(i *db.CertificateRevocationListInfo) error {
|
||||||
|
crlStore = *i
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
MGetCRL: func() (*db.CertificateRevocationListInfo, error) {
|
||||||
|
return &crlStore, nil
|
||||||
|
},
|
||||||
|
MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {
|
||||||
|
return &revokedList, nil
|
||||||
|
},
|
||||||
|
MRevoke: func(rci *db.RevokedCertificateInfo) error {
|
||||||
|
revokedList = append(revokedList, *rci)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
a.config.CRL = &config.CRLConfig{
|
||||||
|
Enabled: true,
|
||||||
|
GenerateOnRevoke: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ex []string
|
||||||
|
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
sn := fmt.Sprintf("%v", i)
|
||||||
|
|
||||||
|
cl := jose.Claims{
|
||||||
|
Subject: fmt.Sprintf("sn-%v", i),
|
||||||
|
Issuer: validIssuer,
|
||||||
|
NotBefore: jose.NewNumericDate(now),
|
||||||
|
Expiry: jose.NewNumericDate(now.Add(time.Minute)),
|
||||||
|
Audience: validAudience,
|
||||||
|
ID: sn,
|
||||||
|
}
|
||||||
|
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
err = a.Revoke(crlCtx, &RevokeOptions{
|
||||||
|
Serial: sn,
|
||||||
|
ReasonCode: reasonCode,
|
||||||
|
Reason: reason,
|
||||||
|
OTT: raw,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
ex = append(ex, sn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return test{
|
||||||
|
auth: a,
|
||||||
|
ctx: crlCtx,
|
||||||
|
expected: ex,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, f := range tests {
|
||||||
|
tc := f()
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
if crlBytes, err := tc.auth.GetCertificateRevocationList(); err == nil {
|
||||||
|
crl, parseErr := x509.ParseCRL(crlBytes)
|
||||||
|
if parseErr != nil {
|
||||||
|
t.Errorf("x509.ParseCertificateRequest() error = %v, wantErr %v", parseErr, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmpList []string
|
||||||
|
for _, c := range crl.TBSCertList.RevokedCertificates {
|
||||||
|
cmpList = append(cmpList, c.SerialNumber.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equals(t, cmpList, tc.expected)
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, tc.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -154,3 +154,13 @@ type CreateCertificateAuthorityResponse struct {
|
||||||
PrivateKey crypto.PrivateKey
|
PrivateKey crypto.PrivateKey
|
||||||
Signer crypto.Signer
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
CreateCRL(req *CreateCRLRequest) (*CreateCRLResponse, 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/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"time"
|
"time"
|
||||||
|
@ -132,6 +133,20 @@ func (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateCRL will create a new CRL based on the RevocationList passed to it
|
||||||
|
func (c *SoftCAS) CreateCRL(req *apiv1.CreateCRLRequest) (*apiv1.CreateCRLResponse, error) {
|
||||||
|
certChain, signer, err := c.getCertSigner()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
revocationListBytes, err := x509.CreateRevocationList(rand.Reader, req.RevocationList, certChain[0], signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apiv1.CreateCRLResponse{CRL: revocationListBytes}, 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 {
|
||||||
|
|
120
db/db.go
120
db/db.go
|
@ -19,6 +19,7 @@ var (
|
||||||
certsTable = []byte("x509_certs")
|
certsTable = []byte("x509_certs")
|
||||||
certsDataTable = []byte("x509_certs_data")
|
certsDataTable = []byte("x509_certs_data")
|
||||||
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")
|
||||||
|
@ -27,6 +28,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")
|
||||||
|
@ -87,6 +91,13 @@ type CertificateStorer interface {
|
||||||
StoreSSHCertificate(crt *ssh.Certificate) error
|
StoreSSHCertificate(crt *ssh.Certificate) 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.
|
// DB is a wrapper over the nosql.DB interface.
|
||||||
type DB struct {
|
type DB struct {
|
||||||
nosql.DB
|
nosql.DB
|
||||||
|
@ -113,7 +124,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, certsDataTable,
|
revokedSSHCertsTable, certsDataTable, crlTable,
|
||||||
}
|
}
|
||||||
for _, b := range tables {
|
for _, b := range tables {
|
||||||
if err := db.CreateTable(b); err != nil {
|
if err := db.CreateTable(b); err != nil {
|
||||||
|
@ -133,11 +144,21 @@ type RevokedCertificateInfo struct {
|
||||||
ReasonCode int
|
ReasonCode int
|
||||||
Reason string
|
Reason string
|
||||||
RevokedAt time.Time
|
RevokedAt time.Time
|
||||||
|
ExpiresAt time.Time
|
||||||
TokenID string
|
TokenID string
|
||||||
MTLS bool
|
MTLS bool
|
||||||
ACME bool
|
ACME bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CertificateRevocationListInfo contains a CRL in DER format and associated
|
||||||
|
// metadata to allow a decision on whether to regenerate the CRL or not easier
|
||||||
|
type CertificateRevocationListInfo struct {
|
||||||
|
Number int64
|
||||||
|
ExpiresAt time.Time
|
||||||
|
Duration time.Duration
|
||||||
|
DER []byte
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -220,6 +241,51 @@ 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 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))
|
||||||
|
@ -382,20 +448,44 @@ func (db *DB) Shutdown() error {
|
||||||
|
|
||||||
// MockAuthDB mocks the AuthDB interface. //
|
// MockAuthDB mocks the AuthDB interface. //
|
||||||
type MockAuthDB struct {
|
type MockAuthDB struct {
|
||||||
Err error
|
Err error
|
||||||
Ret1 interface{}
|
Ret1 interface{}
|
||||||
MIsRevoked func(string) (bool, error)
|
MIsRevoked func(string) (bool, error)
|
||||||
MIsSSHRevoked func(string) (bool, error)
|
MIsSSHRevoked func(string) (bool, error)
|
||||||
MRevoke func(rci *RevokedCertificateInfo) error
|
MRevoke func(rci *RevokedCertificateInfo) error
|
||||||
MRevokeSSH func(rci *RevokedCertificateInfo) error
|
MRevokeSSH func(rci *RevokedCertificateInfo) error
|
||||||
MGetCertificate func(serialNumber string) (*x509.Certificate, error)
|
MGetCertificate func(serialNumber string) (*x509.Certificate, error)
|
||||||
MGetCertificateData func(serialNumber string) (*CertificateData, error)
|
MGetCertificateData func(serialNumber string) (*CertificateData, error)
|
||||||
MStoreCertificate func(crt *x509.Certificate) error
|
MStoreCertificate func(crt *x509.Certificate) error
|
||||||
MUseToken func(id, tok string) (bool, error)
|
MUseToken func(id, tok string) (bool, error)
|
||||||
MIsSSHHost func(principal string) (bool, error)
|
MIsSSHHost func(principal string) (bool, error)
|
||||||
MStoreSSHCertificate func(crt *ssh.Certificate) error
|
MStoreSSHCertificate func(crt *ssh.Certificate) error
|
||||||
MGetSSHHostPrincipals func() ([]string, error)
|
MGetSSHHostPrincipals func() ([]string, error)
|
||||||
MShutdown func() error
|
MShutdown func() error
|
||||||
|
MGetRevokedCertificates func() (*[]RevokedCertificateInfo, error)
|
||||||
|
MGetCRL func() (*CertificateRevocationListInfo, error)
|
||||||
|
MStoreCRL func(*CertificateRevocationListInfo) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAuthDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) {
|
||||||
|
if m.MGetRevokedCertificates != nil {
|
||||||
|
return m.MGetRevokedCertificates()
|
||||||
|
}
|
||||||
|
return m.Ret1.(*[]RevokedCertificateInfo), m.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAuthDB) GetCRL() (*CertificateRevocationListInfo, error) {
|
||||||
|
if m.MGetCRL != nil {
|
||||||
|
return m.MGetCRL()
|
||||||
|
}
|
||||||
|
return m.Ret1.(*CertificateRevocationListInfo), m.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockAuthDB) StoreCRL(info *CertificateRevocationListInfo) error {
|
||||||
|
if m.MStoreCRL != nil {
|
||||||
|
return m.MStoreCRL(info)
|
||||||
|
}
|
||||||
|
return m.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRevoked mock.
|
// IsRevoked mock.
|
||||||
|
|
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
|
||||||
|
|
|
@ -126,17 +126,20 @@ is `json`.
|
||||||
|
|
||||||
* `db`: data persistence layer. See [database documentation](./database.md) for more
|
* `db`: data persistence layer. See [database documentation](./database.md) for more
|
||||||
info.
|
info.
|
||||||
|
* `type`: `badger`, `bbolt`, `mysql`, etc.
|
||||||
|
* `dataSource`: string that can be interpreted differently depending on the
|
||||||
|
type of the database. Usually a path to where the data is stored. See the
|
||||||
|
[database configuration docs](./database.md#configuration) for more info.
|
||||||
|
* `database`: name of the database. Used for backends that may have multiple
|
||||||
|
databases. e.g. MySQL
|
||||||
|
* `valueDir`: directory to store the value log in (Badger specific).
|
||||||
|
|
||||||
- type: `badger`, `bbolt`, `mysql`, etc.
|
* `crl`: Certificate Revocation List settings:
|
||||||
|
* `enable`: enables CRL generation (`true` to generate, `false` to disable)
|
||||||
- dataSource: `string` that can be interpreted differently depending on the
|
* `generateOnRevoke`: a revoke will generate a new CRL if the crl is enabled.
|
||||||
type of the database. Usually a path to where the data is stored. See
|
* `cacheDuration`: the duration until next update of the CRL, defaults to 24h.
|
||||||
the [database configuration docs](./database.md#configuration) for more info.
|
* `renewPeriod`: the time between CRL regeneration. If not set ~2/3 of the
|
||||||
|
cacheDuration will be used.
|
||||||
- database: name of the database. Used for backends that may have
|
|
||||||
multiple databases. e.g. MySQL
|
|
||||||
|
|
||||||
- valueDir: directory to store the value log in (Badger specific).
|
|
||||||
|
|
||||||
* `tls`: settings for negotiating communication with the CA; includes acceptable
|
* `tls`: settings for negotiating communication with the CA; includes acceptable
|
||||||
ciphersuites, min/max TLS version, etc.
|
ciphersuites, min/max TLS version, etc.
|
||||||
|
|
Loading…
Reference in a new issue