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"`
Certificate string `json:"crt,omitempty"`
Key string `json:"key,omitempty"`
Password string `json:"password,omitempty"`
}
// Validate checks the fields in Options.

View file

@ -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

View file

@ -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

View file

@ -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")
}