227 lines
6.2 KiB
Go
227 lines
6.2 KiB
Go
package ca
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
|
"go.step.sm/cli-utils/token"
|
|
"go.step.sm/cli-utils/token/provision"
|
|
"go.step.sm/crypto/jose"
|
|
"go.step.sm/crypto/randutil"
|
|
)
|
|
|
|
const tokenLifetime = 5 * time.Minute
|
|
|
|
// Provisioner is an authorized entity that can sign tokens necessary for
|
|
// signature requests.
|
|
type Provisioner struct {
|
|
*Client
|
|
name string
|
|
kid string
|
|
audience string
|
|
sshAudience string
|
|
fingerprint string
|
|
jwk *jose.JSONWebKey
|
|
tokenLifetime time.Duration
|
|
}
|
|
|
|
// NewProvisioner loads and decrypts key material from the CA for the named
|
|
// provisioner. The key identified by `kid` will be used if specified. If `kid`
|
|
// is the empty string we'll use the first key for the named provisioner that
|
|
// decrypts using `password`.
|
|
func NewProvisioner(name, kid, caURL string, password []byte, opts ...ClientOption) (*Provisioner, error) {
|
|
client, err := NewClient(caURL, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get the fingerprint of the current connection
|
|
fp, err := client.RootFingerprint()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var jwk *jose.JSONWebKey
|
|
switch {
|
|
case name == "":
|
|
return nil, errors.New("provisioner name cannot be empty")
|
|
case kid == "":
|
|
jwk, err = loadProvisionerJWKByName(client, name, password)
|
|
default:
|
|
jwk, err = loadProvisionerJWKByKid(client, kid, password)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Provisioner{
|
|
Client: client,
|
|
name: name,
|
|
kid: jwk.KeyID,
|
|
audience: client.endpoint.ResolveReference(&url.URL{Path: "/1.0/sign"}).String(),
|
|
sshAudience: client.endpoint.ResolveReference(&url.URL{Path: "/1.0/ssh/sign"}).String(),
|
|
fingerprint: fp,
|
|
jwk: jwk,
|
|
tokenLifetime: tokenLifetime,
|
|
}, nil
|
|
}
|
|
|
|
// Name returns the provisioner's name.
|
|
func (p *Provisioner) Name() string {
|
|
return p.name
|
|
}
|
|
|
|
// Kid returns the provisioners key ID.
|
|
func (p *Provisioner) Kid() string {
|
|
return p.kid
|
|
}
|
|
|
|
// SetFingerprint overwrites the default fingerprint used.
|
|
func (p *Provisioner) SetFingerprint(sum string) {
|
|
p.fingerprint = sum
|
|
}
|
|
|
|
// Token generates a bootstrap token for a subject.
|
|
func (p *Provisioner) Token(subject string, sans ...string) (string, error) {
|
|
if len(sans) == 0 {
|
|
sans = []string{subject}
|
|
}
|
|
|
|
// A random jwt id will be used to identify duplicated tokens
|
|
jwtID, err := randutil.Hex(64) // 256 bits
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.Add(tokenLifetime)
|
|
tokOptions := []token.Options{
|
|
token.WithJWTID(jwtID),
|
|
token.WithKid(p.kid),
|
|
token.WithIssuer(p.name),
|
|
token.WithAudience(p.audience),
|
|
token.WithValidity(notBefore, notAfter),
|
|
token.WithSANS(sans),
|
|
}
|
|
|
|
if p.fingerprint != "" {
|
|
tokOptions = append(tokOptions, token.WithSHA(p.fingerprint))
|
|
}
|
|
|
|
tok, err := provision.New(subject, tokOptions...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return tok.SignedString(p.jwk.Algorithm, p.jwk.Key)
|
|
}
|
|
|
|
// SSHToken generates a SSH token.
|
|
func (p *Provisioner) SSHToken(certType, keyID string, principals []string) (string, error) {
|
|
jwtID, err := randutil.Hex(64)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.Add(tokenLifetime)
|
|
tokOptions := []token.Options{
|
|
token.WithJWTID(jwtID),
|
|
token.WithKid(p.kid),
|
|
token.WithIssuer(p.name),
|
|
token.WithAudience(p.sshAudience),
|
|
token.WithValidity(notBefore, notAfter),
|
|
token.WithSSH(provisioner.SignSSHOptions{
|
|
CertType: certType,
|
|
Principals: principals,
|
|
KeyID: keyID,
|
|
}),
|
|
}
|
|
|
|
if p.fingerprint != "" {
|
|
tokOptions = append(tokOptions, token.WithSHA(p.fingerprint))
|
|
}
|
|
|
|
tok, err := provision.New(keyID, tokOptions...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return tok.SignedString(p.jwk.Algorithm, p.jwk.Key)
|
|
}
|
|
|
|
func decryptProvisionerJWK(encryptedKey string, password []byte) (*jose.JSONWebKey, error) {
|
|
enc, err := jose.ParseEncrypted(encryptedKey)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error parsing provisioner encrypted key")
|
|
}
|
|
data, err := enc.Decrypt(password)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error decrypting provisioner key with provided password")
|
|
}
|
|
jwk := new(jose.JSONWebKey)
|
|
if err := json.Unmarshal(data, jwk); err != nil {
|
|
return nil, errors.Wrap(err, "error unmarshaling provisioning key")
|
|
}
|
|
return jwk, nil
|
|
}
|
|
|
|
// loadProvisionerJWKByKid retrieves a provisioner key from the CA by key ID and
|
|
// decrypts it using the specified password.
|
|
func loadProvisionerJWKByKid(client *Client, kid string, password []byte) (*jose.JSONWebKey, error) {
|
|
encrypted, err := getProvisionerKey(client, kid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return decryptProvisionerJWK(encrypted, password)
|
|
}
|
|
|
|
// loadProvisionerJWKByName retrieves the list of provisioners and encrypted key then
|
|
// returns the key of the first provisioner with a matching name that can be successfully
|
|
// decrypted with the specified password.
|
|
func loadProvisionerJWKByName(client *Client, name string, password []byte) (*jose.JSONWebKey, error) {
|
|
provisioners, err := getProvisioners(client)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error getting the provisioners")
|
|
}
|
|
|
|
for _, provisioner := range provisioners {
|
|
if provisioner.GetName() == name {
|
|
if _, encryptedKey, ok := provisioner.GetEncryptedKey(); ok {
|
|
if key, err := decryptProvisionerJWK(encryptedKey, password); err == nil {
|
|
return key, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, errors.Errorf("provisioner '%s' not found (or your password is wrong)", name)
|
|
}
|
|
|
|
// getProvisioners returns the list of provisioners using the configured client.
|
|
func getProvisioners(client *Client) (provisioner.List, error) {
|
|
var cursor string
|
|
var provisioners provisioner.List
|
|
for {
|
|
resp, err := client.Provisioners(WithProvisionerCursor(cursor), WithProvisionerLimit(100))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
provisioners = append(provisioners, resp.Provisioners...)
|
|
if resp.NextCursor == "" {
|
|
return provisioners, nil
|
|
}
|
|
cursor = resp.NextCursor
|
|
}
|
|
}
|
|
|
|
// getProvisionerKey returns the encrypted provisioner key for the given kid.
|
|
func getProvisionerKey(client *Client, kid string) (string, error) {
|
|
resp, err := client.ProvisionerKey(kid)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Key, nil
|
|
}
|