Allow to configure the JWK using the encrypted key.

This commit is contained in:
Mariano Cano 2021-03-24 19:05:56 -07:00
parent e727532963
commit a9297100d8
6 changed files with 311 additions and 81 deletions

View file

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/cas/apiv1"
) )
@ -16,7 +17,7 @@ type stepIssuer interface {
} }
// newStepIssuer returns the configured step issuer. // newStepIssuer returns the configured step issuer.
func newStepIssuer(caURL *url.URL, iss *apiv1.CertificateIssuer) (stepIssuer, error) { func newStepIssuer(caURL *url.URL, client *ca.Client, iss *apiv1.CertificateIssuer) (stepIssuer, error) {
if err := validateCertificateIssuer(iss); err != nil { if err := validateCertificateIssuer(iss); err != nil {
return nil, err return nil, err
} }
@ -25,7 +26,7 @@ func newStepIssuer(caURL *url.URL, iss *apiv1.CertificateIssuer) (stepIssuer, er
case "x5c": case "x5c":
return newX5CIssuer(caURL, iss) return newX5CIssuer(caURL, iss)
case "jwk": case "jwk":
return newJWKIssuer(caURL, iss) return newJWKIssuer(caURL, client, iss)
default: default:
return nil, errors.Errorf("stepCAS `certificateIssuer.type` %s is not supported", iss.Type) return nil, errors.Errorf("stepCAS `certificateIssuer.type` %s is not supported", iss.Type)
} }
@ -65,11 +66,11 @@ func validateX5CIssuer(iss *apiv1.CertificateIssuer) error {
} }
} }
// validateJWKIssuer validates the configuration of jwk issuer. // validateJWKIssuer validates the configuration of jwk issuer. If the key is
// not given, then it will download it from the CA. If the password is not given
// it will be asked.
func validateJWKIssuer(iss *apiv1.CertificateIssuer) error { func validateJWKIssuer(iss *apiv1.CertificateIssuer) error {
switch { switch {
case iss.Key == "":
return errors.New("stepCAS `certificateIssuer.key` cannot be empty")
case iss.Provisioner == "": case iss.Provisioner == "":
return errors.New("stepCAS `certificateIssuer.provisioner` cannot be empty") return errors.New("stepCAS `certificateIssuer.provisioner` cannot be empty")
default: default:

View file

@ -6,7 +6,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/cas/apiv1"
"go.step.sm/crypto/jose"
) )
type mockErrIssuer struct{} type mockErrIssuer struct{}
@ -23,15 +25,27 @@ func (m mockErrIssuer) Lifetime(d time.Duration) time.Duration {
return d return d
} }
type mockErrSigner struct{}
func (s *mockErrSigner) Sign(payload []byte) (*jose.JSONWebSignature, error) {
return nil, apiv1.ErrNotImplemented{}
}
func (s *mockErrSigner) Options() jose.SignerOptions {
return jose.SignerOptions{}
}
func Test_newStepIssuer(t *testing.T) { func Test_newStepIssuer(t *testing.T) {
caURL, err := url.Parse("https://ca.smallstep.com") caURL, client := testCAHelper(t)
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
type args struct { type args struct {
caURL *url.URL caURL *url.URL
iss *apiv1.CertificateIssuer client *ca.Client
iss *apiv1.CertificateIssuer
} }
tests := []struct { tests := []struct {
name string name string
@ -39,7 +53,7 @@ func Test_newStepIssuer(t *testing.T) {
want stepIssuer want stepIssuer
wantErr bool wantErr bool
}{ }{
{"x5c", args{caURL, &apiv1.CertificateIssuer{ {"x5c", args{caURL, client, &apiv1.CertificateIssuer{
Type: "x5c", Type: "x5c",
Provisioner: "X5C", Provisioner: "X5C",
Certificate: testX5CPath, Certificate: testX5CPath,
@ -50,16 +64,16 @@ func Test_newStepIssuer(t *testing.T) {
keyFile: testX5CKeyPath, keyFile: testX5CKeyPath,
issuer: "X5C", issuer: "X5C",
}, false}, }, false},
{"jwk", args{caURL, &apiv1.CertificateIssuer{ {"jwk", args{caURL, client, &apiv1.CertificateIssuer{
Type: "jwk", Type: "jwk",
Provisioner: "ra@doe.org", Provisioner: "ra@doe.org",
Key: testX5CKeyPath, Key: testX5CKeyPath,
}}, &jwkIssuer{ }}, &jwkIssuer{
caURL: caURL, caURL: caURL,
keyFile: testX5CKeyPath, issuer: "ra@doe.org",
issuer: "ra@doe.org", signer: signer,
}, false}, }, false},
{"fail", args{caURL, &apiv1.CertificateIssuer{ {"fail", args{caURL, client, &apiv1.CertificateIssuer{
Type: "unknown", Type: "unknown",
Provisioner: "ra@doe.org", Provisioner: "ra@doe.org",
Key: testX5CKeyPath, Key: testX5CKeyPath,
@ -67,11 +81,14 @@ func Test_newStepIssuer(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := newStepIssuer(tt.args.caURL, tt.args.iss) got, err := newStepIssuer(tt.args.caURL, tt.args.client, tt.args.iss)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("newStepIssuer() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("newStepIssuer() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if tt.args.iss.Type == "jwk" && got != nil && tt.want != nil {
got.(*jwkIssuer).signer = tt.want.(*jwkIssuer).signer
}
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("newStepIssuer() = %v, want %v", got, tt.want) t.Errorf("newStepIssuer() = %v, want %v", got, tt.want)
} }

View file

@ -1,33 +1,55 @@
package stepcas package stepcas
import ( import (
"crypto"
"encoding/json"
"net/url" "net/url"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/cas/apiv1"
"go.step.sm/cli-utils/ui"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/randutil" "go.step.sm/crypto/randutil"
) )
type jwkIssuer struct { type jwkIssuer struct {
caURL *url.URL caURL *url.URL
issuer string issuer string
keyFile string signer jose.Signer
password string
} }
func newJWKIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) { func newJWKIssuer(caURL *url.URL, client *ca.Client, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) {
_, err := newJWKSigner(cfg.Key, cfg.Password) var err error
if err != nil { var signer jose.Signer
return nil, err // 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
}
} }
return &jwkIssuer{ return &jwkIssuer{
caURL: caURL, caURL: caURL,
issuer: cfg.Provisioner, issuer: cfg.Provisioner,
keyFile: cfg.Key, signer: signer,
password: cfg.Password,
}, nil }, nil
} }
@ -50,18 +72,13 @@ func (i *jwkIssuer) Lifetime(d time.Duration) time.Duration {
} }
func (i *jwkIssuer) createToken(aud, sub string, sans []string) (string, error) { func (i *jwkIssuer) createToken(aud, sub string, sans []string) (string, error) {
signer, err := newJWKSigner(i.keyFile, i.password)
if err != nil {
return "", err
}
id, err := randutil.Hex(64) // 256 bits id, err := randutil.Hex(64) // 256 bits
if err != nil { if err != nil {
return "", err return "", err
} }
claims := defaultClaims(i.issuer, sub, aud, id) claims := defaultClaims(i.issuer, sub, aud, id)
builder := jose.Signed(signer).Claims(claims) builder := jose.Signed(i.signer).Claims(claims)
if len(sans) > 0 { if len(sans) > 0 {
builder = builder.Claims(map[string]interface{}{ builder = builder.Claims(map[string]interface{}{
"sans": sans, "sans": sans,
@ -90,3 +107,51 @@ func newJWKSigner(keyFile, password string) (jose.Signer, error) {
so.WithHeader("kid", kid) so.WithHeader("kid", kid)
return newJoseSigner(signer, so) 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(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
}
}

View file

@ -14,11 +14,15 @@ func Test_jwkIssuer_SignToken(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
if err != nil {
t.Fatal(err)
}
type fields struct { type fields struct {
caURL *url.URL caURL *url.URL
keyFile string issuer string
issuer string signer jose.Signer
} }
type args struct { type args struct {
subject string subject string
@ -35,16 +39,15 @@ func Test_jwkIssuer_SignToken(t *testing.T) {
args args args args
wantErr bool wantErr bool
}{ }{
{"ok", fields{caURL, testX5CKeyPath, "ra@doe.org"}, args{"doe", []string{"doe.org"}}, false}, {"ok", fields{caURL, "ra@doe.org", signer}, args{"doe", []string{"doe.org"}}, false},
{"fail key", fields{caURL, "", "ra@doe.org"}, args{"doe", []string{"doe.org"}}, true}, {"fail", fields{caURL, "ra@doe.org", &mockErrSigner{}}, args{"doe", []string{"doe.org"}}, true},
{"fail no signer", fields{caURL, testIssPath, "ra@doe.org"}, args{"doe", []string{"doe.org"}}, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
i := &jwkIssuer{ i := &jwkIssuer{
caURL: tt.fields.caURL, caURL: tt.fields.caURL,
keyFile: tt.fields.keyFile, issuer: tt.fields.issuer,
issuer: tt.fields.issuer, signer: tt.fields.signer,
} }
got, err := i.SignToken(tt.args.subject, tt.args.sans) got, err := i.SignToken(tt.args.subject, tt.args.sans)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
@ -78,11 +81,15 @@ func Test_jwkIssuer_RevokeToken(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
if err != nil {
t.Fatal(err)
}
type fields struct { type fields struct {
caURL *url.URL caURL *url.URL
keyFile string issuer string
issuer string signer jose.Signer
} }
type args struct { type args struct {
subject string subject string
@ -98,16 +105,15 @@ func Test_jwkIssuer_RevokeToken(t *testing.T) {
args args args args
wantErr bool wantErr bool
}{ }{
{"ok", fields{caURL, testX5CKeyPath, "ra@smallstep.com"}, args{"doe"}, false}, {"ok", fields{caURL, "ra@doe.org", signer}, args{"doe"}, false},
{"fail key", fields{caURL, "", "ra@smallstep.com"}, args{"doe"}, true}, {"ok", fields{caURL, "ra@doe.org", &mockErrSigner{}}, args{"doe"}, true},
{"fail no signer", fields{caURL, testIssPath, "ra@smallstep.com"}, args{"doe"}, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
i := &jwkIssuer{ i := &jwkIssuer{
caURL: tt.fields.caURL, caURL: tt.fields.caURL,
keyFile: tt.fields.keyFile, issuer: tt.fields.issuer,
issuer: tt.fields.issuer, signer: tt.fields.signer,
} }
got, err := i.RevokeToken(tt.args.subject) got, err := i.RevokeToken(tt.args.subject)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
@ -140,11 +146,15 @@ func Test_jwkIssuer_Lifetime(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
if err != nil {
t.Fatal(err)
}
type fields struct { type fields struct {
caURL *url.URL caURL *url.URL
keyFile string issuer string
issuer string signer jose.Signer
} }
type args struct { type args struct {
d time.Duration d time.Duration
@ -155,14 +165,14 @@ func Test_jwkIssuer_Lifetime(t *testing.T) {
args args args args
want time.Duration want time.Duration
}{ }{
{"ok", fields{caURL, testX5CKeyPath, "ra@smallstep.com"}, args{time.Second}, time.Second}, {"ok", fields{caURL, "ra@smallstep.com", signer}, args{time.Second}, time.Second},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
i := &jwkIssuer{ i := &jwkIssuer{
caURL: tt.fields.caURL, caURL: tt.fields.caURL,
keyFile: tt.fields.keyFile, issuer: tt.fields.issuer,
issuer: tt.fields.issuer, signer: tt.fields.signer,
} }
if got := i.Lifetime(tt.args.d); got != tt.want { if got := i.Lifetime(tt.args.d); got != tt.want {
t.Errorf("jwkIssuer.Lifetime() = %v, want %v", got, tt.want) t.Errorf("jwkIssuer.Lifetime() = %v, want %v", got, tt.want)
@ -170,3 +180,56 @@ func Test_jwkIssuer_Lifetime(t *testing.T) {
}) })
} }
} }
func Test_newJWKSignerFromEncryptedKey(t *testing.T) {
encrypt := func(plaintext string) string {
recipient := jose.Recipient{
Algorithm: jose.PBES2_HS256_A128KW,
Key: testPassword,
PBES2Count: jose.PBKDF2Iterations,
PBES2Salt: []byte{0x01, 0x02},
}
opts := new(jose.EncrypterOptions)
opts.WithContentType(jose.ContentType("jwk+json"))
encrypter, err := jose.NewEncrypter(jose.DefaultEncAlgorithm, recipient, opts)
if err != nil {
t.Fatal(err)
}
jwe, err := encrypter.Encrypt([]byte(plaintext))
if err != nil {
t.Fatal(err)
}
ret, err := jwe.CompactSerialize()
if err != nil {
t.Fatal(err)
}
return ret
}
type args struct {
kid string
key string
password string
}
tests := []struct {
name string
args args
wantErr bool
}{
{"ok", args{testKeyID, testEncryptedJWKKey, testPassword}, false},
{"fail decrypt", args{testKeyID, testEncryptedJWKKey, "bad-password"}, true},
{"fail unmarshal", args{testKeyID, encrypt(`{not a json}`), testPassword}, true},
{"fail not signer", args{testKeyID, encrypt(`{"kty":"oct","k":"password"}`), testPassword}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := newJWKSignerFromEncryptedKey(tt.args.kid, tt.args.key, tt.args.password)
if (err != nil) != tt.wantErr {
t.Errorf("newJWKSignerFromEncryptedKey() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -41,14 +41,14 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) {
return nil, errors.Wrap(err, "stepCAS `certificateAuthority` is not valid") return nil, errors.Wrap(err, "stepCAS `certificateAuthority` is not valid")
} }
// Create configured issuer // Create client.
iss, err := newStepIssuer(caURL, opts.CertificateIssuer) client, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint))
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Create client. // Create configured issuer
client, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint)) iss, err := newStepIssuer(caURL, client, opts.CertificateIssuer)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -21,8 +21,10 @@ import (
"time" "time"
"github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/ca"
"github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/cas/apiv1"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil" "go.step.sm/crypto/pemutil"
"go.step.sm/crypto/randutil" "go.step.sm/crypto/randutil"
"go.step.sm/crypto/x509util" "go.step.sm/crypto/x509util"
@ -42,6 +44,7 @@ var (
testX5CKey crypto.Signer testX5CKey crypto.Signer
testX5CPath, testX5CKeyPath string testX5CPath, testX5CKeyPath string
testPassword, testEncryptedKeyPath string testPassword, testEncryptedKeyPath string
testKeyID, testEncryptedJWKKey string
testCR *x509.CertificateRequest testCR *x509.CertificateRequest
testCrt *x509.Certificate testCrt *x509.Certificate
@ -157,6 +160,27 @@ func testCAHelper(t *testing.T) (*url.URL, *ca.Client) {
writeJSON(w, api.RevokeResponse{ writeJSON(w, api.RevokeResponse{
Status: "ok", Status: "ok",
}) })
case r.RequestURI == "/provisioners":
w.WriteHeader(http.StatusOK)
writeJSON(w, api.ProvisionersResponse{
NextCursor: "cursor",
Provisioners: []provisioner.Interface{
&provisioner.JWK{
Type: "JWK",
Name: "ra@doe.org",
Key: &jose.JSONWebKey{KeyID: testKeyID, Key: testX5CKey.Public()},
EncryptedKey: testEncryptedJWKKey,
},
&provisioner.JWK{
Type: "JWK",
Name: "empty@doe.org",
Key: &jose.JSONWebKey{KeyID: testKeyID, Key: testX5CKey.Public()},
},
},
})
case r.RequestURI == "/provisioners?cursor=cursor":
w.WriteHeader(http.StatusOK)
writeJSON(w, api.ProvisionersResponse{})
default: default:
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, `{"error":"not found"}`) fmt.Fprintf(w, `{"error":"not found"}`)
@ -203,12 +227,16 @@ func testX5CIssuer(t *testing.T, caURL *url.URL, password string) *x5cIssuer {
func testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer { func testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer {
t.Helper() t.Helper()
key, givenPassword := testX5CKeyPath, password client, err := ca.NewClient(caURL.String(), ca.WithTransport(http.DefaultTransport))
if err != nil {
t.Fatal(err)
}
key := testX5CKeyPath
if password != "" { if password != "" {
key = testEncryptedKeyPath key = testEncryptedKeyPath
password = testPassword password = testPassword
} }
jwk, err := newJWKIssuer(caURL, &apiv1.CertificateIssuer{ jwk, err := newJWKIssuer(caURL, client, &apiv1.CertificateIssuer{
Type: "jwk", Type: "jwk",
Provisioner: "ra@doe.org", Provisioner: "ra@doe.org",
Key: key, Key: key,
@ -217,7 +245,7 @@ func testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
jwk.password = givenPassword
return jwk return jwk
} }
@ -225,6 +253,7 @@ func TestMain(m *testing.M) {
testRootCrt, testRootKey = mustSignCertificate("Test Root Certificate", nil, x509util.DefaultRootTemplate, nil, nil) testRootCrt, testRootKey = mustSignCertificate("Test Root Certificate", nil, x509util.DefaultRootTemplate, nil, nil)
testIssCrt, testIssKey = mustSignCertificate("Test Intermediate Certificate", nil, x509util.DefaultIntermediateTemplate, testRootCrt, testRootKey) testIssCrt, testIssKey = mustSignCertificate("Test Intermediate Certificate", nil, x509util.DefaultIntermediateTemplate, testRootCrt, testRootKey)
testX5CCrt, testX5CKey = mustSignCertificate("Test X5C Certificate", nil, x509util.DefaultLeafTemplate, testIssCrt, testIssKey) testX5CCrt, testX5CKey = mustSignCertificate("Test X5C Certificate", nil, x509util.DefaultLeafTemplate, testIssCrt, testIssKey)
testRootFingerprint = x509util.Fingerprint(testRootCrt)
// Final certificate. // Final certificate.
var err error var err error
@ -241,14 +270,27 @@ func TestMain(m *testing.M) {
panic(err) panic(err)
} }
// Password used to encrypto the key // Password used to encrypt the key.
testPassword, err = randutil.Hex(32) testPassword, err = randutil.Hex(32)
if err != nil { if err != nil {
panic(err) panic(err)
} }
testRootFingerprint = x509util.Fingerprint(testRootCrt) // Encrypted JWK key used when the key is downloaded from the CA.
jwe, err := jose.EncryptJWK(&jose.JSONWebKey{Key: testX5CKey}, []byte(testPassword))
if err != nil {
panic(err)
}
testEncryptedJWKKey, err = jwe.CompactSerialize()
if err != nil {
panic(err)
}
testKeyID, err = jose.Thumbprint(&jose.JSONWebKey{Key: testX5CKey})
if err != nil {
panic(err)
}
// Create test files.
path, err := ioutil.TempDir(os.TempDir(), "stepcas") path, err := ioutil.TempDir(os.TempDir(), "stepcas")
if err != nil { if err != nil {
panic(err) panic(err)
@ -301,6 +343,11 @@ func Test_init(t *testing.T) {
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
caURL, client := testCAHelper(t) caURL, client := testCAHelper(t)
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
if err != nil {
t.Fatal(err)
}
type args struct { type args struct {
ctx context.Context ctx context.Context
opts apiv1.Options opts apiv1.Options
@ -340,9 +387,26 @@ func TestNew(t *testing.T) {
}, },
}}, &StepCAS{ }}, &StepCAS{
iss: &jwkIssuer{ iss: &jwkIssuer{
caURL: caURL, caURL: caURL,
keyFile: testX5CKeyPath, issuer: "ra@doe.org",
issuer: "ra@doe.org", signer: signer,
},
client: client,
fingerprint: testRootFingerprint,
}, false},
{"ok jwk provisioners", args{context.TODO(), apiv1.Options{
CertificateAuthority: caURL.String(),
CertificateAuthorityFingerprint: testRootFingerprint,
CertificateIssuer: &apiv1.CertificateIssuer{
Type: "jwk",
Provisioner: "ra@doe.org",
Password: testPassword,
},
}}, &StepCAS{
iss: &jwkIssuer{
caURL: caURL,
issuer: "ra@doe.org",
signer: signer,
}, },
client: client, client: client,
fingerprint: testRootFingerprint, fingerprint: testRootFingerprint,
@ -396,6 +460,33 @@ func TestNew(t *testing.T) {
Key: testX5CKeyPath, Key: testX5CKeyPath,
}, },
}}, nil, true}, }}, nil, true},
{"fail provisioner not found", args{context.TODO(), apiv1.Options{
CertificateAuthority: caURL.String(),
CertificateAuthorityFingerprint: testRootFingerprint,
CertificateIssuer: &apiv1.CertificateIssuer{
Type: "jwk",
Provisioner: "notfound@doe.org",
Password: testPassword,
},
}}, nil, true},
{"fail invalid password", args{context.TODO(), apiv1.Options{
CertificateAuthority: caURL.String(),
CertificateAuthorityFingerprint: testRootFingerprint,
CertificateIssuer: &apiv1.CertificateIssuer{
Type: "jwk",
Provisioner: "ra@doe.org",
Password: "bad-password",
},
}}, nil, true},
{"fail no key", args{context.TODO(), apiv1.Options{
CertificateAuthority: caURL.String(),
CertificateAuthorityFingerprint: testRootFingerprint,
CertificateIssuer: &apiv1.CertificateIssuer{
Type: "jwk",
Provisioner: "empty@doe.org",
Password: testPassword,
},
}}, nil, true},
{"fail certificate", args{context.TODO(), apiv1.Options{ {"fail certificate", args{context.TODO(), apiv1.Options{
CertificateAuthority: caURL.String(), CertificateAuthority: caURL.String(),
CertificateAuthorityFingerprint: testRootFingerprint, CertificateAuthorityFingerprint: testRootFingerprint,
@ -496,9 +587,12 @@ func TestNew(t *testing.T) {
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
// We cannot compare client // We cannot compare neither the client nor the signer.
if got != nil && tt.want != nil { if got != nil && tt.want != nil {
got.client = tt.want.client got.client = tt.want.client
if jwk, ok := got.iss.(*jwkIssuer); ok {
jwk.signer = signer
}
} }
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want) t.Errorf("New() = %v, want %v", got, tt.want)
@ -514,7 +608,6 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
x5cEnc := testX5CIssuer(t, caURL, testPassword) x5cEnc := testX5CIssuer(t, caURL, testPassword)
jwkEnc := testJWKIssuer(t, caURL, testPassword) jwkEnc := testJWKIssuer(t, caURL, testPassword)
x5cBad := testX5CIssuer(t, caURL, "bad-password") x5cBad := testX5CIssuer(t, caURL, "bad-password")
jwkBad := testJWKIssuer(t, caURL, "bad-password")
type fields struct { type fields struct {
iss stepIssuer iss stepIssuer
@ -579,10 +672,6 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
CSR: testCR, CSR: testCR,
Lifetime: time.Hour, Lifetime: time.Hour,
}}, nil, true}, }}, nil, true},
{"fail jwk password", fields{jwkBad, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testCR,
Lifetime: time.Hour,
}}, nil, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -658,7 +747,6 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
x5cEnc := testX5CIssuer(t, caURL, testPassword) x5cEnc := testX5CIssuer(t, caURL, testPassword)
jwkEnc := testJWKIssuer(t, caURL, testPassword) jwkEnc := testJWKIssuer(t, caURL, testPassword)
x5cBad := testX5CIssuer(t, caURL, "bad-password") x5cBad := testX5CIssuer(t, caURL, "bad-password")
jwkBad := testJWKIssuer(t, caURL, "bad-password")
type fields struct { type fields struct {
iss stepIssuer iss stepIssuer
@ -729,10 +817,6 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
SerialNumber: "ok", SerialNumber: "ok",
Certificate: nil, Certificate: nil,
}}, nil, true}, }}, nil, true},
{"fail jwk password", fields{jwkBad, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
SerialNumber: "ok",
Certificate: nil,
}}, nil, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {