forked from TrueCloudLab/certificates
Allow to configure the JWK using the encrypted key.
This commit is contained in:
parent
e727532963
commit
a9297100d8
6 changed files with 311 additions and 81 deletions
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue