certificates/cas/stepcas/x5c_issuer.go
2021-07-02 19:05:17 -07:00

189 lines
4.4 KiB
Go

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
// timeNow returns the current time.
// This method is used for unit testing purposes.
var timeNow = func() time.Time {
return time.Now()
}
type x5cIssuer struct {
caURL *url.URL
issuer string
certFile string
keyFile string
password 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, cfg.Password)
if err != nil {
return nil, err
}
return &x5cIssuer{
caURL: caURL,
issuer: cfg.Provisioner,
certFile: cfg.Certificate,
keyFile: cfg.Key,
password: cfg.Password,
}, 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) Lifetime(d time.Duration) time.Duration {
cert, err := pemutil.ReadCertificate(i.certFile, pemutil.WithFirstBlock())
if err != nil {
return d
}
now := timeNow()
if now.Add(d + time.Minute).After(cert.NotAfter) {
return cert.NotAfter.Sub(now) - time.Minute
}
return d
}
func (i *x5cIssuer) createToken(aud, sub string, sans []string) (string, error) {
signer, err := newX5CSigner(i.certFile, i.keyFile, i.password)
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 := timeNow()
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 readKey(keyFile, password string) (crypto.Signer, error) {
var opts []pemutil.Options
if password != "" {
opts = append(opts, pemutil.WithPassword([]byte(password)))
}
key, err := pemutil.Read(keyFile, opts...)
if err != nil {
return nil, err
}
signer, ok := key.(crypto.Signer)
if !ok {
return nil, errors.New("key is not a crypto.Signer")
}
return signer, nil
}
func newX5CSigner(certFile, keyFile, password string) (jose.Signer, error) {
signer, err := readKey(keyFile, password)
if err != nil {
return nil, err
}
kid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()})
if err != nil {
return nil, err
}
certs, err := pemutil.ReadCertificateBundle(certFile)
if err != nil {
return nil, errors.Wrap(err, "error reading x5c certificate chain")
}
certStrs, err := jose.ValidateX5C(certs, signer)
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", certStrs)
return newJoseSigner(signer, so)
}
func newJoseSigner(key crypto.Signer, so *jose.SignerOptions) (jose.Signer, error) {
var alg jose.SignatureAlgorithm
switch k := key.Public().(type) {
case *ecdsa.PublicKey:
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.PublicKey:
alg = jose.EdDSA
case *rsa.PublicKey:
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
}