2021-03-23 23:14:49 +00:00
|
|
|
package stepcas
|
|
|
|
|
|
|
|
import (
|
2021-03-25 02:05:56 +00:00
|
|
|
"crypto"
|
|
|
|
"encoding/json"
|
2021-03-23 23:14:49 +00:00
|
|
|
"net/url"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2021-03-25 02:05:56 +00:00
|
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
|
|
|
"github.com/smallstep/certificates/ca"
|
2021-03-23 23:14:49 +00:00
|
|
|
"github.com/smallstep/certificates/cas/apiv1"
|
2021-03-25 02:05:56 +00:00
|
|
|
"go.step.sm/cli-utils/ui"
|
2021-03-23 23:14:49 +00:00
|
|
|
"go.step.sm/crypto/jose"
|
|
|
|
"go.step.sm/crypto/randutil"
|
|
|
|
)
|
|
|
|
|
|
|
|
type jwkIssuer struct {
|
2021-03-25 02:05:56 +00:00
|
|
|
caURL *url.URL
|
|
|
|
issuer string
|
|
|
|
signer jose.Signer
|
2021-03-23 23:14:49 +00:00
|
|
|
}
|
|
|
|
|
2021-03-25 02:05:56 +00:00
|
|
|
func newJWKIssuer(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(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
|
|
|
|
}
|
2021-03-23 23:14:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &jwkIssuer{
|
2021-03-25 02:05:56 +00:00
|
|
|
caURL: caURL,
|
|
|
|
issuer: cfg.Provisioner,
|
|
|
|
signer: signer,
|
2021-03-23 23:14:49 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *jwkIssuer) SignToken(subject string, sans []string) (string, error) {
|
|
|
|
aud := i.caURL.ResolveReference(&url.URL{
|
|
|
|
Path: "/1.0/sign",
|
|
|
|
}).String()
|
|
|
|
return i.createToken(aud, subject, sans)
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *jwkIssuer) Lifetime(d time.Duration) time.Duration {
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *jwkIssuer) createToken(aud, sub string, sans []string) (string, error) {
|
|
|
|
id, err := randutil.Hex(64) // 256 bits
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
claims := defaultClaims(i.issuer, sub, aud, id)
|
2021-03-25 02:05:56 +00:00
|
|
|
builder := jose.Signed(i.signer).Claims(claims)
|
2021-03-23 23:14:49 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-03-24 00:54:42 +00:00
|
|
|
func newJWKSigner(keyFile, password string) (jose.Signer, error) {
|
|
|
|
signer, err := readKey(keyFile, password)
|
2021-03-23 23:14:49 +00:00
|
|
|
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)
|
|
|
|
}
|
2021-03-25 02:05:56 +00:00
|
|
|
|
|
|
|
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(client *ca.Client, typ provisioner.Type, name string) (provisioner.Interface, error) {
|
|
|
|
cursor := ""
|
|
|
|
for {
|
|
|
|
ps, err := client.Provisioners(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
|
|
|
|
}
|
|
|
|
}
|