Merge pull request #516 from smallstep/ra-mode-improvements
RA mode improvements
This commit is contained in:
commit
5249ce794b
9 changed files with 354 additions and 89 deletions
20
ca/ca.go
20
ca/ca.go
|
@ -25,6 +25,7 @@ import (
|
|||
type options struct {
|
||||
configFile string
|
||||
password []byte
|
||||
issuerPassword []byte
|
||||
database db.AuthDB
|
||||
}
|
||||
|
||||
|
@ -53,6 +54,14 @@ func WithPassword(password []byte) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithIssuerPassword sets the given password as the configured certificate
|
||||
// issuer password in the CA options.
|
||||
func WithIssuerPassword(password []byte) Option {
|
||||
return func(o *options) {
|
||||
o.issuerPassword = password
|
||||
}
|
||||
}
|
||||
|
||||
// WithDatabase sets the given authority database to the CA options.
|
||||
func WithDatabase(db db.AuthDB) Option {
|
||||
return func(o *options) {
|
||||
|
@ -82,10 +91,18 @@ func New(config *authority.Config, opts ...Option) (*CA, error) {
|
|||
|
||||
// Init initializes the CA with the given configuration.
|
||||
func (ca *CA) Init(config *authority.Config) (*CA, error) {
|
||||
if l := len(ca.opts.password); l > 0 {
|
||||
// Intermediate Password.
|
||||
if len(ca.opts.password) > 0 {
|
||||
ca.config.Password = string(ca.opts.password)
|
||||
}
|
||||
|
||||
// Certificate issuer password for RA mode.
|
||||
if len(ca.opts.issuerPassword) > 0 {
|
||||
if ca.config.AuthorityConfig != nil && ca.config.AuthorityConfig.CertificateIssuer != nil {
|
||||
ca.config.AuthorityConfig.CertificateIssuer.Password = string(ca.opts.issuerPassword)
|
||||
}
|
||||
}
|
||||
|
||||
var opts []authority.Option
|
||||
if ca.opts.database != nil {
|
||||
opts = append(opts, authority.WithDatabase(ca.opts.database))
|
||||
|
@ -213,6 +230,7 @@ func (ca *CA) Reload() error {
|
|||
|
||||
newCA, err := New(config,
|
||||
WithPassword(ca.opts.password),
|
||||
WithIssuerPassword(ca.opts.issuerPassword),
|
||||
WithConfigFile(ca.opts.configFile),
|
||||
WithDatabase(ca.auth.GetDatabase()),
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
)
|
||||
|
||||
|
@ -16,7 +17,7 @@ type stepIssuer interface {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -25,7 +26,7 @@ func newStepIssuer(caURL *url.URL, iss *apiv1.CertificateIssuer) (stepIssuer, er
|
|||
case "x5c":
|
||||
return newX5CIssuer(caURL, iss)
|
||||
case "jwk":
|
||||
return newJWKIssuer(caURL, iss)
|
||||
return newJWKIssuer(caURL, client, iss)
|
||||
default:
|
||||
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 set
|
||||
// it will be prompted.
|
||||
func validateJWKIssuer(iss *apiv1.CertificateIssuer) error {
|
||||
switch {
|
||||
case iss.Key == "":
|
||||
return errors.New("stepCAS `certificateIssuer.key` cannot be empty")
|
||||
case iss.Provisioner == "":
|
||||
return errors.New("stepCAS `certificateIssuer.provisioner` cannot be empty")
|
||||
default:
|
||||
|
|
|
@ -6,7 +6,9 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
"go.step.sm/crypto/jose"
|
||||
)
|
||||
|
||||
type mockErrIssuer struct{}
|
||||
|
@ -23,14 +25,26 @@ func (m mockErrIssuer) Lifetime(d time.Duration) time.Duration {
|
|||
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) {
|
||||
caURL, err := url.Parse("https://ca.smallstep.com")
|
||||
caURL, client := testCAHelper(t)
|
||||
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
caURL *url.URL
|
||||
client *ca.Client
|
||||
iss *apiv1.CertificateIssuer
|
||||
}
|
||||
tests := []struct {
|
||||
|
@ -39,7 +53,7 @@ func Test_newStepIssuer(t *testing.T) {
|
|||
want stepIssuer
|
||||
wantErr bool
|
||||
}{
|
||||
{"x5c", args{caURL, &apiv1.CertificateIssuer{
|
||||
{"x5c", args{caURL, client, &apiv1.CertificateIssuer{
|
||||
Type: "x5c",
|
||||
Provisioner: "X5C",
|
||||
Certificate: testX5CPath,
|
||||
|
@ -50,16 +64,16 @@ func Test_newStepIssuer(t *testing.T) {
|
|||
keyFile: testX5CKeyPath,
|
||||
issuer: "X5C",
|
||||
}, false},
|
||||
{"jwk", args{caURL, &apiv1.CertificateIssuer{
|
||||
{"jwk", args{caURL, client, &apiv1.CertificateIssuer{
|
||||
Type: "jwk",
|
||||
Provisioner: "ra@doe.org",
|
||||
Key: testX5CKeyPath,
|
||||
}}, &jwkIssuer{
|
||||
caURL: caURL,
|
||||
keyFile: testX5CKeyPath,
|
||||
issuer: "ra@doe.org",
|
||||
signer: signer,
|
||||
}, false},
|
||||
{"fail", args{caURL, &apiv1.CertificateIssuer{
|
||||
{"fail", args{caURL, client, &apiv1.CertificateIssuer{
|
||||
Type: "unknown",
|
||||
Provisioner: "ra@doe.org",
|
||||
Key: testX5CKeyPath,
|
||||
|
@ -67,11 +81,14 @@ func Test_newStepIssuer(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
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 {
|
||||
t.Errorf("newStepIssuer() error = %v, wantErr %v", err, tt.wantErr)
|
||||
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) {
|
||||
t.Errorf("newStepIssuer() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
package stepcas
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
@ -13,21 +18,38 @@ import (
|
|||
type jwkIssuer struct {
|
||||
caURL *url.URL
|
||||
issuer string
|
||||
keyFile string
|
||||
password string
|
||||
signer jose.Signer
|
||||
}
|
||||
|
||||
func newJWKIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) {
|
||||
_, err := newJWKSigner(cfg.Key, cfg.Password)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return &jwkIssuer{
|
||||
caURL: caURL,
|
||||
issuer: cfg.Provisioner,
|
||||
keyFile: cfg.Key,
|
||||
password: cfg.Password,
|
||||
signer: signer,
|
||||
}, 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) {
|
||||
signer, err := newJWKSigner(i.keyFile, i.password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := randutil.Hex(64) // 256 bits
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims := defaultClaims(i.issuer, sub, aud, id)
|
||||
builder := jose.Signed(signer).Claims(claims)
|
||||
builder := jose.Signed(i.signer).Claims(claims)
|
||||
if len(sans) > 0 {
|
||||
builder = builder.Claims(map[string]interface{}{
|
||||
"sans": sans,
|
||||
|
@ -90,3 +107,51 @@ func newJWKSigner(keyFile, password string) (jose.Signer, error) {
|
|||
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(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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
caURL *url.URL
|
||||
keyFile string
|
||||
issuer string
|
||||
signer jose.Signer
|
||||
}
|
||||
type args struct {
|
||||
subject string
|
||||
|
@ -35,16 +39,15 @@ func Test_jwkIssuer_SignToken(t *testing.T) {
|
|||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{caURL, testX5CKeyPath, "ra@doe.org"}, args{"doe", []string{"doe.org"}}, false},
|
||||
{"fail key", fields{caURL, "", "ra@doe.org"}, args{"doe", []string{"doe.org"}}, true},
|
||||
{"fail no signer", fields{caURL, testIssPath, "ra@doe.org"}, args{"doe", []string{"doe.org"}}, true},
|
||||
{"ok", fields{caURL, "ra@doe.org", signer}, args{"doe", []string{"doe.org"}}, false},
|
||||
{"fail", fields{caURL, "ra@doe.org", &mockErrSigner{}}, args{"doe", []string{"doe.org"}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
i := &jwkIssuer{
|
||||
caURL: tt.fields.caURL,
|
||||
keyFile: tt.fields.keyFile,
|
||||
issuer: tt.fields.issuer,
|
||||
signer: tt.fields.signer,
|
||||
}
|
||||
got, err := i.SignToken(tt.args.subject, tt.args.sans)
|
||||
if (err != nil) != tt.wantErr {
|
||||
|
@ -78,11 +81,15 @@ func Test_jwkIssuer_RevokeToken(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
caURL *url.URL
|
||||
keyFile string
|
||||
issuer string
|
||||
signer jose.Signer
|
||||
}
|
||||
type args struct {
|
||||
subject string
|
||||
|
@ -98,16 +105,15 @@ func Test_jwkIssuer_RevokeToken(t *testing.T) {
|
|||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{caURL, testX5CKeyPath, "ra@smallstep.com"}, args{"doe"}, false},
|
||||
{"fail key", fields{caURL, "", "ra@smallstep.com"}, args{"doe"}, true},
|
||||
{"fail no signer", fields{caURL, testIssPath, "ra@smallstep.com"}, args{"doe"}, true},
|
||||
{"ok", fields{caURL, "ra@doe.org", signer}, args{"doe"}, false},
|
||||
{"ok", fields{caURL, "ra@doe.org", &mockErrSigner{}}, args{"doe"}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
i := &jwkIssuer{
|
||||
caURL: tt.fields.caURL,
|
||||
keyFile: tt.fields.keyFile,
|
||||
issuer: tt.fields.issuer,
|
||||
signer: tt.fields.signer,
|
||||
}
|
||||
got, err := i.RevokeToken(tt.args.subject)
|
||||
if (err != nil) != tt.wantErr {
|
||||
|
@ -140,11 +146,15 @@ func Test_jwkIssuer_Lifetime(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
caURL *url.URL
|
||||
keyFile string
|
||||
issuer string
|
||||
signer jose.Signer
|
||||
}
|
||||
type args struct {
|
||||
d time.Duration
|
||||
|
@ -155,14 +165,14 @@ func Test_jwkIssuer_Lifetime(t *testing.T) {
|
|||
args args
|
||||
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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
i := &jwkIssuer{
|
||||
caURL: tt.fields.caURL,
|
||||
keyFile: tt.fields.keyFile,
|
||||
issuer: tt.fields.issuer,
|
||||
signer: tt.fields.signer,
|
||||
}
|
||||
if got := i.Lifetime(tt.args.d); 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")
|
||||
}
|
||||
|
||||
// Create configured issuer
|
||||
iss, err := newStepIssuer(caURL, opts.CertificateIssuer)
|
||||
// Create client.
|
||||
client, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create client.
|
||||
client, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint))
|
||||
// Create configured issuer
|
||||
iss, err := newStepIssuer(caURL, client, opts.CertificateIssuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -21,8 +21,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/randutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
@ -42,6 +44,7 @@ var (
|
|||
testX5CKey crypto.Signer
|
||||
testX5CPath, testX5CKeyPath string
|
||||
testPassword, testEncryptedKeyPath string
|
||||
testKeyID, testEncryptedJWKKey string
|
||||
|
||||
testCR *x509.CertificateRequest
|
||||
testCrt *x509.Certificate
|
||||
|
@ -157,6 +160,27 @@ func testCAHelper(t *testing.T) (*url.URL, *ca.Client) {
|
|||
writeJSON(w, api.RevokeResponse{
|
||||
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:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
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 {
|
||||
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 != "" {
|
||||
key = testEncryptedKeyPath
|
||||
password = testPassword
|
||||
}
|
||||
jwk, err := newJWKIssuer(caURL, &apiv1.CertificateIssuer{
|
||||
jwk, err := newJWKIssuer(caURL, client, &apiv1.CertificateIssuer{
|
||||
Type: "jwk",
|
||||
Provisioner: "ra@doe.org",
|
||||
Key: key,
|
||||
|
@ -217,7 +245,7 @@ func testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jwk.password = givenPassword
|
||||
|
||||
return jwk
|
||||
}
|
||||
|
||||
|
@ -225,6 +253,7 @@ func TestMain(m *testing.M) {
|
|||
testRootCrt, testRootKey = mustSignCertificate("Test Root Certificate", nil, x509util.DefaultRootTemplate, nil, nil)
|
||||
testIssCrt, testIssKey = mustSignCertificate("Test Intermediate Certificate", nil, x509util.DefaultIntermediateTemplate, testRootCrt, testRootKey)
|
||||
testX5CCrt, testX5CKey = mustSignCertificate("Test X5C Certificate", nil, x509util.DefaultLeafTemplate, testIssCrt, testIssKey)
|
||||
testRootFingerprint = x509util.Fingerprint(testRootCrt)
|
||||
|
||||
// Final certificate.
|
||||
var err error
|
||||
|
@ -241,14 +270,27 @@ func TestMain(m *testing.M) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
// Password used to encrypto the key
|
||||
// Password used to encrypt the key.
|
||||
testPassword, err = randutil.Hex(32)
|
||||
if err != nil {
|
||||
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")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -301,6 +343,11 @@ func Test_init(t *testing.T) {
|
|||
|
||||
func TestNew(t *testing.T) {
|
||||
caURL, client := testCAHelper(t)
|
||||
signer, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
opts apiv1.Options
|
||||
|
@ -341,8 +388,25 @@ func TestNew(t *testing.T) {
|
|||
}}, &StepCAS{
|
||||
iss: &jwkIssuer{
|
||||
caURL: caURL,
|
||||
keyFile: testX5CKeyPath,
|
||||
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,
|
||||
fingerprint: testRootFingerprint,
|
||||
|
@ -396,6 +460,33 @@ func TestNew(t *testing.T) {
|
|||
Key: testX5CKeyPath,
|
||||
},
|
||||
}}, 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{
|
||||
CertificateAuthority: caURL.String(),
|
||||
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||
|
@ -496,9 +587,12 @@ func TestNew(t *testing.T) {
|
|||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
// We cannot compare client
|
||||
// We cannot compare neither the client nor the signer.
|
||||
if got != nil && tt.want != nil {
|
||||
got.client = tt.want.client
|
||||
if jwk, ok := got.iss.(*jwkIssuer); ok {
|
||||
jwk.signer = signer
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(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)
|
||||
jwkEnc := testJWKIssuer(t, caURL, testPassword)
|
||||
x5cBad := testX5CIssuer(t, caURL, "bad-password")
|
||||
jwkBad := testJWKIssuer(t, caURL, "bad-password")
|
||||
|
||||
type fields struct {
|
||||
iss stepIssuer
|
||||
|
@ -579,10 +672,6 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
|
|||
CSR: testCR,
|
||||
Lifetime: time.Hour,
|
||||
}}, nil, true},
|
||||
{"fail jwk password", fields{jwkBad, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
|
||||
CSR: testCR,
|
||||
Lifetime: time.Hour,
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -658,7 +747,6 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
|
|||
x5cEnc := testX5CIssuer(t, caURL, testPassword)
|
||||
jwkEnc := testJWKIssuer(t, caURL, testPassword)
|
||||
x5cBad := testX5CIssuer(t, caURL, "bad-password")
|
||||
jwkBad := testJWKIssuer(t, caURL, "bad-password")
|
||||
|
||||
type fields struct {
|
||||
iss stepIssuer
|
||||
|
@ -729,10 +817,6 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
|
|||
SerialNumber: "ok",
|
||||
Certificate: nil,
|
||||
}}, nil, true},
|
||||
{"fail jwk password", fields{jwkBad, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||
SerialNumber: "ok",
|
||||
Certificate: nil,
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -66,6 +66,7 @@ var appHelpTemplate = `## NAME
|
|||
| **{{join .Names ", "}}** | {{.Usage}} |{{end}}
|
||||
{{end}}{{if .VisibleFlags}}{{end}}
|
||||
## OPTIONS
|
||||
|
||||
{{range $index, $option := .VisibleFlags}}{{if $index}}
|
||||
{{end}}{{$option}}
|
||||
{{end}}{{end}}{{if .Copyright}}{{if len .Authors}}
|
||||
|
@ -106,7 +107,7 @@ func main() {
|
|||
app.HelpName = "step-ca"
|
||||
app.Version = config.Version()
|
||||
app.Usage = "an online certificate authority for secure automated certificate management"
|
||||
app.UsageText = `**step-ca** <config> [**--password-file**=<file>] [**--resolver**=<addr>] [**--help**] [**--version**]`
|
||||
app.UsageText = `**step-ca** <config> [**--password-file**=<file>] [**--issuer-password-file**=<file>] [**--resolver**=<addr>] [**--help**] [**--version**]`
|
||||
app.Description = `**step-ca** runs the Step Online Certificate Authority
|
||||
(Step CA) using the given configuration.
|
||||
See the README.md for more detailed configuration documentation.
|
||||
|
|
|
@ -22,13 +22,17 @@ var AppCommand = cli.Command{
|
|||
Name: "start",
|
||||
Action: appAction,
|
||||
UsageText: `**step-ca** <config>
|
||||
[**--password-file**=<file>]
|
||||
[**--resolver**=<addr>]`,
|
||||
[**--password-file**=<file>] [**--issuer-password-file**=<file>] [**--resolver**=<addr>]`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "password-file",
|
||||
Usage: `path to the <file> containing the password to decrypt the
|
||||
intermediate private key.`,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "issuer-password-file",
|
||||
Usage: `path to the <file> containing the password to decrypt the
|
||||
certificate issuer private key used in the RA mode.`,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "resolver",
|
||||
|
@ -40,6 +44,7 @@ intermediate private key.`,
|
|||
// AppAction is the action used when the top command runs.
|
||||
func appAction(ctx *cli.Context) error {
|
||||
passFile := ctx.String("password-file")
|
||||
issuerPassFile := ctx.String("issuer-password-file")
|
||||
resolver := ctx.String("resolver")
|
||||
|
||||
// If zero cmd line args show help, if >1 cmd line args show error.
|
||||
|
@ -64,6 +69,14 @@ func appAction(ctx *cli.Context) error {
|
|||
password = bytes.TrimRightFunc(password, unicode.IsSpace)
|
||||
}
|
||||
|
||||
var issuerPassword []byte
|
||||
if issuerPassFile != "" {
|
||||
if issuerPassword, err = ioutil.ReadFile(issuerPassFile); err != nil {
|
||||
fatal(errors.Wrapf(err, "error reading %s", issuerPassFile))
|
||||
}
|
||||
issuerPassword = bytes.TrimRightFunc(issuerPassword, unicode.IsSpace)
|
||||
}
|
||||
|
||||
// replace resolver if requested
|
||||
if resolver != "" {
|
||||
net.DefaultResolver.PreferGo = true
|
||||
|
@ -72,7 +85,10 @@ func appAction(ctx *cli.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
srv, err := ca.New(config, ca.WithConfigFile(configFile), ca.WithPassword(password))
|
||||
srv, err := ca.New(config,
|
||||
ca.WithConfigFile(configFile),
|
||||
ca.WithPassword(password),
|
||||
ca.WithIssuerPassword(issuerPassword))
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue