forked from TrueCloudLab/certificates
Add support for password encrypted files
This commit is contained in:
parent
80542d6d9a
commit
edc7c4d90e
4 changed files with 133 additions and 40 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
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,
|
||||
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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
@ -39,6 +41,7 @@ var (
|
|||
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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue