From a6115e29c294cd9788f6752f7e1c9734e0113e8b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 17 Mar 2021 19:33:35 -0700 Subject: [PATCH 01/16] Add initial implementation of StepCAS. StepCAS allows to configure step-ca as an RA using another step-ca as the main CA. --- authority/config.go | 1 - authority/tls.go | 9 +- cas/apiv1/options.go | 39 ++++++-- cas/apiv1/requests.go | 11 ++- cas/apiv1/services.go | 2 + cas/stepcas/issuer.go | 40 ++++++++ cas/stepcas/stepcas.go | 189 ++++++++++++++++++++++++++++++++++++++ cas/stepcas/x5c_issuer.go | 153 ++++++++++++++++++++++++++++++ cmd/step-ca/main.go | 1 + 9 files changed, 428 insertions(+), 17 deletions(-) create mode 100644 cas/stepcas/issuer.go create mode 100644 cas/stepcas/stepcas.go create mode 100644 cas/stepcas/x5c_issuer.go diff --git a/authority/config.go b/authority/config.go index 9d79ce9a..86a5c80c 100644 --- a/authority/config.go +++ b/authority/config.go @@ -189,7 +189,6 @@ func (c *Config) Validate() error { // Options holds the RA/CAS configuration. ra := c.AuthorityConfig.Options - // The default RA/CAS requires root, crt and key. if ra.Is(cas.SoftCAS) { switch { diff --git a/authority/tls.go b/authority/tls.go index f22f4624..e1c72310 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -148,6 +148,7 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate)) resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{ Template: leaf, + CSR: csr, Lifetime: lifetime, Backdate: signOpts.Backdate, }) @@ -367,9 +368,10 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error // CAS operation, note that SoftCAS (default) is a noop. // The revoke happens when this is stored in the db. _, err = a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{ - Certificate: revokedCert, - Reason: rci.Reason, - ReasonCode: rci.ReasonCode, + Certificate: revokedCert, + SerialNumber: rci.Serial, + Reason: rci.Reason, + ReasonCode: rci.ReasonCode, }) if err != nil { 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{ Template: certTpl, + CSR: cr, Lifetime: 24 * time.Hour, Backdate: 1 * time.Minute, }) diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 46efae3b..4e0b67bc 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -14,17 +14,29 @@ type Options struct { // The type of the CAS to use. Type string `json:"type"` - // Path to the credentials file used in CloudCAS - CredentialsFile string `json:"credentialsFile"` + // CertificateAuthority reference: + // In StepCAS the values 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 - // `projects/*/locations/*/certificateAuthorities/*`. - CertificateAuthority string `json:"certificateAuthority"` + // CertificateAuthorityFingerprint is the root fingerprint used to + // authenticate the connection to the CA when using StepCAS. + 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. - // They are configured in ca.json crt and key properties. - CertificateChain []*x509.Certificate - Signer crypto.Signer `json:"-"` + // CertificateIssuer contains the configuration used in StepCAS. + CertificateIssuer *CertificateIssuer `json:"certificateIssuer,omitempty"` + + // 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:"-"` // IsCreator is set to true when we're creating a certificate authority. Is // used to skip some validations when initializing a CertificateAuthority. @@ -39,6 +51,15 @@ type Options struct { 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"` +} + // Validate checks the fields in Options. func (o *Options) Validate() error { var typ Type diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index 68f1cb7b..bcda66a3 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -53,6 +53,7 @@ const ( // CreateCertificateRequest is the request used to sign a new certificate. type CreateCertificateRequest struct { Template *x509.Certificate + CSR *x509.CertificateRequest Lifetime time.Duration Backdate time.Duration RequestID string @@ -67,6 +68,7 @@ type CreateCertificateResponse struct { // RenewCertificateRequest is the request used to re-sign a certificate. type RenewCertificateRequest struct { Template *x509.Certificate + CSR *x509.CertificateRequest Lifetime time.Duration Backdate time.Duration RequestID string @@ -80,10 +82,11 @@ type RenewCertificateResponse struct { // RevokeCertificateRequest is the request used to revoke a certificate. type RevokeCertificateRequest struct { - Certificate *x509.Certificate - Reason string - ReasonCode int - RequestID string + Certificate *x509.Certificate + SerialNumber string + Reason string + ReasonCode int + RequestID string } // RevokeCertificateResponse is the response to a revoke certificate request. diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 58a8f139..40122601 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -35,6 +35,8 @@ const ( SoftCAS = "softcas" // CloudCAS is a CertificateAuthorityService using Google Cloud CAS. 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 diff --git a/cas/stepcas/issuer.go b/cas/stepcas/issuer.go new file mode 100644 index 00000000..5b9b0ce9 --- /dev/null +++ b/cas/stepcas/issuer.go @@ -0,0 +1,40 @@ +package stepcas + +import ( + "strings" + + "github.com/pkg/errors" + "github.com/smallstep/certificates/cas/apiv1" +) + +// 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) + 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 + } +} diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go new file mode 100644 index 00000000..5f2acc9f --- /dev/null +++ b/cas/stepcas/stepcas.go @@ -0,0 +1,189 @@ +package stepcas + +import ( + "context" + "crypto/x509" + "net/url" + + "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 { + x5c *x5cIssuer + 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") + } + if err := validateCertificateIssuer(opts.CertificateIssuer); err != nil { + return nil, err + } + + // Create client. + client, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint)) + if err != nil { + 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{ + x5c: x5c, + client: client, + fingerprint: opts.CertificateAuthorityFingerprint, + }, nil +} + +func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { + switch { + case req.CSR == nil: + return nil, errors.New("createCertificateRequest `template` cannot be nil") + case req.Lifetime == 0: + return nil, errors.New("createCertificateRequest `lifetime` cannot be 0") + } + + token, err := s.signToken(req.CSR.Subject.CommonName, req.CSR.DNSNames) + if err != nil { + return nil, err + } + + resp, err := s.client.Sign(&api.SignRequest{ + CsrPEM: api.CertificateRequest{CertificateRequest: req.CSR}, + OTT: token, + }) + if err != nil { + return nil, err + } + + var chain []*x509.Certificate + cert := resp.CertChainPEM[0].Certificate + for _, c := range resp.CertChainPEM[1:] { + chain = append(chain, c.Certificate) + } + + return &apiv1.CreateCertificateResponse{ + Certificate: cert, + CertificateChain: chain, + }, nil +} + +func (s *StepCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { + switch { + case req.CSR == nil: + return nil, errors.New("createCertificateRequest `template` cannot be nil") + case req.Lifetime == 0: + return nil, errors.New("createCertificateRequest `lifetime` cannot be 0") + } + + token, err := s.signToken(req.CSR.Subject.CommonName, req.CSR.DNSNames) + if err != nil { + return nil, err + } + + resp, err := s.client.Sign(&api.SignRequest{ + CsrPEM: api.CertificateRequest{CertificateRequest: req.CSR}, + OTT: token, + }) + if err != nil { + return nil, err + } + + var chain []*x509.Certificate + cert := resp.CertChainPEM[0].Certificate + for _, c := range resp.CertChainPEM[1:] { + chain = append(chain, c.Certificate) + } + + return &apiv1.RenewCertificateResponse{ + Certificate: cert, + CertificateChain: chain, + }, nil +} + +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.revokeToken(serialNumber) + if err != nil { + return nil, err + } + + _, err = s.client.Revoke(&api.RevokeRequest{ + Serial: serialNumber, + ReasonCode: req.ReasonCode, + Reason: req.Reason, + OTT: token, + }, 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) 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") +} diff --git a/cas/stepcas/x5c_issuer.go b/cas/stepcas/x5c_issuer.go new file mode 100644 index 00000000..f7e77a3f --- /dev/null +++ b/cas/stepcas/x5c_issuer.go @@ -0,0 +1,153 @@ +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 + +type x5cIssuer struct { + caURL *url.URL + certFile string + keyFile string + issuer 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) + if err != nil { + return nil, err + } + + return &x5cIssuer{ + caURL: caURL, + certFile: cfg.Certificate, + keyFile: cfg.Key, + issuer: cfg.Provisioner, + }, 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) createToken(aud, sub string, sans []string) (string, error) { + signer, err := newX5CSigner(i.certFile, 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 defaultClaims(iss, sub, aud, id string) jose.Claims { + now := time.Now() + 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 newX5CSigner(certFile, 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 + } + certs, err := jose.ValidateX5C(certFile, key) + 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.(type) { + case *ecdsa.PrivateKey: + 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.PrivateKey: + alg = jose.EdDSA + case *rsa.PrivateKey: + 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 +} diff --git a/cmd/step-ca/main.go b/cmd/step-ca/main.go index dad9cdbe..a243022a 100644 --- a/cmd/step-ca/main.go +++ b/cmd/step-ca/main.go @@ -37,6 +37,7 @@ import ( // Enabled cas interfaces. _ "github.com/smallstep/certificates/cas/cloudcas" _ "github.com/smallstep/certificates/cas/softcas" + _ "github.com/smallstep/certificates/cas/stepcas" ) // commit and buildTime are filled in during build by the Makefile From bcf70206ac051874a6605bfe634ae955af40e91f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 17 Mar 2021 19:47:36 -0700 Subject: [PATCH 02/16] Add support for revocation using an extra provisioner in the RA. --- authority/tls.go | 1 + cas/apiv1/requests.go | 1 + cas/stepcas/stepcas.go | 1 + 3 files changed, 3 insertions(+) diff --git a/authority/tls.go b/authority/tls.go index e1c72310..6b46ac54 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -372,6 +372,7 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error SerialNumber: rci.Serial, Reason: rci.Reason, ReasonCode: rci.ReasonCode, + PassiveOnly: revokeOpts.PassiveOnly, }) if err != nil { return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...) diff --git a/cas/apiv1/requests.go b/cas/apiv1/requests.go index bcda66a3..b47a9c13 100644 --- a/cas/apiv1/requests.go +++ b/cas/apiv1/requests.go @@ -86,6 +86,7 @@ type RevokeCertificateRequest struct { SerialNumber string Reason string ReasonCode int + PassiveOnly bool RequestID string } diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 5f2acc9f..1befdc35 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -149,6 +149,7 @@ func (s *StepCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 ReasonCode: req.ReasonCode, Reason: req.Reason, OTT: token, + Passive: req.PassiveOnly, }, nil) if err != nil { return nil, err From dbb48ecf8d2e8926912b76cba6c5860e07483b34 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 18 Mar 2021 18:01:38 -0700 Subject: [PATCH 03/16] Add tests for stepcas. --- cas/stepcas/stepcas_test.go | 668 +++++++++++++++++++++++++++++++++ cas/stepcas/x5c_issuer.go | 8 +- cas/stepcas/x5c_issuer_test.go | 218 +++++++++++ 3 files changed, 890 insertions(+), 4 deletions(-) create mode 100644 cas/stepcas/stepcas_test.go create mode 100644 cas/stepcas/x5c_issuer_test.go diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go new file mode 100644 index 00000000..c10fb5ca --- /dev/null +++ b/cas/stepcas/stepcas_test.go @@ -0,0 +1,668 @@ +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/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 + + 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 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 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 + testCrt, testKey = mustSignCertificate("Test Certificate", []string{"doe.org"}, x509util.DefaultLeafTemplate, testIssCrt, testIssKey) + testCR, err = x509util.CreateCertificateRequest("Test Certificate", []string{"doe.org"}, testKey) + if err != nil { + panic(err) + } + + // CR used in errors. + testFailCR, err = x509util.CreateCertificateRequest("Test Certificate", []string{"fail.doe.org"}, testKey) + if err != nil { + panic(err) + } + + testRootFingerprint = x509util.Fingerprint(testRootCrt) + + path, err := os.MkdirTemp(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) + + 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{ + x5c: &x5cIssuer{ + caURL: caURL, + certFile: testX5CPath, + keyFile: testX5CKeyPath, + issuer: "X5C", + }, + 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 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}, + {"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}, + {"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, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ + Type: "x5c", + Provisioner: "X5C", + Certificate: testX5CPath, + Key: testX5CKeyPath, + }) + if err != nil { + t.Fatal(err) + } + + type fields struct { + x5c *x5cIssuer + 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}, + {"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{nil, 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}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StepCAS{ + x5c: tt.fields.x5c, + 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, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ + Type: "x5c", + Provisioner: "X5C", + Certificate: testX5CPath, + Key: testX5CKeyPath, + }) + if err != nil { + t.Fatal(err) + } + + type fields struct { + x5c *x5cIssuer + client *ca.Client + fingerprint string + } + type args struct { + req *apiv1.RenewCertificateRequest + } + tests := []struct { + name string + fields fields + args args + want *apiv1.RenewCertificateResponse + wantErr bool + }{ + {"ok", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ + CSR: testCR, + Lifetime: time.Hour, + }}, &apiv1.RenewCertificateResponse{ + Certificate: testCrt, + CertificateChain: []*x509.Certificate{testIssCrt}, + }, false}, + {"fail CSR", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ + CSR: nil, + Lifetime: time.Hour, + }}, nil, true}, + {"fail lifetime", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ + CSR: testCR, + Lifetime: 0, + }}, nil, true}, + {"fail sign token", fields{nil, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ + CSR: testCR, + Lifetime: time.Hour, + }}, nil, true}, + {"fail client sign", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ + CSR: testFailCR, + Lifetime: time.Hour, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StepCAS{ + x5c: tt.fields.x5c, + 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, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ + Type: "x5c", + Provisioner: "X5C", + Certificate: testX5CPath, + Key: testX5CKeyPath, + }) + if err != nil { + t.Fatal(err) + } + + type fields struct { + x5c *x5cIssuer + 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}, + {"fail request", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "", + Certificate: nil, + }}, nil, true}, + {"fail revoke token", fields{nil, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "ok", + }}, nil, true}, + {"fail client revoke", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "fail", + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &StepCAS{ + x5c: tt.fields.x5c, + 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, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ + Type: "x5c", + Provisioner: "X5C", + Certificate: testX5CPath, + Key: testX5CKeyPath, + }) + if err != nil { + t.Fatal(err) + } + + type fields struct { + x5c *x5cIssuer + 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}, + {"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{ + x5c: tt.fields.x5c, + 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) + } + }) + } +} diff --git a/cas/stepcas/x5c_issuer.go b/cas/stepcas/x5c_issuer.go index f7e77a3f..8046b8ae 100644 --- a/cas/stepcas/x5c_issuer.go +++ b/cas/stepcas/x5c_issuer.go @@ -125,8 +125,8 @@ func newX5CSigner(certFile, keyFile string) (jose.Signer, error) { func newJoseSigner(key crypto.Signer, so *jose.SignerOptions) (jose.Signer, error) { var alg jose.SignatureAlgorithm - switch k := key.(type) { - case *ecdsa.PrivateKey: + switch k := key.Public().(type) { + case *ecdsa.PublicKey: switch k.Curve.Params().Name { case "P-256": alg = jose.ES256 @@ -137,9 +137,9 @@ func newJoseSigner(key crypto.Signer, so *jose.SignerOptions) (jose.Signer, erro default: return nil, errors.Errorf("unsupported elliptic curve %s", k.Curve.Params().Name) } - case ed25519.PrivateKey: + case ed25519.PublicKey: alg = jose.EdDSA - case *rsa.PrivateKey: + case *rsa.PublicKey: alg = jose.DefaultRSASigAlgorithm default: return nil, errors.Errorf("unsupported key type %T", k) diff --git a/cas/stepcas/x5c_issuer_test.go b/cas/stepcas/x5c_issuer_test.go new file mode 100644 index 00000000..f8972741 --- /dev/null +++ b/cas/stepcas/x5c_issuer_test.go @@ -0,0 +1,218 @@ +package stepcas + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "io" + "net/url" + "reflect" + "testing" + + "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 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_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) + } + } + }) + } +} From 561341a6f2eedbaf589e9432fde44e07a96ce9f9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Thu, 18 Mar 2021 18:04:38 -0700 Subject: [PATCH 04/16] Update go.step.sm/crypto. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b362b2d8..fbc27c5e 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/smallstep/nosql v0.3.6 github.com/urfave/cli v1.22.4 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/net v0.0.0-20210119194325-5f4716e94777 google.golang.org/api v0.33.0 diff --git a/go.sum b/go.sum index e1140a96..37e51dd6 100644 --- a/go.sum +++ b/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/go.mod h1:+t4qCp5NO+080DdGkJxEh3xL5S4TcYC2JTPLMM72b6Y= 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.7.3/go.mod h1:AKS4yMZVZD4EGjpSkY4eibuMenrvKCscb+BpWMet8c0= +go.step.sm/crypto v0.8.0 h1:S4qBPyy3hR7KWLybSkHB0H14pwFfYkom4RZ96JzmXig= +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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= From ae4b8f58b89db8ee172786aba89d34daf47563b5 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 19 Mar 2021 12:02:03 -0700 Subject: [PATCH 05/16] Add support for emails, ips and uris. --- cas/stepcas/stepcas.go | 79 ++++++++++++++++++++++--------------- cas/stepcas/stepcas_test.go | 7 ++-- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 1befdc35..86a995ef 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "net/url" + "time" "github.com/pkg/errors" "github.com/smallstep/certificates/api" @@ -70,25 +71,11 @@ func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 return nil, errors.New("createCertificateRequest `lifetime` cannot be 0") } - token, err := s.signToken(req.CSR.Subject.CommonName, req.CSR.DNSNames) + cert, chain, err := s.createCertificate(req.CSR, req.Lifetime) if err != nil { return nil, err } - resp, err := s.client.Sign(&api.SignRequest{ - CsrPEM: api.CertificateRequest{CertificateRequest: req.CSR}, - OTT: token, - }) - if err != nil { - return nil, err - } - - var chain []*x509.Certificate - cert := resp.CertChainPEM[0].Certificate - for _, c := range resp.CertChainPEM[1:] { - chain = append(chain, c.Certificate) - } - return &apiv1.CreateCertificateResponse{ Certificate: cert, CertificateChain: chain, @@ -98,30 +85,16 @@ func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 func (s *StepCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) { switch { case req.CSR == nil: - return nil, errors.New("createCertificateRequest `template` cannot be nil") + return nil, errors.New("renewCertificateRequest `template` cannot be nil") case req.Lifetime == 0: - return nil, errors.New("createCertificateRequest `lifetime` cannot be 0") + return nil, errors.New("renewCertificateRequest `lifetime` cannot be 0") } - token, err := s.signToken(req.CSR.Subject.CommonName, req.CSR.DNSNames) + cert, chain, err := s.createCertificate(req.CSR, req.Lifetime) if err != nil { return nil, err } - resp, err := s.client.Sign(&api.SignRequest{ - CsrPEM: api.CertificateRequest{CertificateRequest: req.CSR}, - OTT: token, - }) - if err != nil { - return nil, err - } - - var chain []*x509.Certificate - cert := resp.CertChainPEM[0].Certificate - for _, c := range resp.CertChainPEM[1:] { - chain = append(chain, c.Certificate) - } - return &apiv1.RenewCertificateResponse{ Certificate: cert, CertificateChain: chain, @@ -173,6 +146,48 @@ func (s *StepCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequ }, 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)) + for _, s := range cr.DNSNames { + sans = append(sans, s) + } + for _, s := range cr.EmailAddresses { + sans = append(sans, s) + } + 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.signToken(commonName, sans) + if err != nil { + return nil, nil, err + } + + resp, err := s.client.Sign(&api.SignRequest{ + CsrPEM: api.CertificateRequest{CertificateRequest: cr}, + OTT: token, + }) + 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) signToken(subject string, sans []string) (string, error) { if s.x5c != nil { return s.x5c.SignToken(subject, sans) diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go index c10fb5ca..e954c9ff 100644 --- a/cas/stepcas/stepcas_test.go +++ b/cas/stepcas/stepcas_test.go @@ -174,14 +174,15 @@ func TestMain(m *testing.M) { // Final certificate. var err error - testCrt, testKey = mustSignCertificate("Test Certificate", []string{"doe.org"}, x509util.DefaultLeafTemplate, testIssCrt, testIssKey) - testCR, err = x509util.CreateCertificateRequest("Test Certificate", []string{"doe.org"}, testKey) + 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("Test Certificate", []string{"fail.doe.org"}, testKey) + testFailCR, err = x509util.CreateCertificateRequest("", []string{"fail.doe.org"}, testKey) if err != nil { panic(err) } From 7958f6ebb50b263dabbd1b68724d907560caea1e Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 19 Mar 2021 13:19:49 -0700 Subject: [PATCH 06/16] Add support for lifetime. --- cas/stepcas/stepcas.go | 15 +++++++-- cas/stepcas/x5c_issuer.go | 20 +++++++++++- cas/stepcas/x5c_issuer_test.go | 59 ++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 86a995ef..8b468e25 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -172,8 +172,9 @@ func (s *StepCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.D } resp, err := s.client.Sign(&api.SignRequest{ - CsrPEM: api.CertificateRequest{CertificateRequest: cr}, - OTT: token, + CsrPEM: api.CertificateRequest{CertificateRequest: cr}, + OTT: token, + NotAfter: s.lifetime(lifetime), }) if err != nil { return nil, nil, err @@ -203,3 +204,13 @@ func (s *StepCAS) revokeToken(subject string) (string, error) { return "", errors.New("stepCAS does not have any provisioner configured") } + +func (s *StepCAS) lifetime(d time.Duration) api.TimeDuration { + if s.x5c != nil { + d = s.x5c.Lifetime(d) + } + var td api.TimeDuration + td.SetDuration(d) + println(td.String(), d.String()) + return td +} diff --git a/cas/stepcas/x5c_issuer.go b/cas/stepcas/x5c_issuer.go index 8046b8ae..d5d9a0f0 100644 --- a/cas/stepcas/x5c_issuer.go +++ b/cas/stepcas/x5c_issuer.go @@ -17,6 +17,12 @@ import ( 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 certFile string @@ -58,6 +64,18 @@ func (i *x5cIssuer) RevokeToken(subject string) (string, error) { 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) if err != nil { @@ -86,7 +104,7 @@ func (i *x5cIssuer) createToken(aud, sub string, sans []string) (string, error) } func defaultClaims(iss, sub, aud, id string) jose.Claims { - now := time.Now() + now := timeNow() return jose.Claims{ ID: id, Issuer: iss, diff --git a/cas/stepcas/x5c_issuer_test.go b/cas/stepcas/x5c_issuer_test.go index f8972741..a3190255 100644 --- a/cas/stepcas/x5c_issuer_test.go +++ b/cas/stepcas/x5c_issuer_test.go @@ -11,6 +11,7 @@ import ( "net/url" "reflect" "testing" + "time" "go.step.sm/crypto/jose" ) @@ -25,6 +26,17 @@ func (b noneSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) 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 { @@ -154,6 +166,53 @@ func Test_x5cIssuer_RevokeToken(t *testing.T) { } } +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 { From 6fd6270e7d5321ef490d3d6177581e163b0ace75 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 19 Mar 2021 13:21:14 -0700 Subject: [PATCH 07/16] Remove debug statements. --- cas/cloudcas/cloudcas.go | 1 - cas/stepcas/stepcas.go | 1 - 2 files changed, 2 deletions(-) diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index 4fb75b3a..695258c9 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -141,7 +141,6 @@ func (c *CloudCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityReq Name: name, }) if err != nil { - println(name) return nil, errors.Wrap(err, "cloudCAS GetCertificateAuthority failed") } if len(resp.PemCaCertificates) == 0 { diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 8b468e25..5cb8fd93 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -211,6 +211,5 @@ func (s *StepCAS) lifetime(d time.Duration) api.TimeDuration { } var td api.TimeDuration td.SetDuration(d) - println(td.String(), d.String()) return td } From 08e75b614e210761653986aaa9db3e5aa0f069a9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 19 Mar 2021 13:23:32 -0700 Subject: [PATCH 08/16] Do not depend on Go 1.16. --- cas/stepcas/stepcas_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go index e954c9ff..998b574e 100644 --- a/cas/stepcas/stepcas_test.go +++ b/cas/stepcas/stepcas_test.go @@ -189,7 +189,7 @@ func TestMain(m *testing.M) { testRootFingerprint = x509util.Fingerprint(testRootCrt) - path, err := os.MkdirTemp(os.TempDir(), "stepcas") + path, err := ioutil.TempDir(os.TempDir(), "stepcas") if err != nil { panic(err) } From e7a6c46e54503bd10d4686d6e0feaa13de7708ee Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Fri, 19 Mar 2021 14:21:47 -0700 Subject: [PATCH 09/16] Fix linting errors. --- cas/stepcas/stepcas.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 5cb8fd93..d9171089 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -148,12 +148,8 @@ func (s *StepCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequ 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)) - for _, s := range cr.DNSNames { - sans = append(sans, s) - } - for _, s := range cr.EmailAddresses { - sans = append(sans, s) - } + sans = append(sans, cr.DNSNames...) + sans = append(sans, cr.EmailAddresses...) for _, ip := range cr.IPAddresses { sans = append(sans, ip.String()) } From 348815f4f61dee1c7f70146592a72f96823418d9 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 22 Mar 2021 11:51:11 -0700 Subject: [PATCH 10/16] Fix error message. --- cas/stepcas/stepcas.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index d9171089..81ef5a7e 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -66,7 +66,7 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) { func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) { switch { case req.CSR == nil: - return nil, errors.New("createCertificateRequest `template` cannot be nil") + return nil, errors.New("createCertificateRequest `csr` cannot be nil") case req.Lifetime == 0: return nil, errors.New("createCertificateRequest `lifetime` cannot be 0") } From 96de4e6ec8fe98c930ae70c91f888fe1f3a52861 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 22 Mar 2021 12:56:12 -0700 Subject: [PATCH 11/16] Return a non-implemented error in stepcas.RenewCertificate. --- cas/apiv1/services.go | 20 ++++++++++++++ cas/apiv1/services_test.go | 52 ++++++++++++++++++++++++++++++++++++- cas/stepcas/stepcas.go | 21 ++++----------- cas/stepcas/stepcas_test.go | 21 +-------------- 4 files changed, 77 insertions(+), 37 deletions(-) diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index 40122601..c47f778d 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -1,6 +1,7 @@ package apiv1 import ( + "net/http" "strings" ) @@ -48,3 +49,22 @@ func (t Type) String() string { } return strings.ToLower(string(t)) } + +// ErrNotImplemented is the type of error returned if an operation is not +// implemented. +type ErrNotImplemented struct { + Message string +} + +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 (s ErrNotImplemented) StatusCode() int { + return http.StatusNotImplemented +} diff --git a/cas/apiv1/services_test.go b/cas/apiv1/services_test.go index f9ab9042..eb7d502e 100644 --- a/cas/apiv1/services_test.go +++ b/cas/apiv1/services_test.go @@ -1,6 +1,8 @@ package apiv1 -import "testing" +import ( + "testing" +) func TestType_String(t *testing.T) { 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) + } + }) + } +} diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 81ef5a7e..6a20468f 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -63,6 +63,8 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) { }, 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: @@ -82,23 +84,10 @@ func (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1 }, 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) { - switch { - case req.CSR == nil: - return nil, errors.New("renewCertificateRequest `template` cannot be nil") - case req.Lifetime == 0: - return nil, errors.New("renewCertificateRequest `lifetime` cannot be 0") - } - - cert, chain, err := s.createCertificate(req.CSR, req.Lifetime) - if err != nil { - return nil, err - } - - return &apiv1.RenewCertificateResponse{ - Certificate: cert, - CertificateChain: chain, - }, nil + return nil, apiv1.ErrNotImplemented{Message: "stepCAS does not support mTLS renewals"} } func (s *StepCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go index 998b574e..b4989478 100644 --- a/cas/stepcas/stepcas_test.go +++ b/cas/stepcas/stepcas_test.go @@ -497,28 +497,9 @@ func TestStepCAS_RenewCertificate(t *testing.T) { want *apiv1.RenewCertificateResponse wantErr bool }{ - {"ok", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ + {"not implemented", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ CSR: testCR, Lifetime: time.Hour, - }}, &apiv1.RenewCertificateResponse{ - Certificate: testCrt, - CertificateChain: []*x509.Certificate{testIssCrt}, - }, false}, - {"fail CSR", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ - CSR: nil, - Lifetime: time.Hour, - }}, nil, true}, - {"fail lifetime", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ - CSR: testCR, - Lifetime: 0, - }}, nil, true}, - {"fail sign token", fields{nil, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ - CSR: testCR, - Lifetime: time.Hour, - }}, nil, true}, - {"fail client sign", fields{x5c, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{ - CSR: testFailCR, - Lifetime: time.Hour, }}, nil, true}, } for _, tt := range tests { From 0b8528ce6b47b53ce0f08f5edde447fd2ec95ec2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 22 Mar 2021 13:37:31 -0700 Subject: [PATCH 12/16] Allow mTLS revocation without provisioner. --- authority/tls.go | 11 +++++------ authority/tls_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/authority/tls.go b/authority/tls.go index 6b46ac54..c848d188 100644 --- a/authority/tls.go +++ b/authority/tls.go @@ -334,22 +334,21 @@ func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error if !ok { return errs.InternalServer("authority.Revoke; provisioner not found", opts...) } + rci.ProvisionerID = p.GetID() rci.TokenID, err = p.GetTokenID(revokeOpts.OTT) if err != nil { return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke; could not get ID for token") } + opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID)) opts = append(opts, errs.WithKeyVal("tokenID", rci.TokenID)) } else { // Load the Certificate provisioner if one exists. - p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt) - if err != nil { - return errs.Wrap(http.StatusUnauthorized, err, - "authority.Revoke: unable to load certificate provisioner", opts...) + if p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil { + rci.ProvisionerID = p.GetID() + opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID)) } } - rci.ProvisionerID = p.GetID() - opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID)) if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod { err = a.db.RevokeSSH(rci) diff --git a/authority/tls_test.go b/authority/tls_test.go index bf629a0d..4c936f0c 100644 --- a/authority/tls_test.go +++ b/authority/tls_test.go @@ -1231,6 +1231,30 @@ func TestAuthority_Revoke(t *testing.T) { crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt") 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{ auth: _a, opts: &RevokeOptions{ From ce3e6bfdf6b722348b88fbad6c5b98d5e4871364 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 22 Mar 2021 13:45:20 -0700 Subject: [PATCH 13/16] Fix linting errors. --- cas/apiv1/services.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cas/apiv1/services.go b/cas/apiv1/services.go index c47f778d..d4dd3c8c 100644 --- a/cas/apiv1/services.go +++ b/cas/apiv1/services.go @@ -56,6 +56,7 @@ type ErrNotImplemented struct { Message string } +// ErrNotImplemented implements the error interface. func (e ErrNotImplemented) Error() string { if e.Message != "" { return e.Message @@ -65,6 +66,6 @@ func (e ErrNotImplemented) Error() string { // StatusCode implements the StatusCoder interface and returns the HTTP 501 // error. -func (s ErrNotImplemented) StatusCode() int { +func (e ErrNotImplemented) StatusCode() int { return http.StatusNotImplemented } From 80542d6d9a827d50d44e594cbe08b389923bbf78 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 23 Mar 2021 16:14:49 -0700 Subject: [PATCH 14/16] Add JWK as an issuer for stepcas. --- cas/stepcas/issuer.go | 38 ++++++++ cas/stepcas/issuer_test.go | 80 +++++++++++++++ cas/stepcas/jwk_issuer.go | 96 ++++++++++++++++++ cas/stepcas/jwk_issuer_test.go | 172 +++++++++++++++++++++++++++++++++ cas/stepcas/stepcas.go | 40 ++------ cas/stepcas/stepcas_test.go | 169 +++++++++++++++++++++++--------- 6 files changed, 517 insertions(+), 78 deletions(-) create mode 100644 cas/stepcas/issuer_test.go create mode 100644 cas/stepcas/jwk_issuer.go create mode 100644 cas/stepcas/jwk_issuer_test.go diff --git a/cas/stepcas/issuer.go b/cas/stepcas/issuer.go index 5b9b0ce9..9289709e 100644 --- a/cas/stepcas/issuer.go +++ b/cas/stepcas/issuer.go @@ -1,12 +1,36 @@ 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 { @@ -20,6 +44,8 @@ func validateCertificateIssuer(iss *apiv1.CertificateIssuer) error { 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) } @@ -38,3 +64,15 @@ func validateX5CIssuer(iss *apiv1.CertificateIssuer) error { 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 + } +} diff --git a/cas/stepcas/issuer_test.go b/cas/stepcas/issuer_test.go new file mode 100644 index 00000000..743d6eec --- /dev/null +++ b/cas/stepcas/issuer_test.go @@ -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) + } + }) + } +} diff --git a/cas/stepcas/jwk_issuer.go b/cas/stepcas/jwk_issuer.go new file mode 100644 index 00000000..36b5f488 --- /dev/null +++ b/cas/stepcas/jwk_issuer.go @@ -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) +} diff --git a/cas/stepcas/jwk_issuer_test.go b/cas/stepcas/jwk_issuer_test.go new file mode 100644 index 00000000..2caccf25 --- /dev/null +++ b/cas/stepcas/jwk_issuer_test.go @@ -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) + } + }) + } +} diff --git a/cas/stepcas/stepcas.go b/cas/stepcas/stepcas.go index 6a20468f..7dc01e5a 100644 --- a/cas/stepcas/stepcas.go +++ b/cas/stepcas/stepcas.go @@ -21,7 +21,7 @@ func init() { // StepCAS implements the cas.CertificateAuthorityService interface using // another step-ca instance. type StepCAS struct { - x5c *x5cIssuer + iss stepIssuer client *ca.Client fingerprint string } @@ -40,7 +40,10 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) { if err != nil { 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 } @@ -50,14 +53,8 @@ func New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) { 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{ - x5c: x5c, + iss: iss, client: client, fingerprint: opts.CertificateAuthorityFingerprint, }, nil @@ -101,7 +98,7 @@ func (s *StepCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1 serialNumber = req.Certificate.SerialNumber.String() } - token, err := s.revokeToken(serialNumber) + token, err := s.iss.RevokeToken(serialNumber) if err != nil { return nil, err } @@ -151,7 +148,7 @@ func (s *StepCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.D commonName = sans[0] } - token, err := s.signToken(commonName, sans) + token, err := s.iss.SignToken(commonName, sans) if err != nil { return nil, nil, err } @@ -174,27 +171,8 @@ func (s *StepCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.D 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 { - if s.x5c != nil { - d = s.x5c.Lifetime(d) - } var td api.TimeDuration - td.SetDuration(d) + td.SetDuration(s.iss.Lifetime(d)) return td } diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go index b4989478..3a25927f 100644 --- a/cas/stepcas/stepcas_test.go +++ b/cas/stepcas/stepcas_test.go @@ -167,6 +167,33 @@ func testCAHelper(t *testing.T) (*url.URL, *ca.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) { testRootCrt, testRootKey = mustSignCertificate("Test Root Certificate", nil, x509util.DefaultRootTemplate, nil, nil) testIssCrt, testIssKey = mustSignCertificate("Test Intermediate Certificate", nil, x509util.DefaultIntermediateTemplate, testRootCrt, testRootKey) @@ -258,7 +285,7 @@ func TestNew(t *testing.T) { Key: testX5CKeyPath, }, }}, &StepCAS{ - x5c: &x5cIssuer{ + iss: &x5cIssuer{ caURL: caURL, certFile: testX5CPath, keyFile: testX5CKeyPath, @@ -267,6 +294,23 @@ func TestNew(t *testing.T) { 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, @@ -307,6 +351,15 @@ func TestNew(t *testing.T) { 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, @@ -327,6 +380,15 @@ func TestNew(t *testing.T) { 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, @@ -367,6 +429,15 @@ func TestNew(t *testing.T) { 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, @@ -402,18 +473,11 @@ func TestNew(t *testing.T) { func TestStepCAS_CreateCertificate(t *testing.T) { caURL, client := testCAHelper(t) - x5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ - Type: "x5c", - Provisioner: "X5C", - Certificate: testX5CPath, - Key: testX5CKeyPath, - }) - if err != nil { - t.Fatal(err) - } + x5c := testX5CIssuer(t, caURL) + jwk := testJWKIssuer(t, caURL) type fields struct { - x5c *x5cIssuer + iss stepIssuer client *ca.Client fingerprint string } @@ -434,6 +498,13 @@ func TestStepCAS_CreateCertificate(t *testing.T) { 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}, {"fail CSR", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ CSR: nil, Lifetime: time.Hour, @@ -442,7 +513,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) { CSR: testCR, Lifetime: 0, }}, nil, true}, - {"fail sign token", fields{nil, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ + {"fail sign token", fields{mockErrIssuer{}, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ CSR: testCR, Lifetime: time.Hour, }}, nil, true}, @@ -454,7 +525,7 @@ func TestStepCAS_CreateCertificate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &StepCAS{ - x5c: tt.fields.x5c, + iss: tt.fields.iss, client: tt.fields.client, fingerprint: tt.fields.fingerprint, } @@ -472,18 +543,11 @@ func TestStepCAS_CreateCertificate(t *testing.T) { func TestStepCAS_RenewCertificate(t *testing.T) { caURL, client := testCAHelper(t) - x5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ - Type: "x5c", - Provisioner: "X5C", - Certificate: testX5CPath, - Key: testX5CKeyPath, - }) - if err != nil { - t.Fatal(err) - } + x5c := testX5CIssuer(t, caURL) + jwk := testJWKIssuer(t, caURL) type fields struct { - x5c *x5cIssuer + iss stepIssuer client *ca.Client fingerprint string } @@ -501,11 +565,15 @@ func TestStepCAS_RenewCertificate(t *testing.T) { 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{ - x5c: tt.fields.x5c, + iss: tt.fields.iss, client: tt.fields.client, fingerprint: tt.fields.fingerprint, } @@ -523,18 +591,11 @@ func TestStepCAS_RenewCertificate(t *testing.T) { func TestStepCAS_RevokeCertificate(t *testing.T) { caURL, client := testCAHelper(t) - x5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ - Type: "x5c", - Provisioner: "X5C", - Certificate: testX5CPath, - Key: testX5CKeyPath, - }) - if err != nil { - t.Fatal(err) - } + x5c := testX5CIssuer(t, caURL) + jwk := testJWKIssuer(t, caURL) type fields struct { - x5c *x5cIssuer + iss stepIssuer client *ca.Client fingerprint string } @@ -564,11 +625,27 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { }}, &apiv1.RevokeCertificateResponse{ Certificate: testCrt, }, 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{ SerialNumber: "", Certificate: nil, }}, nil, true}, - {"fail revoke token", fields{nil, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + {"fail revoke token", fields{mockErrIssuer{}, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ SerialNumber: "ok", }}, nil, true}, {"fail client revoke", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ @@ -578,7 +655,7 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &StepCAS{ - x5c: tt.fields.x5c, + iss: tt.fields.iss, client: tt.fields.client, fingerprint: tt.fields.fingerprint, } @@ -596,18 +673,11 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { func TestStepCAS_GetCertificateAuthority(t *testing.T) { caURL, client := testCAHelper(t) - x5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ - Type: "x5c", - Provisioner: "X5C", - Certificate: testX5CPath, - Key: testX5CKeyPath, - }) - if err != nil { - t.Fatal(err) - } + x5c := testX5CIssuer(t, caURL) + jwk := testJWKIssuer(t, caURL) type fields struct { - x5c *x5cIssuer + iss stepIssuer client *ca.Client fingerprint string } @@ -626,6 +696,11 @@ func TestStepCAS_GetCertificateAuthority(t *testing.T) { }}, &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}, @@ -633,7 +708,7 @@ func TestStepCAS_GetCertificateAuthority(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &StepCAS{ - x5c: tt.fields.x5c, + iss: tt.fields.iss, client: tt.fields.client, fingerprint: tt.fields.fingerprint, } From edc7c4d90efe28cae382adb7dc5add1cffd1ba5f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 23 Mar 2021 17:54:42 -0700 Subject: [PATCH 15/16] Add support for password encrypted files --- cas/apiv1/options.go | 1 + cas/stepcas/jwk_issuer.go | 28 ++++----- cas/stepcas/stepcas_test.go | 116 ++++++++++++++++++++++++++++++------ cas/stepcas/x5c_issuer.go | 28 ++++++--- 4 files changed, 133 insertions(+), 40 deletions(-) diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 4e0b67bc..19169398 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -58,6 +58,7 @@ type CertificateIssuer struct { Provisioner string `json:"provisioner,omitempty"` Certificate string `json:"crt,omitempty"` Key string `json:"key,omitempty"` + Password string `json:"password,omitempty"` } // Validate checks the fields in Options. diff --git a/cas/stepcas/jwk_issuer.go b/cas/stepcas/jwk_issuer.go index 36b5f488..9a4cd7d1 100644 --- a/cas/stepcas/jwk_issuer.go +++ b/cas/stepcas/jwk_issuer.go @@ -1,33 +1,33 @@ package stepcas import ( - "crypto" "net/url" "time" "github.com/pkg/errors" "github.com/smallstep/certificates/cas/apiv1" "go.step.sm/crypto/jose" - "go.step.sm/crypto/pemutil" "go.step.sm/crypto/randutil" ) type jwkIssuer struct { - caURL *url.URL - keyFile string - issuer string + caURL *url.URL + issuer string + keyFile string + password string } func newJWKIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) { - _, err := newJWKSigner(cfg.Key) + _, err := newJWKSigner(cfg.Key, cfg.Password) if err != nil { return nil, err } return &jwkIssuer{ - caURL: caURL, - keyFile: cfg.Key, - issuer: cfg.Provisioner, + caURL: caURL, + issuer: cfg.Provisioner, + keyFile: cfg.Key, + password: cfg.Password, }, nil } @@ -50,7 +50,7 @@ func (i *jwkIssuer) Lifetime(d time.Duration) time.Duration { } func (i *jwkIssuer) createToken(aud, sub string, sans []string) (string, error) { - signer, err := newJWKSigner(i.keyFile) + signer, err := newJWKSigner(i.keyFile, i.password) if err != nil { return "", err } @@ -76,15 +76,11 @@ func (i *jwkIssuer) createToken(aud, sub string, sans []string) (string, error) return tok, nil } -func newJWKSigner(keyFile string) (jose.Signer, error) { - key, err := pemutil.Read(keyFile) +func newJWKSigner(keyFile, password string) (jose.Signer, error) { + signer, err := readKey(keyFile, password) if err != nil { return nil, err } - signer, ok := key.(crypto.Signer) - if !ok { - return nil, errors.New("key is not a crypto.Signer") - } kid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()}) if err != nil { return nil, err diff --git a/cas/stepcas/stepcas_test.go b/cas/stepcas/stepcas_test.go index 3a25927f..88cbb227 100644 --- a/cas/stepcas/stepcas_test.go +++ b/cas/stepcas/stepcas_test.go @@ -23,6 +23,8 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/cas/apiv1" + "go.step.sm/crypto/pemutil" + "go.step.sm/crypto/randutil" "go.step.sm/crypto/x509util" ) @@ -36,9 +38,10 @@ var ( testIssKey crypto.Signer testIssPath, testIssKeyPath string - testX5CCrt *x509.Certificate - testX5CKey crypto.Signer - testX5CPath, testX5CKeyPath string + testX5CCrt *x509.Certificate + testX5CKey crypto.Signer + testX5CPath, testX5CKeyPath string + testPassword, testEncryptedKeyPath string testCR *x509.CertificateRequest testCrt *x509.Certificate @@ -104,6 +107,16 @@ func mustSerializeKey(filename string, key crypto.Signer) { } } +func mustEncryptKey(filename string, key crypto.Signer) { + _, err := pemutil.Serialize(key, + pemutil.ToFile(filename, 0600), + pemutil.WithPKCS8(true), + pemutil.WithPassword([]byte(testPassword))) + if err != nil { + panic(err) + } +} + func testCAHelper(t *testing.T) (*url.URL, *ca.Client) { t.Helper() @@ -167,31 +180,45 @@ func testCAHelper(t *testing.T) (*url.URL, *ca.Client) { return u, client } -func testX5CIssuer(t *testing.T, caURL *url.URL) *x5cIssuer { +func testX5CIssuer(t *testing.T, caURL *url.URL, password string) *x5cIssuer { t.Helper() + key, givenPassword := testX5CKeyPath, password + if password != "" { + key = testEncryptedKeyPath + password = testPassword + } x5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{ Type: "x5c", Provisioner: "X5C", Certificate: testX5CPath, - Key: testX5CKeyPath, + Key: key, + Password: password, }) if err != nil { t.Fatal(err) } + x5c.password = givenPassword return x5c } -func testJWKIssuer(t *testing.T, caURL *url.URL) *jwkIssuer { +func testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer { t.Helper() - x5c, err := newJWKIssuer(caURL, &apiv1.CertificateIssuer{ + key, givenPassword := testX5CKeyPath, password + if password != "" { + key = testEncryptedKeyPath + password = testPassword + } + jwk, err := newJWKIssuer(caURL, &apiv1.CertificateIssuer{ Type: "jwk", Provisioner: "ra@doe.org", - Key: testX5CKeyPath, + Key: key, + Password: password, }) if err != nil { t.Fatal(err) } - return x5c + jwk.password = givenPassword + return jwk } func TestMain(m *testing.M) { @@ -214,6 +241,12 @@ func TestMain(m *testing.M) { panic(err) } + // Password used to encrypto the key + testPassword, err = randutil.Hex(32) + if err != nil { + panic(err) + } + testRootFingerprint = x509util.Fingerprint(testRootCrt) path, err := ioutil.TempDir(os.TempDir(), "stepcas") @@ -236,6 +269,9 @@ func TestMain(m *testing.M) { mustSerializeCrt(testX5CPath, testX5CCrt, testIssCrt) mustSerializeKey(testX5CKeyPath, testX5CKey) + testEncryptedKeyPath = filepath.Join(path, "x5c.enc.key") + mustEncryptKey(testEncryptedKeyPath, testX5CKey) + code := m.Run() if err := os.RemoveAll(path); err != nil { panic(err) @@ -473,8 +509,12 @@ func TestNew(t *testing.T) { func TestStepCAS_CreateCertificate(t *testing.T) { caURL, client := testCAHelper(t) - x5c := testX5CIssuer(t, caURL) - jwk := testJWKIssuer(t, caURL) + x5c := testX5CIssuer(t, caURL, "") + jwk := testJWKIssuer(t, caURL, "") + x5cEnc := testX5CIssuer(t, caURL, testPassword) + jwkEnc := testJWKIssuer(t, caURL, testPassword) + x5cBad := testX5CIssuer(t, caURL, "bad-password") + jwkBad := testJWKIssuer(t, caURL, "bad-password") type fields struct { iss stepIssuer @@ -498,6 +538,13 @@ func TestStepCAS_CreateCertificate(t *testing.T) { Certificate: testCrt, CertificateChain: []*x509.Certificate{testIssCrt}, }, false}, + {"ok with password", fields{x5cEnc, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ + CSR: testCR, + Lifetime: time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: testCrt, + CertificateChain: []*x509.Certificate{testIssCrt}, + }, false}, {"ok jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ CSR: testCR, Lifetime: time.Hour, @@ -505,6 +552,13 @@ func TestStepCAS_CreateCertificate(t *testing.T) { Certificate: testCrt, CertificateChain: []*x509.Certificate{testIssCrt}, }, false}, + {"ok jwk with password", fields{jwkEnc, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ + CSR: testCR, + Lifetime: time.Hour, + }}, &apiv1.CreateCertificateResponse{ + Certificate: testCrt, + CertificateChain: []*x509.Certificate{testIssCrt}, + }, false}, {"fail CSR", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ CSR: nil, Lifetime: time.Hour, @@ -521,6 +575,14 @@ func TestStepCAS_CreateCertificate(t *testing.T) { CSR: testFailCR, Lifetime: time.Hour, }}, nil, true}, + {"fail password", fields{x5cBad, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ + CSR: testCR, + Lifetime: time.Hour, + }}, nil, true}, + {"fail jwk password", fields{jwkBad, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{ + CSR: testCR, + Lifetime: time.Hour, + }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -543,8 +605,8 @@ func TestStepCAS_CreateCertificate(t *testing.T) { func TestStepCAS_RenewCertificate(t *testing.T) { caURL, client := testCAHelper(t) - x5c := testX5CIssuer(t, caURL) - jwk := testJWKIssuer(t, caURL) + x5c := testX5CIssuer(t, caURL, "") + jwk := testJWKIssuer(t, caURL, "") type fields struct { iss stepIssuer @@ -591,8 +653,12 @@ func TestStepCAS_RenewCertificate(t *testing.T) { func TestStepCAS_RevokeCertificate(t *testing.T) { caURL, client := testCAHelper(t) - x5c := testX5CIssuer(t, caURL) - jwk := testJWKIssuer(t, caURL) + x5c := testX5CIssuer(t, caURL, "") + jwk := testJWKIssuer(t, caURL, "") + x5cEnc := testX5CIssuer(t, caURL, testPassword) + jwkEnc := testJWKIssuer(t, caURL, testPassword) + x5cBad := testX5CIssuer(t, caURL, "bad-password") + jwkBad := testJWKIssuer(t, caURL, "bad-password") type fields struct { iss stepIssuer @@ -625,6 +691,10 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { }}, &apiv1.RevokeCertificateResponse{ Certificate: testCrt, }, false}, + {"ok with password", fields{x5cEnc, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "ok", + Certificate: nil, + }}, &apiv1.RevokeCertificateResponse{}, false}, {"ok serial number jwk", fields{jwk, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ SerialNumber: "ok", Certificate: nil, @@ -641,6 +711,10 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { }}, &apiv1.RevokeCertificateResponse{ Certificate: testCrt, }, false}, + {"ok jwk with password", fields{jwkEnc, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "ok", + Certificate: nil, + }}, &apiv1.RevokeCertificateResponse{}, false}, {"fail request", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ SerialNumber: "", Certificate: nil, @@ -651,6 +725,14 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { {"fail client revoke", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ SerialNumber: "fail", }}, nil, true}, + {"fail password", fields{x5cBad, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "ok", + Certificate: nil, + }}, nil, true}, + {"fail jwk password", fields{jwkBad, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "ok", + Certificate: nil, + }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -673,8 +755,8 @@ func TestStepCAS_RevokeCertificate(t *testing.T) { func TestStepCAS_GetCertificateAuthority(t *testing.T) { caURL, client := testCAHelper(t) - x5c := testX5CIssuer(t, caURL) - jwk := testJWKIssuer(t, caURL) + x5c := testX5CIssuer(t, caURL, "") + jwk := testJWKIssuer(t, caURL, "") type fields struct { iss stepIssuer diff --git a/cas/stepcas/x5c_issuer.go b/cas/stepcas/x5c_issuer.go index d5d9a0f0..da4aa27e 100644 --- a/cas/stepcas/x5c_issuer.go +++ b/cas/stepcas/x5c_issuer.go @@ -25,24 +25,26 @@ var timeNow = func() time.Time { type x5cIssuer struct { caURL *url.URL + issuer string certFile string keyFile string - issuer string + password string } // newX5CIssuer create a new x5c token issuer. The given configuration should be // already validate. func newX5CIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*x5cIssuer, error) { - _, err := newX5CSigner(cfg.Certificate, cfg.Key) + _, err := newX5CSigner(cfg.Certificate, cfg.Key, cfg.Password) if err != nil { return nil, err } return &x5cIssuer{ caURL: caURL, + issuer: cfg.Provisioner, certFile: cfg.Certificate, keyFile: cfg.Key, - issuer: cfg.Provisioner, + password: cfg.Password, }, nil } @@ -77,7 +79,7 @@ func (i *x5cIssuer) Lifetime(d time.Duration) time.Duration { } func (i *x5cIssuer) createToken(aud, sub string, sans []string) (string, error) { - signer, err := newX5CSigner(i.certFile, i.keyFile) + signer, err := newX5CSigner(i.certFile, i.keyFile, i.password) if err != nil { return "", err } @@ -116,8 +118,12 @@ func defaultClaims(iss, sub, aud, id string) jose.Claims { } } -func newX5CSigner(certFile, keyFile string) (jose.Signer, error) { - key, err := pemutil.Read(keyFile) +func readKey(keyFile, password string) (crypto.Signer, error) { + var opts []pemutil.Options + if password != "" { + opts = append(opts, pemutil.WithPassword([]byte(password))) + } + key, err := pemutil.Read(keyFile, opts...) if err != nil { return nil, err } @@ -125,11 +131,19 @@ func newX5CSigner(certFile, keyFile string) (jose.Signer, error) { if !ok { return nil, errors.New("key is not a crypto.Signer") } + return signer, nil +} + +func newX5CSigner(certFile, keyFile, password string) (jose.Signer, error) { + signer, err := readKey(keyFile, password) + if err != nil { + return nil, err + } kid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()}) if err != nil { return nil, err } - certs, err := jose.ValidateX5C(certFile, key) + certs, err := jose.ValidateX5C(certFile, signer) if err != nil { return nil, errors.Wrap(err, "error validating x5c certificate chain and key") } From d9f93ccfde96e63427c4ae0ec0390f86bcaa680b Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 24 Mar 2021 12:06:29 -0700 Subject: [PATCH 16/16] Fix typo. --- cas/apiv1/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cas/apiv1/options.go b/cas/apiv1/options.go index 19169398..4810d1f3 100644 --- a/cas/apiv1/options.go +++ b/cas/apiv1/options.go @@ -15,7 +15,7 @@ type Options struct { Type string `json:"type"` // CertificateAuthority reference: - // In StepCAS the values is the CA url, e.g. "https://ca.smallstep.com:9000". + // 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"`