185 lines
4.3 KiB
Go
185 lines
4.3 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 := jose.ValidateX5C(certFile, 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", certs)
|
|
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
|
|
}
|