forked from TrueCloudLab/certificates
Add JWK as an issuer for stepcas.
This commit is contained in:
parent
ce3e6bfdf6
commit
80542d6d9a
6 changed files with 517 additions and 78 deletions
|
@ -1,12 +1,36 @@
|
||||||
package stepcas
|
package stepcas
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/cas/apiv1"
|
"github.com/smallstep/certificates/cas/apiv1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type stepIssuer interface {
|
||||||
|
SignToken(subject string, sans []string) (string, error)
|
||||||
|
RevokeToken(subject string) (string, error)
|
||||||
|
Lifetime(d time.Duration) time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStepIssuer returns the configured step issuer.
|
||||||
|
func newStepIssuer(caURL *url.URL, iss *apiv1.CertificateIssuer) (stepIssuer, error) {
|
||||||
|
if err := validateCertificateIssuer(iss); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(iss.Type) {
|
||||||
|
case "x5c":
|
||||||
|
return newX5CIssuer(caURL, iss)
|
||||||
|
case "jwk":
|
||||||
|
return newJWKIssuer(caURL, iss)
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("stepCAS `certificateIssuer.type` %s is not supported", iss.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// validateCertificateIssuer validates the configuration of the certificate
|
// validateCertificateIssuer validates the configuration of the certificate
|
||||||
// issuer.
|
// issuer.
|
||||||
func validateCertificateIssuer(iss *apiv1.CertificateIssuer) error {
|
func validateCertificateIssuer(iss *apiv1.CertificateIssuer) error {
|
||||||
|
@ -20,6 +44,8 @@ func validateCertificateIssuer(iss *apiv1.CertificateIssuer) error {
|
||||||
switch strings.ToLower(iss.Type) {
|
switch strings.ToLower(iss.Type) {
|
||||||
case "x5c":
|
case "x5c":
|
||||||
return validateX5CIssuer(iss)
|
return validateX5CIssuer(iss)
|
||||||
|
case "jwk":
|
||||||
|
return validateJWKIssuer(iss)
|
||||||
default:
|
default:
|
||||||
return errors.Errorf("stepCAS `certificateIssuer.type` %s is not supported", iss.Type)
|
return errors.Errorf("stepCAS `certificateIssuer.type` %s is not supported", iss.Type)
|
||||||
}
|
}
|
||||||
|
@ -38,3 +64,15 @@ func validateX5CIssuer(iss *apiv1.CertificateIssuer) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateJWKIssuer validates the configuration of jwk issuer.
|
||||||
|
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:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
80
cas/stepcas/issuer_test.go
Normal file
80
cas/stepcas/issuer_test.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package stepcas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/cas/apiv1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockErrIssuer struct{}
|
||||||
|
|
||||||
|
func (m mockErrIssuer) SignToken(subject string, sans []string) (string, error) {
|
||||||
|
return "", apiv1.ErrNotImplemented{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockErrIssuer) RevokeToken(subject string) (string, error) {
|
||||||
|
return "", apiv1.ErrNotImplemented{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockErrIssuer) Lifetime(d time.Duration) time.Duration {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newStepIssuer(t *testing.T) {
|
||||||
|
caURL, err := url.Parse("https://ca.smallstep.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
caURL *url.URL
|
||||||
|
iss *apiv1.CertificateIssuer
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want stepIssuer
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"x5c", args{caURL, &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
}}, &x5cIssuer{
|
||||||
|
caURL: caURL,
|
||||||
|
certFile: testX5CPath,
|
||||||
|
keyFile: testX5CKeyPath,
|
||||||
|
issuer: "X5C",
|
||||||
|
}, false},
|
||||||
|
{"jwk", args{caURL, &apiv1.CertificateIssuer{
|
||||||
|
Type: "jwk",
|
||||||
|
Provisioner: "ra@doe.org",
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
}}, &jwkIssuer{
|
||||||
|
caURL: caURL,
|
||||||
|
keyFile: testX5CKeyPath,
|
||||||
|
issuer: "ra@doe.org",
|
||||||
|
}, false},
|
||||||
|
{"fail", args{caURL, &apiv1.CertificateIssuer{
|
||||||
|
Type: "unknown",
|
||||||
|
Provisioner: "ra@doe.org",
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
}}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := newStepIssuer(tt.args.caURL, tt.args.iss)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("newStepIssuer() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("newStepIssuer() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
96
cas/stepcas/jwk_issuer.go
Normal file
96
cas/stepcas/jwk_issuer.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJWKIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) {
|
||||||
|
_, err := newJWKSigner(cfg.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &jwkIssuer{
|
||||||
|
caURL: caURL,
|
||||||
|
keyFile: cfg.Key,
|
||||||
|
issuer: cfg.Provisioner,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *jwkIssuer) SignToken(subject string, sans []string) (string, error) {
|
||||||
|
aud := i.caURL.ResolveReference(&url.URL{
|
||||||
|
Path: "/1.0/sign",
|
||||||
|
}).String()
|
||||||
|
return i.createToken(aud, subject, sans)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *jwkIssuer) RevokeToken(subject string) (string, error) {
|
||||||
|
aud := i.caURL.ResolveReference(&url.URL{
|
||||||
|
Path: "/1.0/revoke",
|
||||||
|
}).String()
|
||||||
|
return i.createToken(aud, subject, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *jwkIssuer) Lifetime(d time.Duration) time.Duration {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *jwkIssuer) createToken(aud, sub string, sans []string) (string, error) {
|
||||||
|
signer, err := newJWKSigner(i.keyFile)
|
||||||
|
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)
|
||||||
|
if len(sans) > 0 {
|
||||||
|
builder = builder.Claims(map[string]interface{}{
|
||||||
|
"sans": sans,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err := builder.CompactSerialize()
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "error signing token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJWKSigner(keyFile string) (jose.Signer, error) {
|
||||||
|
key, err := pemutil.Read(keyFile)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
so := new(jose.SignerOptions)
|
||||||
|
so.WithType("JWT")
|
||||||
|
so.WithHeader("kid", kid)
|
||||||
|
return newJoseSigner(signer, so)
|
||||||
|
}
|
172
cas/stepcas/jwk_issuer_test.go
Normal file
172
cas/stepcas/jwk_issuer_test.go
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
package stepcas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_jwkIssuer_SignToken(t *testing.T) {
|
||||||
|
caURL, err := url.Parse("https://ca.smallstep.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
caURL *url.URL
|
||||||
|
keyFile string
|
||||||
|
issuer string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
subject string
|
||||||
|
sans []string
|
||||||
|
}
|
||||||
|
type claims struct {
|
||||||
|
Aud []string `json:"aud"`
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Sans []string `json:"sans"`
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
got, err := i.SignToken(tt.args.subject, tt.args.sans)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("jwkIssuer.SignToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tt.wantErr {
|
||||||
|
jwt, err := jose.ParseSigned(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("jose.ParseSigned() error = %v", err)
|
||||||
|
}
|
||||||
|
var c claims
|
||||||
|
want := claims{
|
||||||
|
Aud: []string{tt.fields.caURL.String() + "/1.0/sign"},
|
||||||
|
Sub: tt.args.subject,
|
||||||
|
Sans: tt.args.sans,
|
||||||
|
}
|
||||||
|
if err := jwt.Claims(testX5CKey.Public(), &c); err != nil {
|
||||||
|
t.Errorf("jwt.Claims() error = %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(c, want) {
|
||||||
|
t.Errorf("jwt.Claims() claims = %#v, want %#v", c, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_jwkIssuer_RevokeToken(t *testing.T) {
|
||||||
|
caURL, err := url.Parse("https://ca.smallstep.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
caURL *url.URL
|
||||||
|
keyFile string
|
||||||
|
issuer string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
subject string
|
||||||
|
}
|
||||||
|
type claims struct {
|
||||||
|
Aud []string `json:"aud"`
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Sans []string `json:"sans"`
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
got, err := i.RevokeToken(tt.args.subject)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("jwkIssuer.RevokeToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tt.wantErr {
|
||||||
|
jwt, err := jose.ParseSigned(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("jose.ParseSigned() error = %v", err)
|
||||||
|
}
|
||||||
|
var c claims
|
||||||
|
want := claims{
|
||||||
|
Aud: []string{tt.fields.caURL.String() + "/1.0/revoke"},
|
||||||
|
Sub: tt.args.subject,
|
||||||
|
}
|
||||||
|
if err := jwt.Claims(testX5CKey.Public(), &c); err != nil {
|
||||||
|
t.Errorf("jwt.Claims() error = %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(c, want) {
|
||||||
|
t.Errorf("jwt.Claims() claims = %#v, want %#v", c, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_jwkIssuer_Lifetime(t *testing.T) {
|
||||||
|
caURL, err := url.Parse("https://ca.smallstep.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
caURL *url.URL
|
||||||
|
keyFile string
|
||||||
|
issuer string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
d time.Duration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want time.Duration
|
||||||
|
}{
|
||||||
|
{"ok", fields{caURL, testX5CKeyPath, "ra@smallstep.com"}, 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,
|
||||||
|
}
|
||||||
|
if got := i.Lifetime(tt.args.d); got != tt.want {
|
||||||
|
t.Errorf("jwkIssuer.Lifetime() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ func init() {
|
||||||
// StepCAS implements the cas.CertificateAuthorityService interface using
|
// StepCAS implements the cas.CertificateAuthorityService interface using
|
||||||
// another step-ca instance.
|
// another step-ca instance.
|
||||||
type StepCAS struct {
|
type StepCAS struct {
|
||||||
x5c *x5cIssuer
|
iss stepIssuer
|
||||||
client *ca.Client
|
client *ca.Client
|
||||||
fingerprint string
|
fingerprint string
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,10 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "stepCAS `certificateAuthority` is not valid")
|
return nil, errors.Wrap(err, "stepCAS `certificateAuthority` is not valid")
|
||||||
}
|
}
|
||||||
if err := validateCertificateIssuer(opts.CertificateIssuer); err != nil {
|
|
||||||
|
// Create configured issuer
|
||||||
|
iss, err := newStepIssuer(caURL, opts.CertificateIssuer)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,14 +53,8 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// X5C is the only one supported at the moment.
|
|
||||||
x5c, err := newX5CIssuer(caURL, opts.CertificateIssuer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &StepCAS{
|
return &StepCAS{
|
||||||
x5c: x5c,
|
iss: iss,
|
||||||
client: client,
|
client: client,
|
||||||
fingerprint: opts.CertificateAuthorityFingerprint,
|
fingerprint: opts.CertificateAuthorityFingerprint,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -101,7 +98,7 @@ func (s *StepCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1
|
||||||
serialNumber = req.Certificate.SerialNumber.String()
|
serialNumber = req.Certificate.SerialNumber.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := s.revokeToken(serialNumber)
|
token, err := s.iss.RevokeToken(serialNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -151,7 +148,7 @@ func (s *StepCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.D
|
||||||
commonName = sans[0]
|
commonName = sans[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := s.signToken(commonName, sans)
|
token, err := s.iss.SignToken(commonName, sans)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -174,27 +171,8 @@ func (s *StepCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.D
|
||||||
return cert, chain, nil
|
return cert, chain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepCAS) signToken(subject string, sans []string) (string, error) {
|
|
||||||
if s.x5c != nil {
|
|
||||||
return s.x5c.SignToken(subject, sans)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("stepCAS does not have any provisioner configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepCAS) revokeToken(subject string) (string, error) {
|
|
||||||
if s.x5c != nil {
|
|
||||||
return s.x5c.RevokeToken(subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("stepCAS does not have any provisioner configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StepCAS) lifetime(d time.Duration) api.TimeDuration {
|
func (s *StepCAS) lifetime(d time.Duration) api.TimeDuration {
|
||||||
if s.x5c != nil {
|
|
||||||
d = s.x5c.Lifetime(d)
|
|
||||||
}
|
|
||||||
var td api.TimeDuration
|
var td api.TimeDuration
|
||||||
td.SetDuration(d)
|
td.SetDuration(s.iss.Lifetime(d))
|
||||||
return td
|
return td
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,6 +167,33 @@ func testCAHelper(t *testing.T) (*url.URL, *ca.Client) {
|
||||||
return u, client
|
return u, client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testX5CIssuer(t *testing.T, caURL *url.URL) *x5cIssuer {
|
||||||
|
t.Helper()
|
||||||
|
x5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return x5c
|
||||||
|
}
|
||||||
|
|
||||||
|
func testJWKIssuer(t *testing.T, caURL *url.URL) *jwkIssuer {
|
||||||
|
t.Helper()
|
||||||
|
x5c, err := newJWKIssuer(caURL, &apiv1.CertificateIssuer{
|
||||||
|
Type: "jwk",
|
||||||
|
Provisioner: "ra@doe.org",
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return x5c
|
||||||
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
testRootCrt, testRootKey = mustSignCertificate("Test Root Certificate", nil, x509util.DefaultRootTemplate, nil, nil)
|
testRootCrt, testRootKey = mustSignCertificate("Test Root Certificate", nil, x509util.DefaultRootTemplate, nil, nil)
|
||||||
testIssCrt, testIssKey = mustSignCertificate("Test Intermediate Certificate", nil, x509util.DefaultIntermediateTemplate, testRootCrt, testRootKey)
|
testIssCrt, testIssKey = mustSignCertificate("Test Intermediate Certificate", nil, x509util.DefaultIntermediateTemplate, testRootCrt, testRootKey)
|
||||||
|
@ -258,7 +285,7 @@ func TestNew(t *testing.T) {
|
||||||
Key: testX5CKeyPath,
|
Key: testX5CKeyPath,
|
||||||
},
|
},
|
||||||
}}, &StepCAS{
|
}}, &StepCAS{
|
||||||
x5c: &x5cIssuer{
|
iss: &x5cIssuer{
|
||||||
caURL: caURL,
|
caURL: caURL,
|
||||||
certFile: testX5CPath,
|
certFile: testX5CPath,
|
||||||
keyFile: testX5CKeyPath,
|
keyFile: testX5CKeyPath,
|
||||||
|
@ -267,6 +294,23 @@ func TestNew(t *testing.T) {
|
||||||
client: client,
|
client: client,
|
||||||
fingerprint: testRootFingerprint,
|
fingerprint: testRootFingerprint,
|
||||||
}, false},
|
}, false},
|
||||||
|
{"ok jwk", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "jwk",
|
||||||
|
Provisioner: "ra@doe.org",
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, &StepCAS{
|
||||||
|
iss: &jwkIssuer{
|
||||||
|
caURL: caURL,
|
||||||
|
keyFile: testX5CKeyPath,
|
||||||
|
issuer: "ra@doe.org",
|
||||||
|
},
|
||||||
|
client: client,
|
||||||
|
fingerprint: testRootFingerprint,
|
||||||
|
}, false},
|
||||||
{"fail authority", args{context.TODO(), apiv1.Options{
|
{"fail authority", args{context.TODO(), apiv1.Options{
|
||||||
CertificateAuthority: "",
|
CertificateAuthority: "",
|
||||||
CertificateAuthorityFingerprint: testRootFingerprint,
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
@ -307,6 +351,15 @@ func TestNew(t *testing.T) {
|
||||||
Key: testX5CKeyPath,
|
Key: testX5CKeyPath,
|
||||||
},
|
},
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
|
{"fail provisioner jwk", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "jwk",
|
||||||
|
Provisioner: "",
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
{"fail certificate", args{context.TODO(), apiv1.Options{
|
{"fail certificate", args{context.TODO(), apiv1.Options{
|
||||||
CertificateAuthority: caURL.String(),
|
CertificateAuthority: caURL.String(),
|
||||||
CertificateAuthorityFingerprint: testRootFingerprint,
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
@ -327,6 +380,15 @@ func TestNew(t *testing.T) {
|
||||||
Key: "",
|
Key: "",
|
||||||
},
|
},
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
|
{"fail key jwk", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "jwk",
|
||||||
|
Provisioner: "ra@smallstep.com",
|
||||||
|
Key: "",
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
{"bad authority", args{context.TODO(), apiv1.Options{
|
{"bad authority", args{context.TODO(), apiv1.Options{
|
||||||
CertificateAuthority: "https://foobar",
|
CertificateAuthority: "https://foobar",
|
||||||
CertificateAuthorityFingerprint: testRootFingerprint,
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
@ -367,6 +429,15 @@ func TestNew(t *testing.T) {
|
||||||
Key: testX5CKeyPath,
|
Key: testX5CKeyPath,
|
||||||
},
|
},
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
|
{"fail new jwk issuer", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "jwk",
|
||||||
|
Provisioner: "ra@doe.org",
|
||||||
|
Key: testX5CKeyPath + ".missing",
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
{"bad issuer", args{context.TODO(), apiv1.Options{
|
{"bad issuer", args{context.TODO(), apiv1.Options{
|
||||||
CertificateAuthority: caURL.String(),
|
CertificateAuthority: caURL.String(),
|
||||||
CertificateAuthorityFingerprint: testRootFingerprint,
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
@ -402,18 +473,11 @@ 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, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{
|
x5c := testX5CIssuer(t, caURL)
|
||||||
Type: "x5c",
|
jwk := testJWKIssuer(t, caURL)
|
||||||
Provisioner: "X5C",
|
|
||||||
Certificate: testX5CPath,
|
|
||||||
Key: testX5CKeyPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
x5c *x5cIssuer
|
iss stepIssuer
|
||||||
client *ca.Client
|
client *ca.Client
|
||||||
fingerprint string
|
fingerprint string
|
||||||
}
|
}
|
||||||
|
@ -434,6 +498,13 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
|
||||||
Certificate: testCrt,
|
Certificate: testCrt,
|
||||||
CertificateChain: []*x509.Certificate{testIssCrt},
|
CertificateChain: []*x509.Certificate{testIssCrt},
|
||||||
}, false},
|
}, false},
|
||||||
|
{"ok jwk", fields{jwk, 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,
|
||||||
|
@ -442,7 +513,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
|
||||||
CSR: testCR,
|
CSR: testCR,
|
||||||
Lifetime: 0,
|
Lifetime: 0,
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
{"fail sign token", fields{nil, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
|
{"fail sign token", fields{mockErrIssuer{}, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
|
||||||
CSR: testCR,
|
CSR: testCR,
|
||||||
Lifetime: time.Hour,
|
Lifetime: time.Hour,
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
|
@ -454,7 +525,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
s := &StepCAS{
|
s := &StepCAS{
|
||||||
x5c: tt.fields.x5c,
|
iss: tt.fields.iss,
|
||||||
client: tt.fields.client,
|
client: tt.fields.client,
|
||||||
fingerprint: tt.fields.fingerprint,
|
fingerprint: tt.fields.fingerprint,
|
||||||
}
|
}
|
||||||
|
@ -472,18 +543,11 @@ 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, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{
|
x5c := testX5CIssuer(t, caURL)
|
||||||
Type: "x5c",
|
jwk := testJWKIssuer(t, caURL)
|
||||||
Provisioner: "X5C",
|
|
||||||
Certificate: testX5CPath,
|
|
||||||
Key: testX5CKeyPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
x5c *x5cIssuer
|
iss stepIssuer
|
||||||
client *ca.Client
|
client *ca.Client
|
||||||
fingerprint string
|
fingerprint string
|
||||||
}
|
}
|
||||||
|
@ -501,11 +565,15 @@ func TestStepCAS_RenewCertificate(t *testing.T) {
|
||||||
CSR: testCR,
|
CSR: testCR,
|
||||||
Lifetime: time.Hour,
|
Lifetime: time.Hour,
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
|
{"not implemented jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{
|
||||||
|
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) {
|
||||||
s := &StepCAS{
|
s := &StepCAS{
|
||||||
x5c: tt.fields.x5c,
|
iss: tt.fields.iss,
|
||||||
client: tt.fields.client,
|
client: tt.fields.client,
|
||||||
fingerprint: tt.fields.fingerprint,
|
fingerprint: tt.fields.fingerprint,
|
||||||
}
|
}
|
||||||
|
@ -523,18 +591,11 @@ 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, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{
|
x5c := testX5CIssuer(t, caURL)
|
||||||
Type: "x5c",
|
jwk := testJWKIssuer(t, caURL)
|
||||||
Provisioner: "X5C",
|
|
||||||
Certificate: testX5CPath,
|
|
||||||
Key: testX5CKeyPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
x5c *x5cIssuer
|
iss stepIssuer
|
||||||
client *ca.Client
|
client *ca.Client
|
||||||
fingerprint string
|
fingerprint string
|
||||||
}
|
}
|
||||||
|
@ -564,11 +625,27 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
|
||||||
}}, &apiv1.RevokeCertificateResponse{
|
}}, &apiv1.RevokeCertificateResponse{
|
||||||
Certificate: testCrt,
|
Certificate: testCrt,
|
||||||
}, false},
|
}, false},
|
||||||
|
{"ok serial number jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||||
|
SerialNumber: "ok",
|
||||||
|
Certificate: nil,
|
||||||
|
}}, &apiv1.RevokeCertificateResponse{}, false},
|
||||||
|
{"ok certificate jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||||
|
SerialNumber: "",
|
||||||
|
Certificate: testCrt,
|
||||||
|
}}, &apiv1.RevokeCertificateResponse{
|
||||||
|
Certificate: testCrt,
|
||||||
|
}, false},
|
||||||
|
{"ok both jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||||
|
SerialNumber: "ok",
|
||||||
|
Certificate: testCrt,
|
||||||
|
}}, &apiv1.RevokeCertificateResponse{
|
||||||
|
Certificate: testCrt,
|
||||||
|
}, 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,
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
{"fail revoke token", fields{nil, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
{"fail revoke token", fields{mockErrIssuer{}, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||||
SerialNumber: "ok",
|
SerialNumber: "ok",
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
{"fail client revoke", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
{"fail client revoke", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||||
|
@ -578,7 +655,7 @@ func TestStepCAS_RevokeCertificate(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
s := &StepCAS{
|
s := &StepCAS{
|
||||||
x5c: tt.fields.x5c,
|
iss: tt.fields.iss,
|
||||||
client: tt.fields.client,
|
client: tt.fields.client,
|
||||||
fingerprint: tt.fields.fingerprint,
|
fingerprint: tt.fields.fingerprint,
|
||||||
}
|
}
|
||||||
|
@ -596,18 +673,11 @@ 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, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{
|
x5c := testX5CIssuer(t, caURL)
|
||||||
Type: "x5c",
|
jwk := testJWKIssuer(t, caURL)
|
||||||
Provisioner: "X5C",
|
|
||||||
Certificate: testX5CPath,
|
|
||||||
Key: testX5CKeyPath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
x5c *x5cIssuer
|
iss stepIssuer
|
||||||
client *ca.Client
|
client *ca.Client
|
||||||
fingerprint string
|
fingerprint string
|
||||||
}
|
}
|
||||||
|
@ -626,6 +696,11 @@ func TestStepCAS_GetCertificateAuthority(t *testing.T) {
|
||||||
}}, &apiv1.GetCertificateAuthorityResponse{
|
}}, &apiv1.GetCertificateAuthorityResponse{
|
||||||
RootCertificate: testRootCrt,
|
RootCertificate: testRootCrt,
|
||||||
}, false},
|
}, false},
|
||||||
|
{"ok jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.GetCertificateAuthorityRequest{
|
||||||
|
Name: caURL.String(),
|
||||||
|
}}, &apiv1.GetCertificateAuthorityResponse{
|
||||||
|
RootCertificate: testRootCrt,
|
||||||
|
}, false},
|
||||||
{"fail fingerprint", fields{x5c, client, "fail"}, args{&apiv1.GetCertificateAuthorityRequest{
|
{"fail fingerprint", fields{x5c, client, "fail"}, args{&apiv1.GetCertificateAuthorityRequest{
|
||||||
Name: caURL.String(),
|
Name: caURL.String(),
|
||||||
}}, nil, true},
|
}}, nil, true},
|
||||||
|
@ -633,7 +708,7 @@ func TestStepCAS_GetCertificateAuthority(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
s := &StepCAS{
|
s := &StepCAS{
|
||||||
x5c: tt.fields.x5c,
|
iss: tt.fields.iss,
|
||||||
client: tt.fields.client,
|
client: tt.fields.client,
|
||||||
fingerprint: tt.fields.fingerprint,
|
fingerprint: tt.fields.fingerprint,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue