diff --git a/ca/ca.go b/ca/ca.go index 5ba81e9e..c4e79268 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -23,9 +23,10 @@ import ( ) type options struct { - configFile string - password []byte - database db.AuthDB + configFile string + password []byte + issuerPassword []byte + database db.AuthDB } 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. 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()), ) diff --git a/cas/stepcas/issuer.go b/cas/stepcas/issuer.go index 9289709e..be395e33 100644 --- a/cas/stepcas/issuer.go +++ b/cas/stepcas/issuer.go @@ -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: diff --git a/cas/stepcas/issuer_test.go b/cas/stepcas/issuer_test.go index 743d6eec..6fffd729 100644 --- a/cas/stepcas/issuer_test.go +++ b/cas/stepcas/issuer_test.go @@ -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,15 +25,27 @@ 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 - iss *apiv1.CertificateIssuer + caURL *url.URL + client *ca.Client + iss *apiv1.CertificateIssuer } tests := []struct { name string @@ -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", + caURL: caURL, + 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) } diff --git a/cas/stepcas/jwk_issuer.go b/cas/stepcas/jwk_issuer.go index 9a4cd7d1..db45ef48 100644 --- a/cas/stepcas/jwk_issuer.go +++ b/cas/stepcas/jwk_issuer.go @@ -1,33 +1,55 @@ 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" ) type jwkIssuer struct { - caURL *url.URL - issuer string - keyFile string - password string + caURL *url.URL + issuer string + signer jose.Signer } -func newJWKIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) { - _, err := newJWKSigner(cfg.Key, cfg.Password) - if err != nil { - return nil, err +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, + caURL: caURL, + issuer: cfg.Provisioner, + 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 + } +} diff --git a/cas/stepcas/jwk_issuer_test.go b/cas/stepcas/jwk_issuer_test.go index 2caccf25..7ebfcb3f 100644 --- a/cas/stepcas/jwk_issuer_test.go +++ b/cas/stepcas/jwk_issuer_test.go @@ -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 + caURL *url.URL + 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, + caURL: tt.fields.caURL, + 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 + caURL *url.URL + 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, + caURL: tt.fields.caURL, + 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 + caURL *url.URL + 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, + caURL: tt.fields.caURL, + 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) + } + }) + } +} diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 7dc01e5a..49a99963 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -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 } diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go index 88cbb227..fb8259f5 100644 --- a/cas/stepcas/stepcas_test.go +++ b/cas/stepcas/stepcas_test.go @@ -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 @@ -340,9 +387,26 @@ func TestNew(t *testing.T) { }, }}, &StepCAS{ iss: &jwkIssuer{ - caURL: caURL, - keyFile: testX5CKeyPath, - issuer: "ra@doe.org", + caURL: caURL, + 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) { diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index a243022a..4396e028 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -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** [**--password-file**=] [**--resolver**=] [**--help**] [**--version**]` + app.UsageText = `**step-ca** [**--password-file**=] [**--issuer-password-file**=] [**--resolver**=] [**--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. diff --git a/commands/app.go b/commands/app.go index 55e88abe..aff9d473 100644 --- a/commands/app.go +++ b/commands/app.go @@ -22,13 +22,17 @@ var AppCommand = cli.Command{ Name: "start", Action: appAction, UsageText: `**step-ca** - [**--password-file**=] - [**--resolver**=]`, +[**--password-file**=] [**--issuer-password-file**=] [**--resolver**=]`, Flags: []cli.Flag{ cli.StringFlag{ Name: "password-file", Usage: `path to the containing the password to decrypt the intermediate private key.`, + }, + cli.StringFlag{ + Name: "issuer-password-file", + Usage: `path to the 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) }