From edc7c4d90efe28cae382adb7dc5add1cffd1ba5f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 23 Mar 2021 17:54:42 -0700 Subject: [PATCH] Add support for password encrypted files --- cas/apiv1/options.go | 1 + cas/stepcas/jwk_issuer.go | 28 ++++----- cas/stepcas/stepcas_test.go | 116 ++++++++++++++++++++++++++++++------ cas/stepcas/x5c_issuer.go | 28 ++++++--- 4 files changed, 133 insertions(+), 40 deletions(-) diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 4e0b67bc..19169398 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -58,6 +58,7 @@ type CertificateIssuer struct { Provisioner string `json:"provisioner,omitempty"` Certificate string `json:"crt,omitempty"` Key string `json:"key,omitempty"` + Password string `json:"password,omitempty"` } // Validate checks the fields in Options. diff --git a/cas/stepcas/jwk_issuer.go b/cas/stepcas/jwk_issuer.go index 36b5f488..9a4cd7d1 100644 --- a/cas/stepcas/jwk_issuer.go +++ b/cas/stepcas/jwk_issuer.go @@ -1,33 +1,33 @@ package stepcas import ( - "crypto" "net/url" "time" "github.com/pkg/errors" "github.com/smallstep/certificates/cas/apiv1" "go.step.sm/crypto/jose" - "go.step.sm/crypto/pemutil" "go.step.sm/crypto/randutil" ) type jwkIssuer struct { - caURL *url.URL - keyFile string - issuer string + caURL *url.URL + issuer string + keyFile string + password string } func newJWKIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) { - _, err := newJWKSigner(cfg.Key) + _, err := newJWKSigner(cfg.Key, cfg.Password) if err != nil { return nil, err } return &jwkIssuer{ - caURL: caURL, - keyFile: cfg.Key, - issuer: cfg.Provisioner, + caURL: caURL, + issuer: cfg.Provisioner, + keyFile: cfg.Key, + password: cfg.Password, }, nil } @@ -50,7 +50,7 @@ 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) + signer, err := newJWKSigner(i.keyFile, i.password) if err != nil { return "", err } @@ -76,15 +76,11 @@ func (i *jwkIssuer) createToken(aud, sub string, sans []string) (string, error) return tok, nil } -func newJWKSigner(keyFile string) (jose.Signer, error) { - key, err := pemutil.Read(keyFile) +func newJWKSigner(keyFile, password string) (jose.Signer, error) { + signer, err := readKey(keyFile, password) if err != nil { return nil, err } - signer, ok := key.(crypto.Signer) - if !ok { - return nil, errors.New("key is not a crypto.Signer") - } kid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()}) if err != nil { return nil, err diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go index 3a25927f..88cbb227 100644 --- a/cas/stepcas/stepcas_test.go +++ b/cas/stepcas/stepcas_test.go @@ -23,6 +23,8 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/cas/apiv1" + "go.step.sm/crypto/pemutil" + "go.step.sm/crypto/randutil" "go.step.sm/crypto/x509util" ) @@ -36,9 +38,10 @@ var ( testIssKey crypto.Signer testIssPath, testIssKeyPath string - testX5CCrt *x509.Certificate - testX5CKey crypto.Signer - testX5CPath, testX5CKeyPath string + testX5CCrt *x509.Certificate + testX5CKey crypto.Signer + testX5CPath, testX5CKeyPath string + testPassword, testEncryptedKeyPath string testCR *x509.CertificateRequest testCrt *x509.Certificate @@ -104,6 +107,16 @@ func mustSerializeKey(filename string, key crypto.Signer) { } } +func mustEncryptKey(filename string, key crypto.Signer) { + _, err := pemutil.Serialize(key, + pemutil.ToFile(filename, 0600), + pemutil.WithPKCS8(true), + pemutil.WithPassword([]byte(testPassword))) + if err != nil { + panic(err) + } +} + func testCAHelper(t *testing.T) (*url.URL, *ca.Client) { t.Helper() @@ -167,31 +180,45 @@ func testCAHelper(t *testing.T) (*url.URL, *ca.Client) { return u, client } -func testX5CIssuer(t *testing.T, caURL *url.URL) *x5cIssuer { +func testX5CIssuer(t *testing.T, caURL *url.URL, password string) *x5cIssuer { t.Helper() + key, givenPassword := testX5CKeyPath, password + if password != "" { + key = testEncryptedKeyPath + password = testPassword + } x5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ Type: "x5c", Provisioner: "X5C", Certificate: testX5CPath, - Key: testX5CKeyPath, + Key: key, + Password: password, }) if err != nil { t.Fatal(err) } + x5c.password = givenPassword return x5c } -func testJWKIssuer(t *testing.T, caURL *url.URL) *jwkIssuer { +func testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer { t.Helper() - x5c, err := newJWKIssuer(caURL, &apiv1.CertificateIssuer{ + key, givenPassword := testX5CKeyPath, password + if password != "" { + key = testEncryptedKeyPath + password = testPassword + } + jwk, err := newJWKIssuer(caURL, &apiv1.CertificateIssuer{ Type: "jwk", Provisioner: "ra@doe.org", - Key: testX5CKeyPath, + Key: key, + Password: password, }) if err != nil { t.Fatal(err) } - return x5c + jwk.password = givenPassword + return jwk } func TestMain(m *testing.M) { @@ -214,6 +241,12 @@ func TestMain(m *testing.M) { panic(err) } + // Password used to encrypto the key + testPassword, err = randutil.Hex(32) + if err != nil { + panic(err) + } + testRootFingerprint = x509util.Fingerprint(testRootCrt) path, err := ioutil.TempDir(os.TempDir(), "stepcas") @@ -236,6 +269,9 @@ func TestMain(m *testing.M) { mustSerializeCrt(testX5CPath, testX5CCrt, testIssCrt) mustSerializeKey(testX5CKeyPath, testX5CKey) + testEncryptedKeyPath = filepath.Join(path, "x5c.enc.key") + mustEncryptKey(testEncryptedKeyPath, testX5CKey) + code := m.Run() if err := os.RemoveAll(path); err != nil { panic(err) @@ -473,8 +509,12 @@ func TestNew(t *testing.T) { func TestStepCAS_CreateCertificate(t *testing.T) { caURL, client := testCAHelper(t) - x5c := testX5CIssuer(t, caURL) - jwk := testJWKIssuer(t, caURL) + x5c := testX5CIssuer(t, caURL, "") + jwk := testJWKIssuer(t, caURL, "") + 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 @@ -498,6 +538,13 @@ func TestStepCAS_CreateCertificate(t *testing.T) { Certificate: testCrt, CertificateChain: []*x509.Certificate{testIssCrt}, }, false}, + {"ok with password", fields{x5cEnc, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ + CSR: testCR, + Lifetime: time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: testCrt, + CertificateChain: []*x509.Certificate{testIssCrt}, + }, false}, {"ok jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ CSR: testCR, Lifetime: time.Hour, @@ -505,6 +552,13 @@ func TestStepCAS_CreateCertificate(t *testing.T) { Certificate: testCrt, CertificateChain: []*x509.Certificate{testIssCrt}, }, false}, + {"ok jwk with password", fields{jwkEnc, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ + CSR: testCR, + Lifetime: time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: testCrt, + CertificateChain: []*x509.Certificate{testIssCrt}, + }, false}, {"fail CSR", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ CSR: nil, Lifetime: time.Hour, @@ -521,6 +575,14 @@ func TestStepCAS_CreateCertificate(t *testing.T) { CSR: testFailCR, Lifetime: time.Hour, }}, nil, true}, + {"fail password", fields{x5cBad, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ + 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) { @@ -543,8 +605,8 @@ func TestStepCAS_CreateCertificate(t *testing.T) { func TestStepCAS_RenewCertificate(t *testing.T) { caURL, client := testCAHelper(t) - x5c := testX5CIssuer(t, caURL) - jwk := testJWKIssuer(t, caURL) + x5c := testX5CIssuer(t, caURL, "") + jwk := testJWKIssuer(t, caURL, "") type fields struct { iss stepIssuer @@ -591,8 +653,12 @@ func TestStepCAS_RenewCertificate(t *testing.T) { func TestStepCAS_RevokeCertificate(t *testing.T) { caURL, client := testCAHelper(t) - x5c := testX5CIssuer(t, caURL) - jwk := testJWKIssuer(t, caURL) + x5c := testX5CIssuer(t, caURL, "") + jwk := testJWKIssuer(t, caURL, "") + 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 @@ -625,6 +691,10 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { }}, &apiv1.RevokeCertificateResponse{ Certificate: testCrt, }, false}, + {"ok with password", fields{x5cEnc, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "ok", + Certificate: nil, + }}, &apiv1.RevokeCertificateResponse{}, false}, {"ok serial number jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ SerialNumber: "ok", Certificate: nil, @@ -641,6 +711,10 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { }}, &apiv1.RevokeCertificateResponse{ Certificate: testCrt, }, false}, + {"ok jwk with password", fields{jwkEnc, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "ok", + Certificate: nil, + }}, &apiv1.RevokeCertificateResponse{}, false}, {"fail request", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ SerialNumber: "", Certificate: nil, @@ -651,6 +725,14 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { {"fail client revoke", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ SerialNumber: "fail", }}, nil, true}, + {"fail password", fields{x5cBad, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + 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) { @@ -673,8 +755,8 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { func TestStepCAS_GetCertificateAuthority(t *testing.T) { caURL, client := testCAHelper(t) - x5c := testX5CIssuer(t, caURL) - jwk := testJWKIssuer(t, caURL) + x5c := testX5CIssuer(t, caURL, "") + jwk := testJWKIssuer(t, caURL, "") type fields struct { iss stepIssuer diff --git a/cas/stepcas/x5c_issuer.go b/cas/stepcas/x5c_issuer.go index d5d9a0f0..da4aa27e 100644 --- a/cas/stepcas/x5c_issuer.go +++ b/cas/stepcas/x5c_issuer.go @@ -25,24 +25,26 @@ var timeNow = func() time.Time { type x5cIssuer struct { caURL *url.URL + issuer string certFile string keyFile string - issuer string + password string } // newX5CIssuer create a new x5c token issuer. The given configuration should be // already validate. func newX5CIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*x5cIssuer, error) { - _, err := newX5CSigner(cfg.Certificate, cfg.Key) + _, err := newX5CSigner(cfg.Certificate, cfg.Key, cfg.Password) if err != nil { return nil, err } return &x5cIssuer{ caURL: caURL, + issuer: cfg.Provisioner, certFile: cfg.Certificate, keyFile: cfg.Key, - issuer: cfg.Provisioner, + password: cfg.Password, }, nil } @@ -77,7 +79,7 @@ func (i *x5cIssuer) Lifetime(d time.Duration) time.Duration { } func (i *x5cIssuer) createToken(aud, sub string, sans []string) (string, error) { - signer, err := newX5CSigner(i.certFile, i.keyFile) + signer, err := newX5CSigner(i.certFile, i.keyFile, i.password) if err != nil { return "", err } @@ -116,8 +118,12 @@ func defaultClaims(iss, sub, aud, id string) jose.Claims { } } -func newX5CSigner(certFile, keyFile string) (jose.Signer, error) { - key, err := pemutil.Read(keyFile) +func readKey(keyFile, password string) (crypto.Signer, error) { + var opts []pemutil.Options + if password != "" { + opts = append(opts, pemutil.WithPassword([]byte(password))) + } + key, err := pemutil.Read(keyFile, opts...) if err != nil { return nil, err } @@ -125,11 +131,19 @@ func newX5CSigner(certFile, keyFile string) (jose.Signer, error) { if !ok { return nil, errors.New("key is not a crypto.Signer") } + return signer, nil +} + +func newX5CSigner(certFile, keyFile, password string) (jose.Signer, error) { + signer, err := readKey(keyFile, password) + if err != nil { + return nil, err + } kid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()}) if err != nil { return nil, err } - certs, err := jose.ValidateX5C(certFile, key) + certs, err := jose.ValidateX5C(certFile, signer) if err != nil { return nil, errors.Wrap(err, "error validating x5c certificate chain and key") }