forked from TrueCloudLab/certificates
commit
71f59de396
19 changed files with 2022 additions and 28 deletions
|
@ -189,7 +189,6 @@ func (c *Config) Validate() error {
|
||||||
|
|
||||||
// Options holds the RA/CAS configuration.
|
// Options holds the RA/CAS configuration.
|
||||||
ra := c.AuthorityConfig.Options
|
ra := c.AuthorityConfig.Options
|
||||||
|
|
||||||
// The default RA/CAS requires root, crt and key.
|
// The default RA/CAS requires root, crt and key.
|
||||||
if ra.Is(cas.SoftCAS) {
|
if ra.Is(cas.SoftCAS) {
|
||||||
switch {
|
switch {
|
||||||
|
|
|
@ -148,6 +148,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
|
||||||
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
|
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
|
||||||
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
||||||
Template: leaf,
|
Template: leaf,
|
||||||
|
CSR: csr,
|
||||||
Lifetime: lifetime,
|
Lifetime: lifetime,
|
||||||
Backdate: signOpts.Backdate,
|
Backdate: signOpts.Backdate,
|
||||||
})
|
})
|
||||||
|
@ -333,22 +334,21 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
||||||
if !ok {
|
if !ok {
|
||||||
return errs.InternalServer("authority.Revoke; provisioner not found", opts...)
|
return errs.InternalServer("authority.Revoke; provisioner not found", opts...)
|
||||||
}
|
}
|
||||||
|
rci.ProvisionerID = p.GetID()
|
||||||
rci.TokenID, err = p.GetTokenID(revokeOpts.OTT)
|
rci.TokenID, err = p.GetTokenID(revokeOpts.OTT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.Wrap(http.StatusInternalServerError, err,
|
return errs.Wrap(http.StatusInternalServerError, err,
|
||||||
"authority.Revoke; could not get ID for token")
|
"authority.Revoke; could not get ID for token")
|
||||||
}
|
}
|
||||||
|
opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID))
|
||||||
opts = append(opts, errs.WithKeyVal("tokenID", rci.TokenID))
|
opts = append(opts, errs.WithKeyVal("tokenID", rci.TokenID))
|
||||||
} else {
|
} else {
|
||||||
// Load the Certificate provisioner if one exists.
|
// Load the Certificate provisioner if one exists.
|
||||||
p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt)
|
if p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil {
|
||||||
if err != nil {
|
|
||||||
return errs.Wrap(http.StatusUnauthorized, err,
|
|
||||||
"authority.Revoke: unable to load certificate provisioner", opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rci.ProvisionerID = p.GetID()
|
rci.ProvisionerID = p.GetID()
|
||||||
opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID))
|
opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {
|
if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {
|
||||||
err = a.db.RevokeSSH(rci)
|
err = a.db.RevokeSSH(rci)
|
||||||
|
@ -368,8 +368,10 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error
|
||||||
// The revoke happens when this is stored in the db.
|
// The revoke happens when this is stored in the db.
|
||||||
_, err = a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{
|
_, err = a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{
|
||||||
Certificate: revokedCert,
|
Certificate: revokedCert,
|
||||||
|
SerialNumber: rci.Serial,
|
||||||
Reason: rci.Reason,
|
Reason: rci.Reason,
|
||||||
ReasonCode: rci.ReasonCode,
|
ReasonCode: rci.ReasonCode,
|
||||||
|
PassiveOnly: revokeOpts.PassiveOnly,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
|
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
|
||||||
|
@ -427,6 +429,7 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
|
||||||
|
|
||||||
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
|
||||||
Template: certTpl,
|
Template: certTpl,
|
||||||
|
CSR: cr,
|
||||||
Lifetime: 24 * time.Hour,
|
Lifetime: 24 * time.Hour,
|
||||||
Backdate: 1 * time.Minute,
|
Backdate: 1 * time.Minute,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1231,6 +1231,30 @@ func TestAuthority_Revoke(t *testing.T) {
|
||||||
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
|
|
||||||
|
return test{
|
||||||
|
auth: _a,
|
||||||
|
opts: &RevokeOptions{
|
||||||
|
Crt: crt,
|
||||||
|
Serial: "102012593071130646873265215610956555026",
|
||||||
|
ReasonCode: reasonCode,
|
||||||
|
Reason: reason,
|
||||||
|
MTLS: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ok/mTLS-no-provisioner": func() test {
|
||||||
|
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))
|
||||||
|
|
||||||
|
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
// Filter out provisioner extension.
|
||||||
|
for i, ext := range crt.Extensions {
|
||||||
|
if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}) {
|
||||||
|
crt.Extensions = append(crt.Extensions[:i], crt.Extensions[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return test{
|
return test{
|
||||||
auth: _a,
|
auth: _a,
|
||||||
opts: &RevokeOptions{
|
opts: &RevokeOptions{
|
||||||
|
|
|
@ -14,16 +14,28 @@ type Options struct {
|
||||||
// The type of the CAS to use.
|
// The type of the CAS to use.
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
||||||
// Path to the credentials file used in CloudCAS
|
// CertificateAuthority reference:
|
||||||
CredentialsFile string `json:"credentialsFile"`
|
// In StepCAS the value is the CA url, e.g. "https://ca.smallstep.com:9000".
|
||||||
|
// In CloudCAS the format is "projects/*/locations/*/certificateAuthorities/*".
|
||||||
|
CertificateAuthority string `json:"certificateAuthority,omitempty"`
|
||||||
|
|
||||||
// CertificateAuthority reference. In CloudCAS the format is
|
// CertificateAuthorityFingerprint is the root fingerprint used to
|
||||||
// `projects/*/locations/*/certificateAuthorities/*`.
|
// authenticate the connection to the CA when using StepCAS.
|
||||||
CertificateAuthority string `json:"certificateAuthority"`
|
CertificateAuthorityFingerprint string `json:"certificateAuthorityFingerprint,omitempty"`
|
||||||
|
|
||||||
// Certificate and signer are the issuer certificate,along with any other bundled certificates to be returned in the chain for consumers, and signer used in SoftCAS.
|
// CertificateIssuer contains the configuration used in StepCAS.
|
||||||
// They are configured in ca.json crt and key properties.
|
CertificateIssuer *CertificateIssuer `json:"certificateIssuer,omitempty"`
|
||||||
CertificateChain []*x509.Certificate
|
|
||||||
|
// Path to the credentials file used in CloudCAS. If not defined the default
|
||||||
|
// authentication mechanism provided by Google SDK will be used. See
|
||||||
|
// https://cloud.google.com/docs/authentication.
|
||||||
|
CredentialsFile string `json:"credentialsFile,omitempty"`
|
||||||
|
|
||||||
|
// Certificate and signer are the issuer certificate, along with any other
|
||||||
|
// bundled certificates to be returned in the chain for consumers, and
|
||||||
|
// signer used in SoftCAS. They are configured in ca.json crt and key
|
||||||
|
// properties.
|
||||||
|
CertificateChain []*x509.Certificate `json:"-"`
|
||||||
Signer crypto.Signer `json:"-"`
|
Signer crypto.Signer `json:"-"`
|
||||||
|
|
||||||
// IsCreator is set to true when we're creating a certificate authority. Is
|
// IsCreator is set to true when we're creating a certificate authority. Is
|
||||||
|
@ -39,6 +51,16 @@ type Options struct {
|
||||||
Location string `json:"-"`
|
Location string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CertificateIssuer contains the properties used to use the StepCAS certificate
|
||||||
|
// authority service.
|
||||||
|
type CertificateIssuer struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
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.
|
// Validate checks the fields in Options.
|
||||||
func (o *Options) Validate() error {
|
func (o *Options) Validate() error {
|
||||||
var typ Type
|
var typ Type
|
||||||
|
|
|
@ -53,6 +53,7 @@ const (
|
||||||
// CreateCertificateRequest is the request used to sign a new certificate.
|
// CreateCertificateRequest is the request used to sign a new certificate.
|
||||||
type CreateCertificateRequest struct {
|
type CreateCertificateRequest struct {
|
||||||
Template *x509.Certificate
|
Template *x509.Certificate
|
||||||
|
CSR *x509.CertificateRequest
|
||||||
Lifetime time.Duration
|
Lifetime time.Duration
|
||||||
Backdate time.Duration
|
Backdate time.Duration
|
||||||
RequestID string
|
RequestID string
|
||||||
|
@ -67,6 +68,7 @@ type CreateCertificateResponse struct {
|
||||||
// RenewCertificateRequest is the request used to re-sign a certificate.
|
// RenewCertificateRequest is the request used to re-sign a certificate.
|
||||||
type RenewCertificateRequest struct {
|
type RenewCertificateRequest struct {
|
||||||
Template *x509.Certificate
|
Template *x509.Certificate
|
||||||
|
CSR *x509.CertificateRequest
|
||||||
Lifetime time.Duration
|
Lifetime time.Duration
|
||||||
Backdate time.Duration
|
Backdate time.Duration
|
||||||
RequestID string
|
RequestID string
|
||||||
|
@ -81,8 +83,10 @@ type RenewCertificateResponse struct {
|
||||||
// RevokeCertificateRequest is the request used to revoke a certificate.
|
// RevokeCertificateRequest is the request used to revoke a certificate.
|
||||||
type RevokeCertificateRequest struct {
|
type RevokeCertificateRequest struct {
|
||||||
Certificate *x509.Certificate
|
Certificate *x509.Certificate
|
||||||
|
SerialNumber string
|
||||||
Reason string
|
Reason string
|
||||||
ReasonCode int
|
ReasonCode int
|
||||||
|
PassiveOnly bool
|
||||||
RequestID string
|
RequestID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package apiv1
|
package apiv1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,6 +36,8 @@ const (
|
||||||
SoftCAS = "softcas"
|
SoftCAS = "softcas"
|
||||||
// CloudCAS is a CertificateAuthorityService using Google Cloud CAS.
|
// CloudCAS is a CertificateAuthorityService using Google Cloud CAS.
|
||||||
CloudCAS = "cloudcas"
|
CloudCAS = "cloudcas"
|
||||||
|
// StepCAS is a CertificateAuthorityService using another step-ca instance.
|
||||||
|
StepCAS = "stepcas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns a string from the type. It will always return the lower case
|
// String returns a string from the type. It will always return the lower case
|
||||||
|
@ -46,3 +49,23 @@ func (t Type) String() string {
|
||||||
}
|
}
|
||||||
return strings.ToLower(string(t))
|
return strings.ToLower(string(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrNotImplemented is the type of error returned if an operation is not
|
||||||
|
// implemented.
|
||||||
|
type ErrNotImplemented struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNotImplemented implements the error interface.
|
||||||
|
func (e ErrNotImplemented) Error() string {
|
||||||
|
if e.Message != "" {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
return "not implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCode implements the StatusCoder interface and returns the HTTP 501
|
||||||
|
// error.
|
||||||
|
func (e ErrNotImplemented) StatusCode() int {
|
||||||
|
return http.StatusNotImplemented
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package apiv1
|
package apiv1
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestType_String(t *testing.T) {
|
func TestType_String(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -21,3 +23,51 @@ func TestType_String(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestErrNotImplemented_Error(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"default", fields{""}, "not implemented"},
|
||||||
|
{"with message", fields{"method not supported"}, "method not supported"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
e := ErrNotImplemented{
|
||||||
|
Message: tt.fields.Message,
|
||||||
|
}
|
||||||
|
if got := e.Error(); got != tt.want {
|
||||||
|
t.Errorf("ErrNotImplemented.Error() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrNotImplemented_StatusCode(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"default", fields{""}, 501},
|
||||||
|
{"with message", fields{"method not supported"}, 501},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := ErrNotImplemented{
|
||||||
|
Message: tt.fields.Message,
|
||||||
|
}
|
||||||
|
if got := s.StatusCode(); got != tt.want {
|
||||||
|
t.Errorf("ErrNotImplemented.StatusCode() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -141,7 +141,6 @@ func (c *CloudCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityReq
|
||||||
Name: name,
|
Name: name,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println(name)
|
|
||||||
return nil, errors.Wrap(err, "cloudCAS GetCertificateAuthority failed")
|
return nil, errors.Wrap(err, "cloudCAS GetCertificateAuthority failed")
|
||||||
}
|
}
|
||||||
if len(resp.PemCaCertificates) == 0 {
|
if len(resp.PemCaCertificates) == 0 {
|
||||||
|
|
78
cas/stepcas/issuer.go
Normal file
78
cas/stepcas/issuer.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
package stepcas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"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
|
||||||
|
// issuer.
|
||||||
|
func validateCertificateIssuer(iss *apiv1.CertificateIssuer) error {
|
||||||
|
switch {
|
||||||
|
case iss == nil:
|
||||||
|
return errors.New("stepCAS 'certificateIssuer' cannot be nil")
|
||||||
|
case iss.Type == "":
|
||||||
|
return errors.New("stepCAS `certificateIssuer.type` cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(iss.Type) {
|
||||||
|
case "x5c":
|
||||||
|
return validateX5CIssuer(iss)
|
||||||
|
case "jwk":
|
||||||
|
return validateJWKIssuer(iss)
|
||||||
|
default:
|
||||||
|
return errors.Errorf("stepCAS `certificateIssuer.type` %s is not supported", iss.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateX5CIssuer validates the configuration of x5c issuer.
|
||||||
|
func validateX5CIssuer(iss *apiv1.CertificateIssuer) error {
|
||||||
|
switch {
|
||||||
|
case iss.Certificate == "":
|
||||||
|
return errors.New("stepCAS `certificateIssuer.crt` cannot be empty")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
92
cas/stepcas/jwk_issuer.go
Normal file
92
cas/stepcas/jwk_issuer.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package stepcas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/smallstep/certificates/cas/apiv1"
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
"go.step.sm/crypto/randutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jwkIssuer struct {
|
||||||
|
caURL *url.URL
|
||||||
|
issuer string
|
||||||
|
keyFile string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJWKIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) {
|
||||||
|
_, err := newJWKSigner(cfg.Key, cfg.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &jwkIssuer{
|
||||||
|
caURL: caURL,
|
||||||
|
issuer: cfg.Provisioner,
|
||||||
|
keyFile: cfg.Key,
|
||||||
|
password: cfg.Password,
|
||||||
|
}, 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, i.password)
|
||||||
|
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, 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
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
178
cas/stepcas/stepcas.go
Normal file
178
cas/stepcas/stepcas.go
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
package stepcas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/smallstep/certificates/api"
|
||||||
|
"github.com/smallstep/certificates/ca"
|
||||||
|
"github.com/smallstep/certificates/cas/apiv1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
apiv1.Register(apiv1.StepCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {
|
||||||
|
return New(ctx, opts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepCAS implements the cas.CertificateAuthorityService interface using
|
||||||
|
// another step-ca instance.
|
||||||
|
type StepCAS struct {
|
||||||
|
iss stepIssuer
|
||||||
|
client *ca.Client
|
||||||
|
fingerprint string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new CertificateAuthorityService implementation using another
|
||||||
|
// step-ca instance.
|
||||||
|
func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) {
|
||||||
|
switch {
|
||||||
|
case opts.CertificateAuthority == "":
|
||||||
|
return nil, errors.New("stepCAS 'certificateAuthority' cannot be empty")
|
||||||
|
case opts.CertificateAuthorityFingerprint == "":
|
||||||
|
return nil, errors.New("stepCAS 'certificateAuthorityFingerprint' cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
caURL, err := url.Parse(opts.CertificateAuthority)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "stepCAS `certificateAuthority` is not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create configured issuer
|
||||||
|
iss, err := newStepIssuer(caURL, opts.CertificateIssuer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create client.
|
||||||
|
client, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StepCAS{
|
||||||
|
iss: iss,
|
||||||
|
client: client,
|
||||||
|
fingerprint: opts.CertificateAuthorityFingerprint,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCertificate uses the step-ca sign request with the configured
|
||||||
|
// provisioner to get a new certificate from the certificate authority.
|
||||||
|
func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {
|
||||||
|
switch {
|
||||||
|
case req.CSR == nil:
|
||||||
|
return nil, errors.New("createCertificateRequest `csr` cannot be nil")
|
||||||
|
case req.Lifetime == 0:
|
||||||
|
return nil, errors.New("createCertificateRequest `lifetime` cannot be 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, chain, err := s.createCertificate(req.CSR, req.Lifetime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apiv1.CreateCertificateResponse{
|
||||||
|
Certificate: cert,
|
||||||
|
CertificateChain: chain,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenewCertificate will always return a non-implemented error as mTLS renewals
|
||||||
|
// are not supported yet.
|
||||||
|
func (s *StepCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {
|
||||||
|
return nil, apiv1.ErrNotImplemented{Message: "stepCAS does not support mTLS renewals"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
|
||||||
|
switch {
|
||||||
|
case req.SerialNumber == "" && req.Certificate == nil:
|
||||||
|
return nil, errors.New("revokeCertificateRequest `serialNumber` or `certificate` are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
serialNumber := req.SerialNumber
|
||||||
|
if req.Certificate != nil {
|
||||||
|
serialNumber = req.Certificate.SerialNumber.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := s.iss.RevokeToken(serialNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.client.Revoke(&api.RevokeRequest{
|
||||||
|
Serial: serialNumber,
|
||||||
|
ReasonCode: req.ReasonCode,
|
||||||
|
Reason: req.Reason,
|
||||||
|
OTT: token,
|
||||||
|
Passive: req.PassiveOnly,
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apiv1.RevokeCertificateResponse{
|
||||||
|
Certificate: req.Certificate,
|
||||||
|
CertificateChain: nil,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCertificateAuthority returns the root certificate of the certificate
|
||||||
|
// authority using the configured fingerprint.
|
||||||
|
func (s *StepCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {
|
||||||
|
resp, err := s.client.Root(s.fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &apiv1.GetCertificateAuthorityResponse{
|
||||||
|
RootCertificate: resp.RootPEM.Certificate,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.Duration) (*x509.Certificate, []*x509.Certificate, error) {
|
||||||
|
sans := make([]string, 0, len(cr.DNSNames)+len(cr.EmailAddresses)+len(cr.IPAddresses)+len(cr.URIs))
|
||||||
|
sans = append(sans, cr.DNSNames...)
|
||||||
|
sans = append(sans, cr.EmailAddresses...)
|
||||||
|
for _, ip := range cr.IPAddresses {
|
||||||
|
sans = append(sans, ip.String())
|
||||||
|
}
|
||||||
|
for _, u := range cr.URIs {
|
||||||
|
sans = append(sans, u.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
commonName := cr.Subject.CommonName
|
||||||
|
if commonName == "" && len(sans) > 0 {
|
||||||
|
commonName = sans[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := s.iss.SignToken(commonName, sans)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.client.Sign(&api.SignRequest{
|
||||||
|
CsrPEM: api.CertificateRequest{CertificateRequest: cr},
|
||||||
|
OTT: token,
|
||||||
|
NotAfter: s.lifetime(lifetime),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var chain []*x509.Certificate
|
||||||
|
cert := resp.CertChainPEM[0].Certificate
|
||||||
|
for _, c := range resp.CertChainPEM[1:] {
|
||||||
|
chain = append(chain, c.Certificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, chain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepCAS) lifetime(d time.Duration) api.TimeDuration {
|
||||||
|
var td api.TimeDuration
|
||||||
|
td.SetDuration(s.iss.Lifetime(d))
|
||||||
|
return td
|
||||||
|
}
|
807
cas/stepcas/stepcas_test.go
Normal file
807
cas/stepcas/stepcas_test.go
Normal file
|
@ -0,0 +1,807 @@
|
||||||
|
package stepcas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testRootCrt *x509.Certificate
|
||||||
|
testRootKey crypto.Signer
|
||||||
|
testRootPath, testRootKeyPath string
|
||||||
|
testRootFingerprint string
|
||||||
|
|
||||||
|
testIssCrt *x509.Certificate
|
||||||
|
testIssKey crypto.Signer
|
||||||
|
testIssPath, testIssKeyPath string
|
||||||
|
|
||||||
|
testX5CCrt *x509.Certificate
|
||||||
|
testX5CKey crypto.Signer
|
||||||
|
testX5CPath, testX5CKeyPath string
|
||||||
|
testPassword, testEncryptedKeyPath string
|
||||||
|
|
||||||
|
testCR *x509.CertificateRequest
|
||||||
|
testCrt *x509.Certificate
|
||||||
|
testKey crypto.Signer
|
||||||
|
testFailCR *x509.CertificateRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustSignCertificate(subject string, sans []string, template string, parent *x509.Certificate, signer crypto.Signer) (*x509.Certificate, crypto.Signer) {
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cr, err := x509util.CreateCertificateRequest(subject, sans, priv)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cert, err := x509util.NewCertificate(cr, x509util.WithTemplate(template, x509util.CreateTemplateData(subject, sans)))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
crt := cert.GetCertificate()
|
||||||
|
crt.NotBefore = time.Now()
|
||||||
|
crt.NotAfter = crt.NotBefore.Add(time.Hour)
|
||||||
|
if parent == nil {
|
||||||
|
parent = crt
|
||||||
|
}
|
||||||
|
if signer == nil {
|
||||||
|
signer = priv
|
||||||
|
}
|
||||||
|
if crt, err = x509util.CreateCertificate(crt, parent, pub, signer); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return crt, priv
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustSerializeCrt(filename string, certs ...*x509.Certificate) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
for _, c := range certs {
|
||||||
|
if err := pem.Encode(buf, &pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: c.Raw,
|
||||||
|
}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(filename, buf.Bytes(), 0600); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustSerializeKey(filename string, key crypto.Signer) {
|
||||||
|
b, err := x509.MarshalPKCS8PrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
b = pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PRIVATE KEY",
|
||||||
|
Bytes: b,
|
||||||
|
})
|
||||||
|
if err := ioutil.WriteFile(filename, b, 0600); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
writeJSON := func(w http.ResponseWriter, v interface{}) {
|
||||||
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
parseJSON := func(r *http.Request, v interface{}) {
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(v)
|
||||||
|
}
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.RequestURI == "/root/"+testRootFingerprint:
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
writeJSON(w, api.RootResponse{
|
||||||
|
RootPEM: api.NewCertificate(testRootCrt),
|
||||||
|
})
|
||||||
|
case r.RequestURI == "/sign":
|
||||||
|
var msg api.SignRequest
|
||||||
|
parseJSON(r, &msg)
|
||||||
|
if msg.CsrPEM.DNSNames[0] == "fail.doe.org" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, `{"error":"fail","message":"fail"}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
writeJSON(w, api.SignResponse{
|
||||||
|
CertChainPEM: []api.Certificate{api.NewCertificate(testCrt), api.NewCertificate(testIssCrt)},
|
||||||
|
})
|
||||||
|
case r.RequestURI == "/revoke":
|
||||||
|
var msg api.RevokeRequest
|
||||||
|
parseJSON(r, &msg)
|
||||||
|
if msg.Serial == "fail" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, `{"error":"fail","message":"fail"}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
writeJSON(w, api.RevokeResponse{
|
||||||
|
Status: "ok",
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, `{"error":"not found"}`)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
srv.Close()
|
||||||
|
})
|
||||||
|
u, err := url.Parse(srv.URL)
|
||||||
|
if err != nil {
|
||||||
|
srv.Close()
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ca.NewClient(srv.URL, ca.WithTransport(http.DefaultTransport))
|
||||||
|
if err != nil {
|
||||||
|
srv.Close()
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, client
|
||||||
|
}
|
||||||
|
|
||||||
|
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: key,
|
||||||
|
Password: password,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
x5c.password = givenPassword
|
||||||
|
return x5c
|
||||||
|
}
|
||||||
|
|
||||||
|
func testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer {
|
||||||
|
t.Helper()
|
||||||
|
key, givenPassword := testX5CKeyPath, password
|
||||||
|
if password != "" {
|
||||||
|
key = testEncryptedKeyPath
|
||||||
|
password = testPassword
|
||||||
|
}
|
||||||
|
jwk, err := newJWKIssuer(caURL, &apiv1.CertificateIssuer{
|
||||||
|
Type: "jwk",
|
||||||
|
Provisioner: "ra@doe.org",
|
||||||
|
Key: key,
|
||||||
|
Password: password,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
jwk.password = givenPassword
|
||||||
|
return jwk
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
testRootCrt, testRootKey = mustSignCertificate("Test Root Certificate", nil, x509util.DefaultRootTemplate, nil, nil)
|
||||||
|
testIssCrt, testIssKey = mustSignCertificate("Test Intermediate Certificate", nil, x509util.DefaultIntermediateTemplate, testRootCrt, testRootKey)
|
||||||
|
testX5CCrt, testX5CKey = mustSignCertificate("Test X5C Certificate", nil, x509util.DefaultLeafTemplate, testIssCrt, testIssKey)
|
||||||
|
|
||||||
|
// Final certificate.
|
||||||
|
var err error
|
||||||
|
sans := []string{"doe.org", "jane@doe.org", "127.0.0.1", "::1", "localhost", "uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6;name=value"}
|
||||||
|
testCrt, testKey = mustSignCertificate("Test Certificate", sans, x509util.DefaultLeafTemplate, testIssCrt, testIssKey)
|
||||||
|
testCR, err = x509util.CreateCertificateRequest("Test Certificate", sans, testKey)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CR used in errors.
|
||||||
|
testFailCR, err = x509util.CreateCertificateRequest("", []string{"fail.doe.org"}, testKey)
|
||||||
|
if err != nil {
|
||||||
|
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")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testRootPath = filepath.Join(path, "root_ca.crt")
|
||||||
|
testRootKeyPath = filepath.Join(path, "root_ca.key")
|
||||||
|
mustSerializeCrt(testRootPath, testRootCrt)
|
||||||
|
mustSerializeKey(testRootKeyPath, testRootKey)
|
||||||
|
|
||||||
|
testIssPath = filepath.Join(path, "intermediate_ca.crt")
|
||||||
|
testIssKeyPath = filepath.Join(path, "intermediate_ca.key")
|
||||||
|
mustSerializeCrt(testIssPath, testIssCrt)
|
||||||
|
mustSerializeKey(testIssKeyPath, testIssKey)
|
||||||
|
|
||||||
|
testX5CPath = filepath.Join(path, "x5c.crt")
|
||||||
|
testX5CKeyPath = filepath.Join(path, "x5c.key")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_init(t *testing.T) {
|
||||||
|
caURL, _ := testCAHelper(t)
|
||||||
|
|
||||||
|
fn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.StepCAS)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("apiv1.Register() ok = %v, want true", ok)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn(context.Background(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
caURL, client := testCAHelper(t)
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
opts apiv1.Options
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *StepCAS
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, &StepCAS{
|
||||||
|
iss: &x5cIssuer{
|
||||||
|
caURL: caURL,
|
||||||
|
certFile: testX5CPath,
|
||||||
|
keyFile: testX5CKeyPath,
|
||||||
|
issuer: "X5C",
|
||||||
|
},
|
||||||
|
client: client,
|
||||||
|
fingerprint: testRootFingerprint,
|
||||||
|
}, 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{
|
||||||
|
CertificateAuthority: "",
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail fingerprint", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: "",
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail type", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail provisioner", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, 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{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: "",
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail key", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: "",
|
||||||
|
},
|
||||||
|
}}, 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{
|
||||||
|
CertificateAuthority: "https://foobar",
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail parse url", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: "::failparse",
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail new client", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: "foobar",
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail new x5c issuer", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "x5c",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath + ".missing",
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, 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{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: nil}}, nil, true},
|
||||||
|
{"bad issuer type", args{context.TODO(), apiv1.Options{
|
||||||
|
CertificateAuthority: caURL.String(),
|
||||||
|
CertificateAuthorityFingerprint: testRootFingerprint,
|
||||||
|
CertificateIssuer: &apiv1.CertificateIssuer{
|
||||||
|
Type: "fail",
|
||||||
|
Provisioner: "X5C",
|
||||||
|
Certificate: testX5CPath,
|
||||||
|
Key: testX5CKeyPath,
|
||||||
|
},
|
||||||
|
}}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := New(tt.args.ctx, tt.args.opts)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// We cannot compare client
|
||||||
|
if got != nil && tt.want != nil {
|
||||||
|
got.client = tt.want.client
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("New() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCAS_CreateCertificate(t *testing.T) {
|
||||||
|
caURL, client := testCAHelper(t)
|
||||||
|
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
|
||||||
|
client *ca.Client
|
||||||
|
fingerprint string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
req *apiv1.CreateCertificateRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *apiv1.CreateCertificateResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
|
||||||
|
CSR: testCR,
|
||||||
|
Lifetime: time.Hour,
|
||||||
|
}}, &apiv1.CreateCertificateResponse{
|
||||||
|
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,
|
||||||
|
}}, &apiv1.CreateCertificateResponse{
|
||||||
|
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,
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail lifetime", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
|
||||||
|
CSR: testCR,
|
||||||
|
Lifetime: 0,
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail sign token", fields{mockErrIssuer{}, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
|
||||||
|
CSR: testCR,
|
||||||
|
Lifetime: time.Hour,
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail client sign", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{
|
||||||
|
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) {
|
||||||
|
s := &StepCAS{
|
||||||
|
iss: tt.fields.iss,
|
||||||
|
client: tt.fields.client,
|
||||||
|
fingerprint: tt.fields.fingerprint,
|
||||||
|
}
|
||||||
|
got, err := s.CreateCertificate(tt.args.req)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("StepCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("StepCAS.CreateCertificate() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCAS_RenewCertificate(t *testing.T) {
|
||||||
|
caURL, client := testCAHelper(t)
|
||||||
|
x5c := testX5CIssuer(t, caURL, "")
|
||||||
|
jwk := testJWKIssuer(t, caURL, "")
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
iss stepIssuer
|
||||||
|
client *ca.Client
|
||||||
|
fingerprint string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
req *apiv1.RenewCertificateRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *apiv1.RenewCertificateResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"not implemented", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{
|
||||||
|
CSR: testCR,
|
||||||
|
Lifetime: time.Hour,
|
||||||
|
}}, nil, true},
|
||||||
|
{"not implemented jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{
|
||||||
|
CSR: testCR,
|
||||||
|
Lifetime: time.Hour,
|
||||||
|
}}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := &StepCAS{
|
||||||
|
iss: tt.fields.iss,
|
||||||
|
client: tt.fields.client,
|
||||||
|
fingerprint: tt.fields.fingerprint,
|
||||||
|
}
|
||||||
|
got, err := s.RenewCertificate(tt.args.req)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("StepCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("StepCAS.RenewCertificate() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCAS_RevokeCertificate(t *testing.T) {
|
||||||
|
caURL, client := testCAHelper(t)
|
||||||
|
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
|
||||||
|
client *ca.Client
|
||||||
|
fingerprint string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
req *apiv1.RevokeCertificateRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *apiv1.RevokeCertificateResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok serial number", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||||
|
SerialNumber: "ok",
|
||||||
|
Certificate: nil,
|
||||||
|
}}, &apiv1.RevokeCertificateResponse{}, false},
|
||||||
|
{"ok certificate", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||||
|
SerialNumber: "",
|
||||||
|
Certificate: testCrt,
|
||||||
|
}}, &apiv1.RevokeCertificateResponse{
|
||||||
|
Certificate: testCrt,
|
||||||
|
}, false},
|
||||||
|
{"ok both", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||||
|
SerialNumber: "ok",
|
||||||
|
Certificate: testCrt,
|
||||||
|
}}, &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,
|
||||||
|
}}, &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},
|
||||||
|
{"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,
|
||||||
|
}}, nil, true},
|
||||||
|
{"fail revoke token", fields{mockErrIssuer{}, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{
|
||||||
|
SerialNumber: "ok",
|
||||||
|
}}, nil, true},
|
||||||
|
{"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) {
|
||||||
|
s := &StepCAS{
|
||||||
|
iss: tt.fields.iss,
|
||||||
|
client: tt.fields.client,
|
||||||
|
fingerprint: tt.fields.fingerprint,
|
||||||
|
}
|
||||||
|
got, err := s.RevokeCertificate(tt.args.req)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("StepCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("StepCAS.RevokeCertificate() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCAS_GetCertificateAuthority(t *testing.T) {
|
||||||
|
caURL, client := testCAHelper(t)
|
||||||
|
x5c := testX5CIssuer(t, caURL, "")
|
||||||
|
jwk := testJWKIssuer(t, caURL, "")
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
iss stepIssuer
|
||||||
|
client *ca.Client
|
||||||
|
fingerprint string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
req *apiv1.GetCertificateAuthorityRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *apiv1.GetCertificateAuthorityResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", fields{x5c, client, testRootFingerprint}, args{&apiv1.GetCertificateAuthorityRequest{
|
||||||
|
Name: caURL.String(),
|
||||||
|
}}, &apiv1.GetCertificateAuthorityResponse{
|
||||||
|
RootCertificate: testRootCrt,
|
||||||
|
}, 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{
|
||||||
|
Name: caURL.String(),
|
||||||
|
}}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := &StepCAS{
|
||||||
|
iss: tt.fields.iss,
|
||||||
|
client: tt.fields.client,
|
||||||
|
fingerprint: tt.fields.fingerprint,
|
||||||
|
}
|
||||||
|
got, err := s.GetCertificateAuthority(tt.args.req)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("StepCAS.GetCertificateAuthority() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("StepCAS.GetCertificateAuthority() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
185
cas/stepcas/x5c_issuer.go
Normal file
185
cas/stepcas/x5c_issuer.go
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
package stepcas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rsa"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultValidity = 5 * time.Minute
|
||||||
|
|
||||||
|
// timeNow returns the current time.
|
||||||
|
// This method is used for unit testing purposes.
|
||||||
|
var timeNow = func() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
type x5cIssuer struct {
|
||||||
|
caURL *url.URL
|
||||||
|
issuer string
|
||||||
|
certFile string
|
||||||
|
keyFile 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, cfg.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &x5cIssuer{
|
||||||
|
caURL: caURL,
|
||||||
|
issuer: cfg.Provisioner,
|
||||||
|
certFile: cfg.Certificate,
|
||||||
|
keyFile: cfg.Key,
|
||||||
|
password: cfg.Password,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *x5cIssuer) SignToken(subject string, sans []string) (string, error) {
|
||||||
|
aud := i.caURL.ResolveReference(&url.URL{
|
||||||
|
Path: "/1.0/sign",
|
||||||
|
Fragment: "x5c/" + i.issuer,
|
||||||
|
}).String()
|
||||||
|
|
||||||
|
return i.createToken(aud, subject, sans)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *x5cIssuer) RevokeToken(subject string) (string, error) {
|
||||||
|
aud := i.caURL.ResolveReference(&url.URL{
|
||||||
|
Path: "/1.0/revoke",
|
||||||
|
Fragment: "x5c/" + i.issuer,
|
||||||
|
}).String()
|
||||||
|
|
||||||
|
return i.createToken(aud, subject, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *x5cIssuer) Lifetime(d time.Duration) time.Duration {
|
||||||
|
cert, err := pemutil.ReadCertificate(i.certFile, pemutil.WithFirstBlock())
|
||||||
|
if err != nil {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
now := timeNow()
|
||||||
|
if now.Add(d + time.Minute).After(cert.NotAfter) {
|
||||||
|
return cert.NotAfter.Sub(now) - time.Minute
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *x5cIssuer) createToken(aud, sub string, sans []string) (string, error) {
|
||||||
|
signer, err := newX5CSigner(i.certFile, i.keyFile, i.password)
|
||||||
|
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 defaultClaims(iss, sub, aud, id string) jose.Claims {
|
||||||
|
now := timeNow()
|
||||||
|
return jose.Claims{
|
||||||
|
ID: id,
|
||||||
|
Issuer: iss,
|
||||||
|
Subject: sub,
|
||||||
|
Audience: jose.Audience{aud},
|
||||||
|
Expiry: jose.NewNumericDate(now.Add(defaultValidity)),
|
||||||
|
NotBefore: jose.NewNumericDate(now),
|
||||||
|
IssuedAt: jose.NewNumericDate(now),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
signer, ok := key.(crypto.Signer)
|
||||||
|
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, signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error validating x5c certificate chain and key")
|
||||||
|
}
|
||||||
|
|
||||||
|
so := new(jose.SignerOptions)
|
||||||
|
so.WithType("JWT")
|
||||||
|
so.WithHeader("kid", kid)
|
||||||
|
so.WithHeader("x5c", certs)
|
||||||
|
return newJoseSigner(signer, so)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJoseSigner(key crypto.Signer, so *jose.SignerOptions) (jose.Signer, error) {
|
||||||
|
var alg jose.SignatureAlgorithm
|
||||||
|
switch k := key.Public().(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
switch k.Curve.Params().Name {
|
||||||
|
case "P-256":
|
||||||
|
alg = jose.ES256
|
||||||
|
case "P-384":
|
||||||
|
alg = jose.ES384
|
||||||
|
case "P-521":
|
||||||
|
alg = jose.ES512
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("unsupported elliptic curve %s", k.Curve.Params().Name)
|
||||||
|
}
|
||||||
|
case ed25519.PublicKey:
|
||||||
|
alg = jose.EdDSA
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
alg = jose.DefaultRSASigAlgorithm
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("unsupported key type %T", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: key}, so)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error creating jose.Signer")
|
||||||
|
}
|
||||||
|
return signer, nil
|
||||||
|
}
|
277
cas/stepcas/x5c_issuer_test.go
Normal file
277
cas/stepcas/x5c_issuer_test.go
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
package stepcas
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
)
|
||||||
|
|
||||||
|
type noneSigner []byte
|
||||||
|
|
||||||
|
func (b noneSigner) Public() crypto.PublicKey {
|
||||||
|
return []byte(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b noneSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
|
||||||
|
return digest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fakeTime(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
tmp := timeNow
|
||||||
|
t.Cleanup(func() {
|
||||||
|
timeNow = tmp
|
||||||
|
})
|
||||||
|
timeNow = func() time.Time {
|
||||||
|
return testX5CCrt.NotBefore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_x5cIssuer_SignToken(t *testing.T) {
|
||||||
|
caURL, err := url.Parse("https://ca.smallstep.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
type fields struct {
|
||||||
|
caURL *url.URL
|
||||||
|
certFile string
|
||||||
|
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, testX5CPath, testX5CKeyPath, "X5C"}, args{"doe", []string{"doe.org"}}, false},
|
||||||
|
{"fail crt", fields{caURL, "", testX5CKeyPath, "X5C"}, args{"doe", []string{"doe.org"}}, true},
|
||||||
|
{"fail key", fields{caURL, testX5CPath, "", "X5C"}, args{"doe", []string{"doe.org"}}, true},
|
||||||
|
{"fail no signer", fields{caURL, testIssKeyPath, testIssPath, "X5C"}, args{"doe", []string{"doe.org"}}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
i := &x5cIssuer{
|
||||||
|
caURL: tt.fields.caURL,
|
||||||
|
certFile: tt.fields.certFile,
|
||||||
|
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("x5cIssuer.SignToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
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#x5c/X5C"},
|
||||||
|
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_x5cIssuer_RevokeToken(t *testing.T) {
|
||||||
|
caURL, err := url.Parse("https://ca.smallstep.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
type fields struct {
|
||||||
|
caURL *url.URL
|
||||||
|
certFile string
|
||||||
|
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, testX5CPath, testX5CKeyPath, "X5C"}, args{"doe"}, false},
|
||||||
|
{"fail crt", fields{caURL, "", testX5CKeyPath, "X5C"}, args{"doe"}, true},
|
||||||
|
{"fail key", fields{caURL, testX5CPath, "", "X5C"}, args{"doe"}, true},
|
||||||
|
{"fail no signer", fields{caURL, testIssKeyPath, testIssPath, "X5C"}, args{"doe"}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
i := &x5cIssuer{
|
||||||
|
caURL: tt.fields.caURL,
|
||||||
|
certFile: tt.fields.certFile,
|
||||||
|
keyFile: tt.fields.keyFile,
|
||||||
|
issuer: tt.fields.issuer,
|
||||||
|
}
|
||||||
|
got, err := i.RevokeToken(tt.args.subject)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("x5cIssuer.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#x5c/X5C"},
|
||||||
|
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_x5cIssuer_Lifetime(t *testing.T) {
|
||||||
|
fakeTime(t)
|
||||||
|
caURL, err := url.Parse("https://ca.smallstep.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With a leeway of 1m the max duration will be 59m.
|
||||||
|
maxDuration := testX5CCrt.NotAfter.Sub(timeNow()) - time.Minute
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
caURL *url.URL
|
||||||
|
certFile string
|
||||||
|
keyFile string
|
||||||
|
issuer string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
d time.Duration
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want time.Duration
|
||||||
|
}{
|
||||||
|
{"ok 0s", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{0}, 0},
|
||||||
|
{"ok 1m", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{time.Minute}, time.Minute},
|
||||||
|
{"ok max-1m", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{maxDuration - time.Minute}, maxDuration - time.Minute},
|
||||||
|
{"ok max", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{maxDuration}, maxDuration},
|
||||||
|
{"ok max+1m", fields{caURL, testX5CPath, testX5CKeyPath, "X5C"}, args{maxDuration + time.Minute}, maxDuration},
|
||||||
|
{"ok fail", fields{caURL, testX5CPath + ".missing", testX5CKeyPath, "X5C"}, args{maxDuration + time.Minute}, maxDuration + time.Minute},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
i := &x5cIssuer{
|
||||||
|
caURL: tt.fields.caURL,
|
||||||
|
certFile: tt.fields.certFile,
|
||||||
|
keyFile: tt.fields.keyFile,
|
||||||
|
issuer: tt.fields.issuer,
|
||||||
|
}
|
||||||
|
if got := i.Lifetime(tt.args.d); got != tt.want {
|
||||||
|
t.Errorf("x5cIssuer.Lifetime() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newJoseSigner(t *testing.T) {
|
||||||
|
mustSigner := func(args ...interface{}) crypto.Signer {
|
||||||
|
if err := args[len(args)-1]; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, a := range args {
|
||||||
|
if s, ok := a.(crypto.Signer); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatal("signer not found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p224 := mustSigner(ecdsa.GenerateKey(elliptic.P224(), rand.Reader))
|
||||||
|
p256 := mustSigner(ecdsa.GenerateKey(elliptic.P256(), rand.Reader))
|
||||||
|
p384 := mustSigner(ecdsa.GenerateKey(elliptic.P384(), rand.Reader))
|
||||||
|
p521 := mustSigner(ecdsa.GenerateKey(elliptic.P521(), rand.Reader))
|
||||||
|
edKey := mustSigner(ed25519.GenerateKey(rand.Reader))
|
||||||
|
rsaKey := mustSigner(rsa.GenerateKey(rand.Reader, 2048))
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
key crypto.Signer
|
||||||
|
so *jose.SignerOptions
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []jose.Header
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"p256", args{p256, nil}, []jose.Header{{Algorithm: "ES256"}}, false},
|
||||||
|
{"p384", args{p384, new(jose.SignerOptions).WithType("JWT")}, []jose.Header{{Algorithm: "ES384", ExtraHeaders: map[jose.HeaderKey]interface{}{"typ": "JWT"}}}, false},
|
||||||
|
{"p521", args{p521, new(jose.SignerOptions).WithHeader("kid", "the-kid")}, []jose.Header{{Algorithm: "ES512", KeyID: "the-kid"}}, false},
|
||||||
|
{"ed25519", args{edKey, nil}, []jose.Header{{Algorithm: "EdDSA"}}, false},
|
||||||
|
{"rsa", args{rsaKey, nil}, []jose.Header{{Algorithm: "RS256"}}, false},
|
||||||
|
{"fail p224", args{p224, nil}, nil, true},
|
||||||
|
{"fail signer", args{noneSigner{1, 2, 3}, nil}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := newJoseSigner(tt.args.key, tt.args.so)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("newJoseSigner() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !tt.wantErr {
|
||||||
|
jws, err := got.Sign([]byte("{}"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("jose.Signer.Sign() err = %v", err)
|
||||||
|
}
|
||||||
|
jwt, err := jose.ParseSigned(jws.FullSerialize())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("jose.ParseSigned() err = %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(jwt.Headers, tt.want) {
|
||||||
|
t.Errorf("jose.Header got = %v, want = %v", jwt.Headers, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ import (
|
||||||
// Enabled cas interfaces.
|
// Enabled cas interfaces.
|
||||||
_ "github.com/smallstep/certificates/cas/cloudcas"
|
_ "github.com/smallstep/certificates/cas/cloudcas"
|
||||||
_ "github.com/smallstep/certificates/cas/softcas"
|
_ "github.com/smallstep/certificates/cas/softcas"
|
||||||
|
_ "github.com/smallstep/certificates/cas/stepcas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// commit and buildTime are filled in during build by the Makefile
|
// commit and buildTime are filled in during build by the Makefile
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -21,7 +21,7 @@ require (
|
||||||
github.com/smallstep/nosql v0.3.6
|
github.com/smallstep/nosql v0.3.6
|
||||||
github.com/urfave/cli v1.22.4
|
github.com/urfave/cli v1.22.4
|
||||||
go.step.sm/cli-utils v0.2.0
|
go.step.sm/cli-utils v0.2.0
|
||||||
go.step.sm/crypto v0.7.3
|
go.step.sm/crypto v0.8.0
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
|
||||||
google.golang.org/api v0.33.0
|
google.golang.org/api v0.33.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -322,8 +322,8 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
go.step.sm/cli-utils v0.2.0 h1:hpVu9+6dpv/7/Bd8nGJFc3V+gQ+TciSJRTu9TavDUQ4=
|
go.step.sm/cli-utils v0.2.0 h1:hpVu9+6dpv/7/Bd8nGJFc3V+gQ+TciSJRTu9TavDUQ4=
|
||||||
go.step.sm/cli-utils v0.2.0/go.mod h1:+t4qCp5NO+080DdGkJxEh3xL5S4TcYC2JTPLMM72b6Y=
|
go.step.sm/cli-utils v0.2.0/go.mod h1:+t4qCp5NO+080DdGkJxEh3xL5S4TcYC2JTPLMM72b6Y=
|
||||||
go.step.sm/crypto v0.6.1/go.mod h1:AKS4yMZVZD4EGjpSkY4eibuMenrvKCscb+BpWMet8c0=
|
go.step.sm/crypto v0.6.1/go.mod h1:AKS4yMZVZD4EGjpSkY4eibuMenrvKCscb+BpWMet8c0=
|
||||||
go.step.sm/crypto v0.7.3 h1:uWkT0vsaZVixgn5x6Ojqittry9PiyVn2ihEYG/qOxV8=
|
go.step.sm/crypto v0.8.0 h1:S4qBPyy3hR7KWLybSkHB0H14pwFfYkom4RZ96JzmXig=
|
||||||
go.step.sm/crypto v0.7.3/go.mod h1:AKS4yMZVZD4EGjpSkY4eibuMenrvKCscb+BpWMet8c0=
|
go.step.sm/crypto v0.8.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
|
Loading…
Reference in a new issue