certificates/cas/stepcas/jwk_issuer.go
2022-12-21 16:02:26 +01:00

165 lines
4.1 KiB
Go

package stepcas
import (
"context"
"crypto"
"encoding/json"
"net/url"
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas/apiv1"
"go.step.sm/cli-utils/ui"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/randutil"
)
type jwkIssuer struct {
caURL *url.URL
issuer string
signer jose.Signer
}
func newJWKIssuer(ctx context.Context, caURL *url.URL, client *ca.Client, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) {
var err error
var signer jose.Signer
// Read the key from the CA if not provided.
// Or read it from a PEM file.
if cfg.Key == "" {
p, err := findProvisioner(ctx, client, provisioner.TypeJWK, cfg.Provisioner)
if err != nil {
return nil, err
}
kid, key, ok := p.GetEncryptedKey()
if !ok {
return nil, errors.Errorf("provisioner with name %s does not have an encrypted key", cfg.Provisioner)
}
signer, err = newJWKSignerFromEncryptedKey(kid, key, cfg.Password)
if err != nil {
return nil, err
}
} else {
signer, err = newJWKSigner(cfg.Key, cfg.Password)
if err != nil {
return nil, err
}
}
return &jwkIssuer{
caURL: caURL,
issuer: cfg.Provisioner,
signer: signer,
}, nil
}
func (i *jwkIssuer) SignToken(subject string, sans []string, info *raInfo) (string, error) {
aud := i.caURL.ResolveReference(&url.URL{
Path: "/1.0/sign",
}).String()
return i.createToken(aud, subject, sans, info)
}
func (i *jwkIssuer) RevokeToken(subject string) (string, error) {
aud := i.caURL.ResolveReference(&url.URL{
Path: "/1.0/revoke",
}).String()
return i.createToken(aud, subject, nil, nil)
}
func (i *jwkIssuer) Lifetime(d time.Duration) time.Duration {
return d
}
func (i *jwkIssuer) createToken(aud, sub string, sans []string, info *raInfo) (string, error) {
id, err := randutil.Hex(64) // 256 bits
if err != nil {
return "", err
}
claims := defaultClaims(i.issuer, sub, aud, id)
builder := jose.Signed(i.signer).Claims(claims)
if len(sans) > 0 {
builder = builder.Claims(map[string]interface{}{
"sans": sans,
})
}
if info != nil {
builder = builder.Claims(map[string]interface{}{
"step": map[string]interface{}{
"ra": info,
},
})
}
tok, err := builder.CompactSerialize()
if err != nil {
return "", errors.Wrap(err, "error signing token")
}
return tok, nil
}
func newJWKSigner(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
}
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader("kid", kid)
return newJoseSigner(signer, so)
}
func newJWKSignerFromEncryptedKey(kid, key, password string) (jose.Signer, error) {
var jwk jose.JSONWebKey
// If the password is empty it will use the password prompter.
b, err := jose.Decrypt([]byte(key),
jose.WithPassword([]byte(password)),
jose.WithPasswordPrompter("Please enter the password to decrypt the provisioner key", func(msg string) ([]byte, error) {
return ui.PromptPassword(msg)
}))
if err != nil {
return nil, err
}
// Decrypt returns the JSON representation of the JWK.
if err := json.Unmarshal(b, &jwk); err != nil {
return nil, errors.Wrap(err, "error parsing provisioner key")
}
signer, ok := jwk.Key.(crypto.Signer)
if !ok {
return nil, errors.New("error parsing provisioner key: key is not a crypto.Signer")
}
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader("kid", kid)
return newJoseSigner(signer, so)
}
func findProvisioner(ctx context.Context, client *ca.Client, typ provisioner.Type, name string) (provisioner.Interface, error) {
cursor := ""
for {
ps, err := client.ProvisionersWithContext(ctx, ca.WithProvisionerCursor(cursor))
if err != nil {
return nil, err
}
for _, p := range ps.Provisioners {
if p.GetType() == typ && p.GetName() == name {
return p, nil
}
}
if ps.NextCursor == "" {
return nil, errors.Errorf("provisioner with name %s was not found", name)
}
cursor = ps.NextCursor
}
}