Add support for password encrypted files

This commit is contained in:
Mariano Cano 2021-03-23 17:54:42 -07:00
parent 80542d6d9a
commit edc7c4d90e
4 changed files with 133 additions and 40 deletions

View file

@ -58,6 +58,7 @@ type CertificateIssuer struct {
Provisioner string `json:"provisioner,omitempty"` Provisioner string `json:"provisioner,omitempty"`
Certificate string `json:"crt,omitempty"` Certificate string `json:"crt,omitempty"`
Key string `json:"key,omitempty"` Key string `json:"key,omitempty"`
Password string `json:"password,omitempty"`
} }
// Validate checks the fields in Options. // Validate checks the fields in Options.

View file

@ -1,33 +1,33 @@
package stepcas package stepcas
import ( import (
"crypto"
"net/url" "net/url"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/certificates/cas/apiv1" "github.com/smallstep/certificates/cas/apiv1"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/randutil" "go.step.sm/crypto/randutil"
) )
type jwkIssuer struct { type jwkIssuer struct {
caURL *url.URL caURL *url.URL
keyFile string
issuer string issuer string
keyFile string
password string
} }
func newJWKIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) { func newJWKIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) {
_, err := newJWKSigner(cfg.Key) _, err := newJWKSigner(cfg.Key, cfg.Password)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &jwkIssuer{ return &jwkIssuer{
caURL: caURL, caURL: caURL,
keyFile: cfg.Key,
issuer: cfg.Provisioner, issuer: cfg.Provisioner,
keyFile: cfg.Key,
password: cfg.Password,
}, nil }, 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) { 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 { if err != nil {
return "", err return "", err
} }
@ -76,15 +76,11 @@ func (i *jwkIssuer) createToken(aud, sub string, sans []string) (string, error)
return tok, nil return tok, nil
} }
func newJWKSigner(keyFile string) (jose.Signer, error) { func newJWKSigner(keyFile, password string) (jose.Signer, error) {
key, err := pemutil.Read(keyFile) signer, err := readKey(keyFile, password)
if err != nil { if err != nil {
return nil, err 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()}) kid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()})
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -23,6 +23,8 @@ import (
"github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api"
"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/pemutil"
"go.step.sm/crypto/randutil"
"go.step.sm/crypto/x509util" "go.step.sm/crypto/x509util"
) )
@ -39,6 +41,7 @@ var (
testX5CCrt *x509.Certificate testX5CCrt *x509.Certificate
testX5CKey crypto.Signer testX5CKey crypto.Signer
testX5CPath, testX5CKeyPath string testX5CPath, testX5CKeyPath string
testPassword, testEncryptedKeyPath string
testCR *x509.CertificateRequest testCR *x509.CertificateRequest
testCrt *x509.Certificate 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) { func testCAHelper(t *testing.T) (*url.URL, *ca.Client) {
t.Helper() t.Helper()
@ -167,31 +180,45 @@ func testCAHelper(t *testing.T) (*url.URL, *ca.Client) {
return u, 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() t.Helper()
key, givenPassword := testX5CKeyPath, password
if password != "" {
key = testEncryptedKeyPath
password = testPassword
}
x5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ x5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{
Type: "x5c", Type: "x5c",
Provisioner: "X5C", Provisioner: "X5C",
Certificate: testX5CPath, Certificate: testX5CPath,
Key: testX5CKeyPath, Key: key,
Password: password,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
x5c.password = givenPassword
return x5c return x5c
} }
func testJWKIssuer(t *testing.T, caURL *url.URL) *jwkIssuer { func testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer {
t.Helper() 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", Type: "jwk",
Provisioner: "ra@doe.org", Provisioner: "ra@doe.org",
Key: testX5CKeyPath, Key: key,
Password: password,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return x5c jwk.password = givenPassword
return jwk
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -214,6 +241,12 @@ func TestMain(m *testing.M) {
panic(err) panic(err)
} }
// Password used to encrypto the key
testPassword, err = randutil.Hex(32)
if err != nil {
panic(err)
}
testRootFingerprint = x509util.Fingerprint(testRootCrt) testRootFingerprint = x509util.Fingerprint(testRootCrt)
path, err := ioutil.TempDir(os.TempDir(), "stepcas") path, err := ioutil.TempDir(os.TempDir(), "stepcas")
@ -236,6 +269,9 @@ func TestMain(m *testing.M) {
mustSerializeCrt(testX5CPath, testX5CCrt, testIssCrt) mustSerializeCrt(testX5CPath, testX5CCrt, testIssCrt)
mustSerializeKey(testX5CKeyPath, testX5CKey) mustSerializeKey(testX5CKeyPath, testX5CKey)
testEncryptedKeyPath = filepath.Join(path, "x5c.enc.key")
mustEncryptKey(testEncryptedKeyPath, testX5CKey)
code := m.Run() code := m.Run()
if err := os.RemoveAll(path); err != nil { if err := os.RemoveAll(path); err != nil {
panic(err) panic(err)
@ -473,8 +509,12 @@ func TestNew(t *testing.T) {
func TestStepCAS_CreateCertificate(t *testing.T) { func TestStepCAS_CreateCertificate(t *testing.T) {
caURL, client := testCAHelper(t) caURL, client := testCAHelper(t)
x5c := testX5CIssuer(t, caURL) x5c := testX5CIssuer(t, caURL, "")
jwk := testJWKIssuer(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 { type fields struct {
iss stepIssuer iss stepIssuer
@ -498,6 +538,13 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
Certificate: testCrt, Certificate: testCrt,
CertificateChain: []*x509.Certificate{testIssCrt}, CertificateChain: []*x509.Certificate{testIssCrt},
}, false}, }, 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{ {"ok jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: testCR, CSR: testCR,
Lifetime: time.Hour, Lifetime: time.Hour,
@ -505,6 +552,13 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
Certificate: testCrt, Certificate: testCrt,
CertificateChain: []*x509.Certificate{testIssCrt}, CertificateChain: []*x509.Certificate{testIssCrt},
}, false}, }, 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{ {"fail CSR", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
CSR: nil, CSR: nil,
Lifetime: time.Hour, Lifetime: time.Hour,
@ -521,6 +575,14 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
CSR: testFailCR, CSR: testFailCR,
Lifetime: time.Hour, Lifetime: time.Hour,
}}, nil, true}, }}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -543,8 +605,8 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
func TestStepCAS_RenewCertificate(t *testing.T) { func TestStepCAS_RenewCertificate(t *testing.T) {
caURL, client := testCAHelper(t) caURL, client := testCAHelper(t)
x5c := testX5CIssuer(t, caURL) x5c := testX5CIssuer(t, caURL, "")
jwk := testJWKIssuer(t, caURL) jwk := testJWKIssuer(t, caURL, "")
type fields struct { type fields struct {
iss stepIssuer iss stepIssuer
@ -591,8 +653,12 @@ func TestStepCAS_RenewCertificate(t *testing.T) {
func TestStepCAS_RevokeCertificate(t *testing.T) { func TestStepCAS_RevokeCertificate(t *testing.T) {
caURL, client := testCAHelper(t) caURL, client := testCAHelper(t)
x5c := testX5CIssuer(t, caURL) x5c := testX5CIssuer(t, caURL, "")
jwk := testJWKIssuer(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 { type fields struct {
iss stepIssuer iss stepIssuer
@ -625,6 +691,10 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
}}, &apiv1.RevokeCertificateResponse{ }}, &apiv1.RevokeCertificateResponse{
Certificate: testCrt, Certificate: testCrt,
}, false}, }, 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{ {"ok serial number jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
SerialNumber: "ok", SerialNumber: "ok",
Certificate: nil, Certificate: nil,
@ -641,6 +711,10 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
}}, &apiv1.RevokeCertificateResponse{ }}, &apiv1.RevokeCertificateResponse{
Certificate: testCrt, Certificate: testCrt,
}, false}, }, 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{ {"fail request", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
SerialNumber: "", SerialNumber: "",
Certificate: nil, Certificate: nil,
@ -651,6 +725,14 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
{"fail client revoke", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ {"fail client revoke", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
SerialNumber: "fail", SerialNumber: "fail",
}}, nil, true}, }}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -673,8 +755,8 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
func TestStepCAS_GetCertificateAuthority(t *testing.T) { func TestStepCAS_GetCertificateAuthority(t *testing.T) {
caURL, client := testCAHelper(t) caURL, client := testCAHelper(t)
x5c := testX5CIssuer(t, caURL) x5c := testX5CIssuer(t, caURL, "")
jwk := testJWKIssuer(t, caURL) jwk := testJWKIssuer(t, caURL, "")
type fields struct { type fields struct {
iss stepIssuer iss stepIssuer

View file

@ -25,24 +25,26 @@ var timeNow = func() time.Time {
type x5cIssuer struct { type x5cIssuer struct {
caURL *url.URL caURL *url.URL
issuer string
certFile string certFile string
keyFile string keyFile string
issuer string password string
} }
// newX5CIssuer create a new x5c token issuer. The given configuration should be // newX5CIssuer create a new x5c token issuer. The given configuration should be
// already validate. // already validate.
func newX5CIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*x5cIssuer, error) { 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 { if err != nil {
return nil, err return nil, err
} }
return &x5cIssuer{ return &x5cIssuer{
caURL: caURL, caURL: caURL,
issuer: cfg.Provisioner,
certFile: cfg.Certificate, certFile: cfg.Certificate,
keyFile: cfg.Key, keyFile: cfg.Key,
issuer: cfg.Provisioner, password: cfg.Password,
}, nil }, 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) { 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 { if err != nil {
return "", err return "", err
} }
@ -116,8 +118,12 @@ func defaultClaims(iss, sub, aud, id string) jose.Claims {
} }
} }
func newX5CSigner(certFile, keyFile string) (jose.Signer, error) { func readKey(keyFile, password string) (crypto.Signer, error) {
key, err := pemutil.Read(keyFile) var opts []pemutil.Options
if password != "" {
opts = append(opts, pemutil.WithPassword([]byte(password)))
}
key, err := pemutil.Read(keyFile, opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -125,11 +131,19 @@ func newX5CSigner(certFile, keyFile string) (jose.Signer, error) {
if !ok { if !ok {
return nil, errors.New("key is not a crypto.Signer") 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()}) kid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()})
if err != nil { if err != nil {
return nil, err return nil, err
} }
certs, err := jose.ValidateX5C(certFile, key) certs, err := jose.ValidateX5C(certFile, signer)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "error validating x5c certificate chain and key") return nil, errors.Wrap(err, "error validating x5c certificate chain and key")
} }