forked from TrueCloudLab/certificates
Add initial implementation of StepCAS.
StepCAS allows to configure step-ca as an RA using another step-ca as the main CA.
This commit is contained in:
parent
3b9eed003d
commit
a6115e29c2
9 changed files with 428 additions and 17 deletions
|
@ -189,7 +189,6 @@ func (c *Config) Validate() error {
|
|||
|
||||
// Options holds the RA/CAS configuration.
|
||||
ra := c.AuthorityConfig.Options
|
||||
|
||||
// The default RA/CAS requires root, crt and key.
|
||||
if ra.Is(cas.SoftCAS) {
|
||||
switch {
|
||||
|
|
|
@ -148,6 +148,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
|
|||
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
|
||||
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
||||
Template: leaf,
|
||||
CSR: csr,
|
||||
Lifetime: lifetime,
|
||||
Backdate: signOpts.Backdate,
|
||||
})
|
||||
|
@ -367,9 +368,10 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
|||
// CAS operation, note that SoftCAS (default) is a noop.
|
||||
// The revoke happens when this is stored in the db.
|
||||
_, err = a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{
|
||||
Certificate: revokedCert,
|
||||
Reason: rci.Reason,
|
||||
ReasonCode: rci.ReasonCode,
|
||||
Certificate: revokedCert,
|
||||
SerialNumber: rci.Serial,
|
||||
Reason: rci.Reason,
|
||||
ReasonCode: rci.ReasonCode,
|
||||
})
|
||||
if err != nil {
|
||||
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
|
||||
|
@ -427,6 +429,7 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
|
|||
|
||||
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
||||
Template: certTpl,
|
||||
CSR: cr,
|
||||
Lifetime: 24 * time.Hour,
|
||||
Backdate: 1 * time.Minute,
|
||||
})
|
||||
|
|
|
@ -14,17 +14,29 @@ type Options struct {
|
|||
// The type of the CAS to use.
|
||||
Type string `json:"type"`
|
||||
|
||||
// Path to the credentials file used in CloudCAS
|
||||
CredentialsFile string `json:"credentialsFile"`
|
||||
// CertificateAuthority reference:
|
||||
// In StepCAS the values is the CA url, e.g. "https://ca.smallstep.com:9000".
|
||||
// In CloudCAS the format is "projects/*/locations/*/certificateAuthorities/*".
|
||||
CertificateAuthority string `json:"certificateAuthority,omitempty"`
|
||||
|
||||
// CertificateAuthority reference. In CloudCAS the format is
|
||||
// `projects/*/locations/*/certificateAuthorities/*`.
|
||||
CertificateAuthority string `json:"certificateAuthority"`
|
||||
// CertificateAuthorityFingerprint is the root fingerprint used to
|
||||
// authenticate the connection to the CA when using StepCAS.
|
||||
CertificateAuthorityFingerprint string `json:"certificateAuthorityFingerprint,omitempty"`
|
||||
|
||||
// Certificate and signer are the issuer certificate,along with any other bundled certificates to be returned in the chain for consumers, and signer used in SoftCAS.
|
||||
// They are configured in ca.json crt and key properties.
|
||||
CertificateChain []*x509.Certificate
|
||||
Signer crypto.Signer `json:"-"`
|
||||
// CertificateIssuer contains the configuration used in StepCAS.
|
||||
CertificateIssuer *CertificateIssuer `json:"certificateIssuer,omitempty"`
|
||||
|
||||
// Path to the credentials file used in CloudCAS. If not defined the default
|
||||
// authentication mechanism provided by Google SDK will be used. See
|
||||
// https://cloud.google.com/docs/authentication.
|
||||
CredentialsFile string `json:"credentialsFile,omitempty"`
|
||||
|
||||
// Certificate and signer are the issuer certificate, along with any other
|
||||
// bundled certificates to be returned in the chain for consumers, and
|
||||
// signer used in SoftCAS. They are configured in ca.json crt and key
|
||||
// properties.
|
||||
CertificateChain []*x509.Certificate `json:"-"`
|
||||
Signer crypto.Signer `json:"-"`
|
||||
|
||||
// IsCreator is set to true when we're creating a certificate authority. Is
|
||||
// used to skip some validations when initializing a CertificateAuthority.
|
||||
|
@ -39,6 +51,15 @@ type Options struct {
|
|||
Location string `json:"-"`
|
||||
}
|
||||
|
||||
// CertificateIssuer contains the properties used to use the StepCAS certificate
|
||||
// authority service.
|
||||
type CertificateIssuer struct {
|
||||
Type string `json:"type"`
|
||||
Provisioner string `json:"provisioner,omitempty"`
|
||||
Certificate string `json:"crt,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
}
|
||||
|
||||
// Validate checks the fields in Options.
|
||||
func (o *Options) Validate() error {
|
||||
var typ Type
|
||||
|
|
|
@ -53,6 +53,7 @@ const (
|
|||
// CreateCertificateRequest is the request used to sign a new certificate.
|
||||
type CreateCertificateRequest struct {
|
||||
Template *x509.Certificate
|
||||
CSR *x509.CertificateRequest
|
||||
Lifetime time.Duration
|
||||
Backdate time.Duration
|
||||
RequestID string
|
||||
|
@ -67,6 +68,7 @@ type CreateCertificateResponse struct {
|
|||
// RenewCertificateRequest is the request used to re-sign a certificate.
|
||||
type RenewCertificateRequest struct {
|
||||
Template *x509.Certificate
|
||||
CSR *x509.CertificateRequest
|
||||
Lifetime time.Duration
|
||||
Backdate time.Duration
|
||||
RequestID string
|
||||
|
@ -80,10 +82,11 @@ type RenewCertificateResponse struct {
|
|||
|
||||
// RevokeCertificateRequest is the request used to revoke a certificate.
|
||||
type RevokeCertificateRequest struct {
|
||||
Certificate *x509.Certificate
|
||||
Reason string
|
||||
ReasonCode int
|
||||
RequestID string
|
||||
Certificate *x509.Certificate
|
||||
SerialNumber string
|
||||
Reason string
|
||||
ReasonCode int
|
||||
RequestID string
|
||||
}
|
||||
|
||||
// RevokeCertificateResponse is the response to a revoke certificate request.
|
||||
|
|
|
@ -35,6 +35,8 @@ const (
|
|||
SoftCAS = "softcas"
|
||||
// CloudCAS is a CertificateAuthorityService using Google Cloud CAS.
|
||||
CloudCAS = "cloudcas"
|
||||
// StepCAS is a CertificateAuthorityService using another step-ca instance.
|
||||
StepCAS = "stepcas"
|
||||
)
|
||||
|
||||
// String returns a string from the type. It will always return the lower case
|
||||
|
|
40
cas/stepcas/issuer.go
Normal file
40
cas/stepcas/issuer.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package stepcas
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
)
|
||||
|
||||
// validateCertificateIssuer validates the configuration of the certificate
|
||||
// issuer.
|
||||
func validateCertificateIssuer(iss *apiv1.CertificateIssuer) error {
|
||||
switch {
|
||||
case iss == nil:
|
||||
return errors.New("stepCAS 'certificateIssuer' cannot be nil")
|
||||
case iss.Type == "":
|
||||
return errors.New("stepCAS `certificateIssuer.type` cannot be empty")
|
||||
}
|
||||
|
||||
switch strings.ToLower(iss.Type) {
|
||||
case "x5c":
|
||||
return validateX5CIssuer(iss)
|
||||
default:
|
||||
return errors.Errorf("stepCAS `certificateIssuer.type` %s is not supported", iss.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// validateX5CIssuer validates the configuration of x5c issuer.
|
||||
func validateX5CIssuer(iss *apiv1.CertificateIssuer) error {
|
||||
switch {
|
||||
case iss.Certificate == "":
|
||||
return errors.New("stepCAS `certificateIssuer.crt` cannot be empty")
|
||||
case iss.Key == "":
|
||||
return errors.New("stepCAS `certificateIssuer.key` cannot be empty")
|
||||
case iss.Provisioner == "":
|
||||
return errors.New("stepCAS `certificateIssuer.provisioner` cannot be empty")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
189
cas/stepcas/stepcas.go
Normal file
189
cas/stepcas/stepcas.go
Normal file
|
@ -0,0 +1,189 @@
|
|||
package stepcas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apiv1.Register(apiv1.StepCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {
|
||||
return New(ctx, opts)
|
||||
})
|
||||
}
|
||||
|
||||
// StepCAS implements the cas.CertificateAuthorityService interface using
|
||||
// another step-ca instance.
|
||||
type StepCAS struct {
|
||||
x5c *x5cIssuer
|
||||
client *ca.Client
|
||||
fingerprint string
|
||||
}
|
||||
|
||||
// New creates a new CertificateAuthorityService implementation using another
|
||||
// step-ca instance.
|
||||
func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) {
|
||||
switch {
|
||||
case opts.CertificateAuthority == "":
|
||||
return nil, errors.New("stepCAS 'certificateAuthority' cannot be empty")
|
||||
case opts.CertificateAuthorityFingerprint == "":
|
||||
return nil, errors.New("stepCAS 'certificateAuthorityFingerprint' cannot be empty")
|
||||
}
|
||||
|
||||
caURL, err := url.Parse(opts.CertificateAuthority)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "stepCAS `certificateAuthority` is not valid")
|
||||
}
|
||||
if err := validateCertificateIssuer(opts.CertificateIssuer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create client.
|
||||
client, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// X5C is the only one supported at the moment.
|
||||
x5c, err := newX5CIssuer(caURL, opts.CertificateIssuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &StepCAS{
|
||||
x5c: x5c,
|
||||
client: client,
|
||||
fingerprint: opts.CertificateAuthorityFingerprint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {
|
||||
switch {
|
||||
case req.CSR == nil:
|
||||
return nil, errors.New("createCertificateRequest `template` cannot be nil")
|
||||
case req.Lifetime == 0:
|
||||
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
|
||||
}
|
||||
|
||||
token, err := s.signToken(req.CSR.Subject.CommonName, req.CSR.DNSNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Sign(&api.SignRequest{
|
||||
CsrPEM: api.CertificateRequest{CertificateRequest: req.CSR},
|
||||
OTT: token,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var chain []*x509.Certificate
|
||||
cert := resp.CertChainPEM[0].Certificate
|
||||
for _, c := range resp.CertChainPEM[1:] {
|
||||
chain = append(chain, c.Certificate)
|
||||
}
|
||||
|
||||
return &apiv1.CreateCertificateResponse{
|
||||
Certificate: cert,
|
||||
CertificateChain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *StepCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {
|
||||
switch {
|
||||
case req.CSR == nil:
|
||||
return nil, errors.New("createCertificateRequest `template` cannot be nil")
|
||||
case req.Lifetime == 0:
|
||||
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
|
||||
}
|
||||
|
||||
token, err := s.signToken(req.CSR.Subject.CommonName, req.CSR.DNSNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Sign(&api.SignRequest{
|
||||
CsrPEM: api.CertificateRequest{CertificateRequest: req.CSR},
|
||||
OTT: token,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var chain []*x509.Certificate
|
||||
cert := resp.CertChainPEM[0].Certificate
|
||||
for _, c := range resp.CertChainPEM[1:] {
|
||||
chain = append(chain, c.Certificate)
|
||||
}
|
||||
|
||||
return &apiv1.RenewCertificateResponse{
|
||||
Certificate: cert,
|
||||
CertificateChain: chain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *StepCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
|
||||
switch {
|
||||
case req.SerialNumber == "" && req.Certificate == nil:
|
||||
return nil, errors.New("revokeCertificateRequest `serialNumber` or `certificate` are required")
|
||||
}
|
||||
|
||||
serialNumber := req.SerialNumber
|
||||
if req.Certificate != nil {
|
||||
serialNumber = req.Certificate.SerialNumber.String()
|
||||
}
|
||||
|
||||
token, err := s.revokeToken(serialNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = s.client.Revoke(&api.RevokeRequest{
|
||||
Serial: serialNumber,
|
||||
ReasonCode: req.ReasonCode,
|
||||
Reason: req.Reason,
|
||||
OTT: token,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apiv1.RevokeCertificateResponse{
|
||||
Certificate: req.Certificate,
|
||||
CertificateChain: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCertificateAuthority returns the root certificate of the certificate
|
||||
// authority using the configured fingerprint.
|
||||
func (s *StepCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {
|
||||
resp, err := s.client.Root(s.fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &apiv1.GetCertificateAuthorityResponse{
|
||||
RootCertificate: resp.RootPEM.Certificate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *StepCAS) signToken(subject string, sans []string) (string, error) {
|
||||
if s.x5c != nil {
|
||||
return s.x5c.SignToken(subject, sans)
|
||||
}
|
||||
|
||||
return "", errors.New("stepCAS does not have any provisioner configured")
|
||||
}
|
||||
|
||||
func (s *StepCAS) revokeToken(subject string) (string, error) {
|
||||
if s.x5c != nil {
|
||||
return s.x5c.RevokeToken(subject)
|
||||
}
|
||||
|
||||
return "", errors.New("stepCAS does not have any provisioner configured")
|
||||
}
|
153
cas/stepcas/x5c_issuer.go
Normal file
153
cas/stepcas/x5c_issuer.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
package stepcas
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/randutil"
|
||||
)
|
||||
|
||||
const defaultValidity = 5 * time.Minute
|
||||
|
||||
type x5cIssuer struct {
|
||||
caURL *url.URL
|
||||
certFile string
|
||||
keyFile string
|
||||
issuer string
|
||||
}
|
||||
|
||||
// newX5CIssuer create a new x5c token issuer. The given configuration should be
|
||||
// already validate.
|
||||
func newX5CIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*x5cIssuer, error) {
|
||||
_, err := newX5CSigner(cfg.Certificate, cfg.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &x5cIssuer{
|
||||
caURL: caURL,
|
||||
certFile: cfg.Certificate,
|
||||
keyFile: cfg.Key,
|
||||
issuer: cfg.Provisioner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *x5cIssuer) SignToken(subject string, sans []string) (string, error) {
|
||||
aud := i.caURL.ResolveReference(&url.URL{
|
||||
Path: "/1.0/sign",
|
||||
Fragment: "x5c/" + i.issuer,
|
||||
}).String()
|
||||
|
||||
return i.createToken(aud, subject, sans)
|
||||
}
|
||||
|
||||
func (i *x5cIssuer) RevokeToken(subject string) (string, error) {
|
||||
aud := i.caURL.ResolveReference(&url.URL{
|
||||
Path: "/1.0/revoke",
|
||||
Fragment: "x5c/" + i.issuer,
|
||||
}).String()
|
||||
|
||||
return i.createToken(aud, subject, nil)
|
||||
}
|
||||
|
||||
func (i *x5cIssuer) createToken(aud, sub string, sans []string) (string, error) {
|
||||
signer, err := newX5CSigner(i.certFile, i.keyFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := randutil.Hex(64) // 256 bits
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims := defaultClaims(i.issuer, sub, aud, id)
|
||||
builder := jose.Signed(signer).Claims(claims)
|
||||
if len(sans) > 0 {
|
||||
builder = builder.Claims(map[string]interface{}{
|
||||
"sans": sans,
|
||||
})
|
||||
}
|
||||
|
||||
tok, err := builder.CompactSerialize()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error signing token")
|
||||
}
|
||||
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
func defaultClaims(iss, sub, aud, id string) jose.Claims {
|
||||
now := time.Now()
|
||||
return jose.Claims{
|
||||
ID: id,
|
||||
Issuer: iss,
|
||||
Subject: sub,
|
||||
Audience: jose.Audience{aud},
|
||||
Expiry: jose.NewNumericDate(now.Add(defaultValidity)),
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
IssuedAt: jose.NewNumericDate(now),
|
||||
}
|
||||
}
|
||||
|
||||
func newX5CSigner(certFile, keyFile string) (jose.Signer, error) {
|
||||
key, err := pemutil.Read(keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer, ok := key.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, errors.New("key is not a crypto.Signer")
|
||||
}
|
||||
kid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certs, err := jose.ValidateX5C(certFile, key)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error validating x5c certificate chain and key")
|
||||
}
|
||||
|
||||
so := new(jose.SignerOptions)
|
||||
so.WithType("JWT")
|
||||
so.WithHeader("kid", kid)
|
||||
so.WithHeader("x5c", certs)
|
||||
return newJoseSigner(signer, so)
|
||||
}
|
||||
|
||||
func newJoseSigner(key crypto.Signer, so *jose.SignerOptions) (jose.Signer, error) {
|
||||
var alg jose.SignatureAlgorithm
|
||||
switch k := key.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
switch k.Curve.Params().Name {
|
||||
case "P-256":
|
||||
alg = jose.ES256
|
||||
case "P-384":
|
||||
alg = jose.ES384
|
||||
case "P-521":
|
||||
alg = jose.ES512
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported elliptic curve %s", k.Curve.Params().Name)
|
||||
}
|
||||
case ed25519.PrivateKey:
|
||||
alg = jose.EdDSA
|
||||
case *rsa.PrivateKey:
|
||||
alg = jose.DefaultRSASigAlgorithm
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported key type %T", k)
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: key}, so)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error creating jose.Signer")
|
||||
}
|
||||
return signer, nil
|
||||
}
|
|
@ -37,6 +37,7 @@ import (
|
|||
// Enabled cas interfaces.
|
||||
_ "github.com/smallstep/certificates/cas/cloudcas"
|
||||
_ "github.com/smallstep/certificates/cas/softcas"
|
||||
_ "github.com/smallstep/certificates/cas/stepcas"
|
||||
)
|
||||
|
||||
// commit and buildTime are filled in during build by the Makefile
|
||||
|
|
Loading…
Reference in a new issue