Merge pull request #516 from smallstep/ra-mode-improvements

RA mode improvements
This commit is contained in:
Mariano Cano 2021-03-25 11:58:13 -07:00 committed by GitHub
commit 5249ce794b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 354 additions and 89 deletions

View file

@ -23,9 +23,10 @@ import (
) )
type options struct { type options struct {
configFile string configFile string
password []byte password []byte
database db.AuthDB issuerPassword []byte
database db.AuthDB
} }
func (o *options) apply(opts []Option) { func (o *options) apply(opts []Option) {
@ -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. // WithDatabase sets the given authority database to the CA options.
func WithDatabase(db db.AuthDB) Option { func WithDatabase(db db.AuthDB) Option {
return func(o *options) { 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. // Init initializes the CA with the given configuration.
func (ca *CA) Init(config *authority.Config) (*CA, error) { 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) 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 var opts []authority.Option
if ca.opts.database != nil { if ca.opts.database != nil {
opts = append(opts, authority.WithDatabase(ca.opts.database)) opts = append(opts, authority.WithDatabase(ca.opts.database))
@ -213,6 +230,7 @@ func (ca *CA) Reload() error {
newCA, err := New(config, newCA, err := New(config,
WithPassword(ca.opts.password), WithPassword(ca.opts.password),
WithIssuerPassword(ca.opts.issuerPassword),
WithConfigFile(ca.opts.configFile), WithConfigFile(ca.opts.configFile),
WithDatabase(ca.auth.GetDatabase()), WithDatabase(ca.auth.GetDatabase()),
) )

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 set
// it will be prompted.
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) {

View file

@ -66,6 +66,7 @@ var appHelpTemplate = `## NAME
| **{{join .Names ", "}}** | {{.Usage}} |{{end}} | **{{join .Names ", "}}** | {{.Usage}} |{{end}}
{{end}}{{if .VisibleFlags}}{{end}} {{end}}{{if .VisibleFlags}}{{end}}
## OPTIONS ## OPTIONS
{{range $index, $option := .VisibleFlags}}{{if $index}} {{range $index, $option := .VisibleFlags}}{{if $index}}
{{end}}{{$option}} {{end}}{{$option}}
{{end}}{{end}}{{if .Copyright}}{{if len .Authors}} {{end}}{{end}}{{if .Copyright}}{{if len .Authors}}
@ -106,7 +107,7 @@ func main() {
app.HelpName = "step-ca" app.HelpName = "step-ca"
app.Version = config.Version() app.Version = config.Version()
app.Usage = "an online certificate authority for secure automated certificate management" 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 app.Description = `**step-ca** runs the Step Online Certificate Authority
(Step CA) using the given configuration. (Step CA) using the given configuration.
See the README.md for more detailed configuration documentation. See the README.md for more detailed configuration documentation.

View file

@ -22,13 +22,17 @@ var AppCommand = cli.Command{
Name: "start", Name: "start",
Action: appAction, Action: appAction,
UsageText: `**step-ca** <config> UsageText: `**step-ca** <config>
[**--password-file**=<file>] [**--password-file**=<file>] [**--issuer-password-file**=<file>] [**--resolver**=<addr>]`,
[**--resolver**=<addr>]`,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{
Name: "password-file", Name: "password-file",
Usage: `path to the <file> containing the password to decrypt the Usage: `path to the <file> containing the password to decrypt the
intermediate private key.`, 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{ cli.StringFlag{
Name: "resolver", Name: "resolver",
@ -40,6 +44,7 @@ intermediate private key.`,
// AppAction is the action used when the top command runs. // AppAction is the action used when the top command runs.
func appAction(ctx *cli.Context) error { func appAction(ctx *cli.Context) error {
passFile := ctx.String("password-file") passFile := ctx.String("password-file")
issuerPassFile := ctx.String("issuer-password-file")
resolver := ctx.String("resolver") resolver := ctx.String("resolver")
// If zero cmd line args show help, if >1 cmd line args show error. // 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) 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 // replace resolver if requested
if resolver != "" { if resolver != "" {
net.DefaultResolver.PreferGo = true 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 { if err != nil {
fatal(err) fatal(err)
} }