Introduce generalized statusCoder errors and loads of ssh unit tests.

* StatusCoder api errors that have friendly user messages.
* Unit tests for SSH sign/renew/rekey/revoke across all provisioners.
This commit is contained in:
max furman 2019-12-20 13:30:05 -08:00
parent 3ce267cdd6
commit c387b21808
75 changed files with 5292 additions and 2201 deletions

View file

@ -5,7 +5,6 @@ import (
"crypto/dsa"
"crypto/ecdsa"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
@ -209,14 +208,6 @@ type RootResponse struct {
RootPEM Certificate `json:"ca"`
}
// SignRequest is the request body for a certificate signature request.
type SignRequest struct {
CsrPEM CertificateRequest `json:"csr"`
OTT string `json:"ott"`
NotAfter TimeDuration `json:"notAfter"`
NotBefore TimeDuration `json:"notBefore"`
}
// ProvisionersResponse is the response object that returns the list of
// provisioners.
type ProvisionersResponse struct {
@ -230,31 +221,6 @@ type ProvisionerKeyResponse struct {
Key string `json:"key"`
}
// Validate checks the fields of the SignRequest and returns nil if they are ok
// or an error if something is wrong.
func (s *SignRequest) Validate() error {
if s.CsrPEM.CertificateRequest == nil {
return errs.BadRequest(errors.New("missing csr"))
}
if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil {
return errs.BadRequest(errors.Wrap(err, "invalid csr"))
}
if s.OTT == "" {
return errs.BadRequest(errors.New("missing ott"))
}
return nil
}
// SignResponse is the response object of the certificate signature request.
type SignResponse struct {
ServerPEM Certificate `json:"crt"`
CaPEM Certificate `json:"ca"`
CertChainPEM []Certificate `json:"certChain"`
TLSOptions *tlsutil.TLSOptions `json:"tlsOptions,omitempty"`
TLS *tls.ConnectionState `json:"-"`
}
// RootsResponse is the response object of the roots request.
type RootsResponse struct {
Certificates []Certificate `json:"crts"`
@ -344,80 +310,6 @@ func certChainToPEM(certChain []*x509.Certificate) []Certificate {
return certChainPEM
}
// Sign is an HTTP handler that reads a certificate request and an
// one-time-token (ott) from the body and creates a new certificate with the
// information in the certificate request.
func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) {
var body SignRequest
if err := ReadJSON(r.Body, &body); err != nil {
WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body")))
return
}
logOtt(w, body.OTT)
if err := body.Validate(); err != nil {
WriteError(w, err)
return
}
opts := provisioner.Options{
NotBefore: body.NotBefore,
NotAfter: body.NotAfter,
}
signOpts, err := h.Authority.AuthorizeSign(body.OTT)
if err != nil {
WriteError(w, errs.Unauthorized(err))
return
}
certChain, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...)
if err != nil {
WriteError(w, errs.Forbidden(err))
return
}
certChainPEM := certChainToPEM(certChain)
var caPEM Certificate
if len(certChainPEM) > 0 {
caPEM = certChainPEM[1]
}
logCertificate(w, certChain[0])
JSONStatus(w, &SignResponse{
ServerPEM: certChainPEM[0],
CaPEM: caPEM,
CertChainPEM: certChainPEM,
TLSOptions: h.Authority.GetTLSOptions(),
}, http.StatusCreated)
}
// Renew uses the information of certificate in the TLS connection to create a
// new one.
func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
WriteError(w, errs.BadRequest(errors.New("missing peer certificate")))
return
}
certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0])
if err != nil {
WriteError(w, errs.Forbidden(err))
return
}
certChainPEM := certChainToPEM(certChain)
var caPEM Certificate
if len(certChainPEM) > 0 {
caPEM = certChainPEM[1]
}
logCertificate(w, certChain[0])
JSONStatus(w, &SignResponse{
ServerPEM: certChainPEM[0],
CaPEM: caPEM,
CertChainPEM: certChainPEM,
TLSOptions: h.Authority.GetTLSOptions(),
}, http.StatusCreated)
}
// Provisioners returns the list of provisioners configured in the authority.
func (h *caHandler) Provisioners(w http.ResponseWriter, r *http.Request) {
cursor, limit, err := parseCursor(r)

View file

@ -28,6 +28,7 @@ import (
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/certificates/logging"
"github.com/smallstep/certificates/sshutil"
"github.com/smallstep/certificates/templates"
@ -914,7 +915,7 @@ func Test_caHandler_Renew(t *testing.T) {
{"ok", cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},
{"no tls", nil, nil, nil, nil, http.StatusBadRequest},
{"no peer certificates", &tls.ConnectionState{}, nil, nil, nil, http.StatusBadRequest},
{"renew error", cs, nil, nil, fmt.Errorf("an error"), http.StatusForbidden},
{"renew error", cs, nil, nil, errs.Forbidden(fmt.Errorf("an error")), http.StatusForbidden},
}
expected := []byte(`{"crt":"` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","ca":"` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n","certChain":["` + strings.Replace(certPEM, "\n", `\n`, -1) + `\n","` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`)
@ -934,13 +935,13 @@ func Test_caHandler_Renew(t *testing.T) {
res := w.Result()
if res.StatusCode != tt.statusCode {
t.Errorf("caHandler.Root StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
t.Errorf("caHandler.Renew StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
}
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Errorf("caHandler.Root unexpected error = %v", err)
t.Errorf("caHandler.Renew unexpected error = %v", err)
}
if tt.statusCode < http.StatusBadRequest {
if !bytes.Equal(bytes.TrimSpace(body), expected) {
@ -1009,8 +1010,12 @@ func Test_caHandler_Provisioners(t *testing.T) {
t.Fatal(err)
}
expectedError400 := []byte(`{"status":400,"message":"Bad Request"}`)
expectedError500 := []byte(`{"status":500,"message":"Internal Server Error"}`)
expectedError400 := errs.BadRequest(errors.New("force"))
expectedError400Bytes, err := json.Marshal(expectedError400)
assert.FatalError(t, err)
expectedError500 := errs.InternalServerError(errors.New("force"))
expectedError500Bytes, err := json.Marshal(expectedError500)
assert.FatalError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &caHandler{
@ -1035,12 +1040,12 @@ func Test_caHandler_Provisioners(t *testing.T) {
} else {
switch tt.statusCode {
case 400:
if !bytes.Equal(bytes.TrimSpace(body), expectedError400) {
t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError400)
if !bytes.Equal(bytes.TrimSpace(body), expectedError400Bytes) {
t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError400Bytes)
}
case 500:
if !bytes.Equal(bytes.TrimSpace(body), expectedError500) {
t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError500)
if !bytes.Equal(bytes.TrimSpace(body), expectedError500Bytes) {
t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError500Bytes)
}
default:
t.Errorf("caHandler.Provisioner unexpected status code = %d", tt.statusCode)
@ -1077,7 +1082,9 @@ func Test_caHandler_ProvisionerKey(t *testing.T) {
}
expected := []byte(`{"key":"` + privKey + `"}`)
expectedError := []byte(`{"status":404,"message":"Not Found"}`)
expectedError404 := errs.NotFound(errors.New("force"))
expectedError404Bytes, err := json.Marshal(expectedError404)
assert.FatalError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -1101,8 +1108,8 @@ func Test_caHandler_ProvisionerKey(t *testing.T) {
t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expected)
}
} else {
if !bytes.Equal(bytes.TrimSpace(body), expectedError) {
t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError)
if !bytes.Equal(bytes.TrimSpace(body), expectedError404Bytes) {
t.Errorf("caHandler.Provisioners Body = %s, wants %s", body, expectedError404Bytes)
}
}
})

36
api/renew.go Normal file
View file

@ -0,0 +1,36 @@
package api
import (
"net/http"
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
)
// Renew uses the information of certificate in the TLS connection to create a
// new one.
func (h *caHandler) Renew(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
WriteError(w, errs.BadRequest(errors.New("missing peer certificate")))
return
}
certChain, err := h.Authority.Renew(r.TLS.PeerCertificates[0])
if err != nil {
WriteError(w, errs.Wrap(http.StatusInternalServerError, err, "cahandler.Renew"))
return
}
certChainPEM := certChainToPEM(certChain)
var caPEM Certificate
if len(certChainPEM) > 0 {
caPEM = certChainPEM[1]
}
logCertificate(w, certChain[0])
JSONStatus(w, &SignResponse{
ServerPEM: certChainPEM[0],
CaPEM: caPEM,
CertChainPEM: certChainPEM,
TLSOptions: h.Authority.GetTLSOptions(),
}, http.StatusCreated)
}

90
api/sign.go Normal file
View file

@ -0,0 +1,90 @@
package api
import (
"crypto/tls"
"net/http"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/tlsutil"
)
// SignRequest is the request body for a certificate signature request.
type SignRequest struct {
CsrPEM CertificateRequest `json:"csr"`
OTT string `json:"ott"`
NotAfter TimeDuration `json:"notAfter"`
NotBefore TimeDuration `json:"notBefore"`
}
// Validate checks the fields of the SignRequest and returns nil if they are ok
// or an error if something is wrong.
func (s *SignRequest) Validate() error {
if s.CsrPEM.CertificateRequest == nil {
return errs.BadRequest(errors.New("missing csr"))
}
if err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil {
return errs.BadRequest(errors.Wrap(err, "invalid csr"))
}
if s.OTT == "" {
return errs.BadRequest(errors.New("missing ott"))
}
return nil
}
// SignResponse is the response object of the certificate signature request.
type SignResponse struct {
ServerPEM Certificate `json:"crt"`
CaPEM Certificate `json:"ca"`
CertChainPEM []Certificate `json:"certChain"`
TLSOptions *tlsutil.TLSOptions `json:"tlsOptions,omitempty"`
TLS *tls.ConnectionState `json:"-"`
}
// Sign is an HTTP handler that reads a certificate request and an
// one-time-token (ott) from the body and creates a new certificate with the
// information in the certificate request.
func (h *caHandler) Sign(w http.ResponseWriter, r *http.Request) {
var body SignRequest
if err := ReadJSON(r.Body, &body); err != nil {
WriteError(w, errs.BadRequest(errors.Wrap(err, "error reading request body")))
return
}
logOtt(w, body.OTT)
if err := body.Validate(); err != nil {
WriteError(w, err)
return
}
opts := provisioner.Options{
NotBefore: body.NotBefore,
NotAfter: body.NotAfter,
}
signOpts, err := h.Authority.AuthorizeSign(body.OTT)
if err != nil {
WriteError(w, errs.Unauthorized(err))
return
}
certChain, err := h.Authority.Sign(body.CsrPEM.CertificateRequest, opts, signOpts...)
if err != nil {
WriteError(w, errs.Forbidden(err))
return
}
certChainPEM := certChainToPEM(certChain)
var caPEM Certificate
if len(certChainPEM) > 0 {
caPEM = certChainPEM[1]
}
logCertificate(w, certChain[0])
JSONStatus(w, &SignResponse{
ServerPEM: certChainPEM[0],
CaPEM: caPEM,
CertChainPEM: certChainPEM,
TLSOptions: h.Authority.GetTLSOptions(),
}, http.StatusCreated)
}

View file

@ -282,7 +282,7 @@ func (h *caHandler) SSHSign(w http.ResponseWriter, r *http.Request) {
ValidAfter: body.ValidAfter,
}
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignSSHMethod)
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod)
signOpts, err := h.Authority.Authorize(ctx, body.OTT)
if err != nil {
WriteError(w, errs.Unauthorized(err))

View file

@ -56,13 +56,13 @@ func (h *caHandler) SSHRekey(w http.ResponseWriter, r *http.Request) {
return
}
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RekeySSHMethod)
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod)
signOpts, err := h.Authority.Authorize(ctx, body.OTT)
if err != nil {
WriteError(w, errs.Unauthorized(err))
return
}
oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT)
oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT)
if err != nil {
WriteError(w, errs.InternalServerError(err))
}

View file

@ -46,13 +46,13 @@ func (h *caHandler) SSHRenew(w http.ResponseWriter, r *http.Request) {
return
}
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RenewSSHMethod)
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod)
_, err := h.Authority.Authorize(ctx, body.OTT)
if err != nil {
WriteError(w, errs.Unauthorized(err))
return
}
oldCert, err := provisioner.ExtractSSHPOPCert(body.OTT)
oldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT)
if err != nil {
WriteError(w, errs.InternalServerError(err))
}

View file

@ -66,7 +66,7 @@ func (h *caHandler) SSHRevoke(w http.ResponseWriter, r *http.Request) {
PassiveOnly: body.Passive,
}
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeSSHMethod)
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod)
// A token indicates that we are using the api via a provisioner token,
// otherwise it is assumed that the certificate is revoking itself over mTLS.
logOtt(w, body.OTT)

View file

@ -13,12 +13,13 @@ import (
stepJOSE "github.com/smallstep/cli/jose"
)
func testAuthority(t *testing.T) *Authority {
func testAuthority(t *testing.T, opts ...Option) *Authority {
maxjwk, err := stepJOSE.ParseKey("testdata/secrets/max_pub.jwk")
assert.FatalError(t, err)
clijwk, err := stepJOSE.ParseKey("testdata/secrets/step_cli_key_pub.jwk")
assert.FatalError(t, err)
disableRenewal := true
enableSSHCA := true
p := provisioner.List{
&provisioner.JWK{
Name: "Max",
@ -29,6 +30,9 @@ func testAuthority(t *testing.T) *Authority {
Name: "step-cli",
Type: "JWK",
Key: clijwk,
Claims: &provisioner.Claims{
EnableSSHCA: &enableSSHCA,
},
},
&provisioner.JWK{
Name: "dev",
@ -46,19 +50,30 @@ func testAuthority(t *testing.T) *Authority {
DisableRenewal: &disableRenewal,
},
},
&provisioner.SSHPOP{
Name: "sshpop",
Type: "SSHPOP",
Claims: &provisioner.Claims{
EnableSSHCA: &enableSSHCA,
},
},
}
c := &Config{
Address: "127.0.0.1:443",
Root: []string{"testdata/certs/root_ca.crt"},
IntermediateCert: "testdata/certs/intermediate_ca.crt",
IntermediateKey: "testdata/secrets/intermediate_ca_key",
DNSNames: []string{"test.ca.smallstep.com"},
Password: "pass",
SSH: &SSHConfig{
HostKey: "testdata/secrets/ssh_host_ca_key",
UserKey: "testdata/secrets/ssh_user_ca_key",
},
DNSNames: []string{"example.com"},
Password: "pass",
AuthorityConfig: &AuthConfig{
Provisioners: p,
},
}
a, err := New(c)
a, err := New(c, opts...)
assert.FatalError(t, err)
return a
}

View file

@ -8,7 +8,9 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ssh"
)
// Claims extends jose.Claims with step attributes.
@ -36,22 +38,19 @@ func SkipTokenReuseFromContext(ctx context.Context) bool {
// authorizeToken parses the token and returns the provisioner used to generate
// the token. This method enforces the One-Time use policy (tokens can only be
// used once).
func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner.Interface, error) {
var errContext = map[string]interface{}{"ott": ott}
func (a *Authority) authorizeToken(ctx context.Context, token string) (provisioner.Interface, error) {
// Validate payload
token, err := jose.ParseSigned(ott)
tok, err := jose.ParseSigned(token)
if err != nil {
return nil, &apiError{errors.Wrapf(err, "authorizeToken: error parsing token"),
http.StatusUnauthorized, errContext}
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeToken: error parsing token")
}
// Get claims w/out verification. We need to look up the provisioner
// key in order to verify the claims and we need the issuer from the claims
// before we can look up the provisioner.
var claims Claims
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeToken"), http.StatusUnauthorized, errContext}
if err = tok.UnsafeClaimsWithoutVerification(&claims); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeToken")
}
// TODO: use new persistence layer abstraction.
@ -59,29 +58,27 @@ func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner
// This check is meant as a stopgap solution to the current lack of a persistence layer.
if a.config.AuthorityConfig != nil && !a.config.AuthorityConfig.DisableIssuedAtCheck {
if claims.IssuedAt != nil && claims.IssuedAt.Time().Before(a.startTime) {
return nil, &apiError{errors.New("authorizeToken: token issued before the bootstrap of certificate authority"),
http.StatusUnauthorized, errContext}
return nil, errs.Unauthorized(errors.New("authority.authorizeToken: token issued before the bootstrap of certificate authority"))
}
}
// This method will also validate the audiences for JWK provisioners.
p, ok := a.provisioners.LoadByToken(token, &claims.Claims)
p, ok := a.provisioners.LoadByToken(tok, &claims.Claims)
if !ok {
return nil, &apiError{
errors.Errorf("authorizeToken: provisioner not found or invalid audience (%s)", strings.Join(claims.Audience, ", ")),
http.StatusUnauthorized, errContext}
return nil, errs.Unauthorized(errors.Errorf("authority.authorizeToken: provisioner "+
"not found or invalid audience (%s)", strings.Join(claims.Audience, ", ")))
}
// Store the token to protect against reuse unless it's skipped.
if !SkipTokenReuseFromContext(ctx) {
if reuseKey, err := p.GetTokenID(ott); err == nil {
ok, err := a.db.UseToken(reuseKey, ott)
if reuseKey, err := p.GetTokenID(token); err == nil {
ok, err := a.db.UseToken(reuseKey, token)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeToken: failed when attempting to store token"),
http.StatusInternalServerError, errContext}
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.authorizeToken: failed when attempting to store token")
}
if !ok {
return nil, &apiError{errors.Errorf("authorizeToken: token already used"), http.StatusUnauthorized, errContext}
return nil, errs.Unauthorized(errors.Errorf("authority.authorizeToken: token already used"))
}
}
}
@ -89,125 +86,158 @@ func (a *Authority) authorizeToken(ctx context.Context, ott string) (provisioner
return p, nil
}
// Authorize grabs the method from the context and authorizes a signature
// request by validating the one-time-token.
func (a *Authority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
var errContext = apiCtx{"ott": ott}
// Authorize grabs the method from the context and authorizes the request by
// validating the one-time-token.
func (a *Authority) Authorize(ctx context.Context, token string) ([]provisioner.SignOption, error) {
var opts = []errs.Option{errs.WithKeyVal("token", token)}
switch m := provisioner.MethodFromContext(ctx); m {
case provisioner.SignMethod:
return a.authorizeSign(ctx, ott)
signOpts, err := a.authorizeSign(ctx, token)
return signOpts, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...)
case provisioner.RevokeMethod:
return nil, a.authorizeRevoke(ctx, ott)
case provisioner.SignSSHMethod:
return nil, errs.Wrap(http.StatusInternalServerError, a.authorizeRevoke(ctx, token), "authority.Authorize", opts...)
case provisioner.SSHSignMethod:
if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...)
}
return a.authorizeSSHSign(ctx, ott)
case provisioner.RenewSSHMethod:
_, err := a.authorizeSSHSign(ctx, token)
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...)
case provisioner.SSHRenewMethod:
if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...)
}
if _, err := a.authorizeSSHRenew(ctx, ott); err != nil {
return nil, err
}
return nil, nil
case provisioner.RevokeSSHMethod:
return nil, a.authorizeSSHRevoke(ctx, ott)
case provisioner.RekeySSHMethod:
_, err := a.authorizeSSHRenew(ctx, token)
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...)
case provisioner.SSHRevokeMethod:
return nil, errs.Wrap(http.StatusInternalServerError, a.authorizeSSHRevoke(ctx, token), "authority.Authorize", opts...)
case provisioner.SSHRekeyMethod:
if a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {
return nil, &apiError{errors.New("authorize: ssh signing is not enabled"), http.StatusNotImplemented, errContext}
return nil, errs.NotImplemented(errors.New("authority.Authorize; ssh certificate flows are not enabled"), opts...)
}
_, opts, err := a.authorizeSSHRekey(ctx, ott)
if err != nil {
return nil, err
}
return opts, nil
_, signOpts, err := a.authorizeSSHRekey(ctx, token)
return signOpts, errs.Wrap(http.StatusInternalServerError, err, "authority.Authorize", opts...)
default:
return nil, &apiError{errors.Errorf("authorize: method %d is not supported", m), http.StatusInternalServerError, errContext}
return nil, errs.InternalServerError(errors.Errorf("authority.Authorize; method %d is not supported", m), opts...)
}
}
// authorizeSign loads the provisioner from the token, checks that it has not
// been used again and calls the provisioner AuthorizeSign method. Returns a
// list of methods to apply to the signing flow.
func (a *Authority) authorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
var errContext = apiCtx{"ott": ott}
p, err := a.authorizeToken(ctx, ott)
// authorizeSign loads the provisioner from the token and calls the provisioner
// AuthorizeSign method. Returns a list of methods to apply to the signing flow.
func (a *Authority) authorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) {
p, err := a.authorizeToken(ctx, token)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext}
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSign")
}
opts, err := p.AuthorizeSign(ctx, ott)
signOpts, err := p.AuthorizeSign(ctx, token)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeSign"), http.StatusUnauthorized, errContext}
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSign")
}
return opts, nil
return signOpts, nil
}
// AuthorizeSign authorizes a signature request by validating and authenticating
// a OTT that must be sent w/ the request.
// a token that must be sent w/ the request.
//
// NOTE: This method is deprecated and should not be used. We make it available
// in the short term os as not to break existing clients.
func (a *Authority) AuthorizeSign(ott string) ([]provisioner.SignOption, error) {
func (a *Authority) AuthorizeSign(token string) ([]provisioner.SignOption, error) {
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod)
return a.Authorize(ctx, ott)
return a.Authorize(ctx, token)
}
// authorizeRevoke authorizes a revocation request by validating and authenticating
// the RevokeOptions POSTed with the request.
// Returns a tuple of the provisioner ID and error, if one occurred.
// authorizeRevoke locates the provisioner used to generate the authenticating
// token and then performs the token validation flow.
func (a *Authority) authorizeRevoke(ctx context.Context, token string) error {
errContext := map[string]interface{}{"ott": token}
p, err := a.authorizeToken(ctx, token)
if err != nil {
return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext}
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRevoke")
}
if err = p.AuthorizeRevoke(ctx, token); err != nil {
return &apiError{errors.Wrap(err, "authorizeRevoke"), http.StatusUnauthorized, errContext}
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRevoke")
}
return nil
}
// authorizeRenewl tries to locate the step provisioner extension, and checks
// authorizeRenew locates the provisioner (using the provisioner extension in the cert), and checks
// if for the configured provisioner, the renewal is enabled or not. If the
// extra extension cannot be found, authorize the renewal by default.
//
// TODO(mariano): should we authorize by default?
func (a *Authority) authorizeRenew(crt *x509.Certificate) error {
errContext := map[string]interface{}{"serialNumber": crt.SerialNumber.String()}
func (a *Authority) authorizeRenew(cert *x509.Certificate) error {
var opts = []errs.Option{errs.WithKeyVal("serialNumber", cert.SerialNumber.String())}
// Check the passive revocation table.
isRevoked, err := a.db.IsRevoked(crt.SerialNumber.String())
isRevoked, err := a.db.IsRevoked(cert.SerialNumber.String())
if err != nil {
return &apiError{
err: errors.Wrap(err, "renew"),
code: http.StatusInternalServerError,
context: errContext,
}
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
}
if isRevoked {
return &apiError{
err: errors.New("renew: certificate has been revoked"),
code: http.StatusUnauthorized,
context: errContext,
}
return errs.Unauthorized(errors.New("authority.authorizeRenew: certificate has been revoked"), opts...)
}
p, ok := a.provisioners.LoadByCertificate(crt)
p, ok := a.provisioners.LoadByCertificate(cert)
if !ok {
return &apiError{
err: errors.New("renew: provisioner not found"),
code: http.StatusUnauthorized,
context: errContext,
}
return errs.Unauthorized(errors.New("authority.authorizeRenew: provisioner not found"), opts...)
}
if err := p.AuthorizeRenew(context.Background(), crt); err != nil {
return &apiError{
err: errors.Wrap(err, "renew"),
code: http.StatusUnauthorized,
context: errContext,
}
if err := p.AuthorizeRenew(context.Background(), cert); err != nil {
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeRenew", opts...)
}
return nil
}
// authorizeSSHSign loads the provisioner from the token, checks that it has not
// been used again and calls the provisioner AuthorizeSSHSign method. Returns a
// list of methods to apply to the signing flow.
func (a *Authority) authorizeSSHSign(ctx context.Context, token string) ([]provisioner.SignOption, error) {
p, err := a.authorizeToken(ctx, token)
if err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeSSHSign")
}
signOpts, err := p.AuthorizeSSHSign(ctx, token)
if err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.authorizeSSHSign")
}
return signOpts, nil
}
// authorizeSSHRenew authorizes an SSH certificate renewal request, by
// validating the contents of an SSHPOP token.
func (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
p, err := a.authorizeToken(ctx, token)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRenew")
}
cert, err := p.AuthorizeSSHRenew(ctx, token)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRenew")
}
return cert, nil
}
// authorizeSSHRekey authorizes an SSH certificate rekey request, by
// validating the contents of an SSHPOP token.
func (a *Authority) authorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) {
p, err := a.authorizeToken(ctx, token)
if err != nil {
return nil, nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRekey")
}
cert, signOpts, err := p.AuthorizeSSHRekey(ctx, token)
if err != nil {
return nil, nil, errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRekey")
}
return cert, signOpts, nil
}
// authorizeSSHRevoke authorizes an SSH certificate revoke request, by
// validating the contents of an SSHPOP token.
func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error {
p, err := a.authorizeToken(ctx, token)
if err != nil {
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRevoke")
}
if err = p.AuthorizeSSHRevoke(ctx, token); err != nil {
return errs.Wrap(http.StatusInternalServerError, err, "authority.authorizeSSHRevoke")
}
return nil
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
package authority
import (
"fmt"
"testing"
"github.com/pkg/errors"
@ -9,7 +10,6 @@ import (
"github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util"
stepJOSE "github.com/smallstep/cli/jose"
jose "gopkg.in/square/go-jose.v2"
)
func TestConfigValidate(t *testing.T) {
@ -255,28 +255,19 @@ func TestAuthConfigValidate(t *testing.T) {
err: errors.New("authority cannot be undefined"),
}
},
"fail-invalid-provisioners": func(t *testing.T) AuthConfigValidateTest {
return AuthConfigValidateTest{
ac: &AuthConfig{
Provisioners: provisioner.List{
&provisioner.JWK{Name: "foo", Type: "bar", Key: &jose.JSONWebKey{}},
&provisioner.JWK{Name: "foo", Key: &jose.JSONWebKey{}},
/*
"fail-invalid-claims": func(t *testing.T) AuthConfigValidateTest {
return AuthConfigValidateTest{
ac: &AuthConfig{
Provisioners: p,
Claims: &provisioner.Claims{
MinTLSDur: &provisioner.Duration{Duration: -1},
},
},
},
err: errors.New("provisioner type cannot be empty"),
}
},
"fail-invalid-claims": func(t *testing.T) AuthConfigValidateTest {
return AuthConfigValidateTest{
ac: &AuthConfig{
Provisioners: p,
Claims: &provisioner.Claims{
MinTLSDur: &provisioner.Duration{Duration: -1},
},
},
err: errors.New("claims: MinTLSCertDuration must be greater than 0"),
}
},
err: errors.New("claims: MinTLSCertDuration must be greater than 0"),
}
},
*/
"ok-empty-provisioners": func(t *testing.T) AuthConfigValidateTest {
return AuthConfigValidateTest{
ac: &AuthConfig{},
@ -311,7 +302,7 @@ func TestAuthConfigValidate(t *testing.T) {
assert.Equals(t, tc.err.Error(), err.Error())
}
} else {
if assert.Nil(t, tc.err) {
if assert.Nil(t, tc.err, fmt.Sprintf("expected error: %s, but got <nil>", tc.err)) {
assert.Equals(t, *tc.ac.Template, tc.asn1dn)
}
}

View file

@ -1,96 +0,0 @@
package authority
import (
"crypto/x509"
"github.com/smallstep/certificates/db"
"golang.org/x/crypto/ssh"
)
type MockAuthDB struct {
err error
ret1 interface{}
isRevoked func(string) (bool, error)
isSSHRevoked func(string) (bool, error)
revoke func(rci *db.RevokedCertificateInfo) error
revokeSSH func(rci *db.RevokedCertificateInfo) error
storeCertificate func(crt *x509.Certificate) error
useToken func(id, tok string) (bool, error)
isSSHHost func(principal string) (bool, error)
storeSSHCertificate func(crt *ssh.Certificate) error
getSSHHostPrincipals func() ([]string, error)
shutdown func() error
}
func (m *MockAuthDB) IsRevoked(sn string) (bool, error) {
if m.isRevoked != nil {
return m.isRevoked(sn)
}
return m.ret1.(bool), m.err
}
func (m *MockAuthDB) IsSSHRevoked(sn string) (bool, error) {
if m.isSSHRevoked != nil {
return m.isSSHRevoked(sn)
}
return m.ret1.(bool), m.err
}
func (m *MockAuthDB) UseToken(id, tok string) (bool, error) {
if m.useToken != nil {
return m.useToken(id, tok)
}
if m.ret1 == nil {
return false, m.err
}
return m.ret1.(bool), m.err
}
func (m *MockAuthDB) Revoke(rci *db.RevokedCertificateInfo) error {
if m.revoke != nil {
return m.revoke(rci)
}
return m.err
}
func (m *MockAuthDB) RevokeSSH(rci *db.RevokedCertificateInfo) error {
if m.revokeSSH != nil {
return m.revokeSSH(rci)
}
return m.err
}
func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error {
if m.storeCertificate != nil {
return m.storeCertificate(crt)
}
return m.err
}
func (m *MockAuthDB) IsSSHHost(principal string) (bool, error) {
if m.isSSHHost != nil {
return m.isSSHHost(principal)
}
return m.ret1.(bool), m.err
}
func (m *MockAuthDB) StoreSSHCertificate(crt *ssh.Certificate) error {
if m.storeSSHCertificate != nil {
return m.storeSSHCertificate(crt)
}
return m.err
}
func (m *MockAuthDB) GetSSHHostPrincipals() ([]string, error) {
if m.getSSHHostPrincipals != nil {
return m.getSSHHostPrincipals()
}
return m.ret1.([]string), m.err
}
func (m *MockAuthDB) Shutdown() error {
if m.shutdown != nil {
return m.shutdown()
}
return m.err
}

View file

@ -5,6 +5,7 @@ import (
"crypto/x509"
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
)
// ACME is the acme provisioner type, an entity that can authorize the ACME
@ -79,7 +80,7 @@ func (p *ACME) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
// certificate was configured to allow renewals.
func (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
return errs.Unauthorized(errors.Errorf("acme.AuthorizeRenew; renew is disabled for acme provisioner %s", p.GetID()))
}
return nil
}

View file

@ -3,11 +3,13 @@ package provisioner
import (
"context"
"crypto/x509"
"net/http"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/errs"
)
func TestACME_Getters(t *testing.T) {
@ -88,86 +90,98 @@ func TestACME_Init(t *testing.T) {
}
}
func TestACME_AuthorizeRevoke(t *testing.T) {
p, err := generateACME()
assert.FatalError(t, err)
assert.Nil(t, p.AuthorizeRevoke(context.TODO(), ""))
}
func TestACME_AuthorizeRenew(t *testing.T) {
p1, err := generateACME()
assert.FatalError(t, err)
p2, err := generateACME()
assert.FatalError(t, err)
// disable renewal
disable := true
p2.Claims = &Claims{DisableRenewal: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
type args struct {
type test struct {
p *ACME
cert *x509.Certificate
}
tests := []struct {
name string
prov *ACME
args args
err error
}{
{"ok", p1, args{nil}, nil},
{"fail", p2, args{nil}, errors.Errorf("renew is disabled for provisioner %s", p2.GetID())},
code int
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
tests := map[string]func(*testing.T) test{
"fail/renew-disabled": func(t *testing.T) test {
p, err := generateACME()
assert.FatalError(t, err)
// disable renewal
disable := true
p.Claims = &Claims{DisableRenewal: &disable}
p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
return test{
p: p,
cert: &x509.Certificate{},
code: http.StatusUnauthorized,
err: errors.Errorf("acme.AuthorizeRenew; renew is disabled for acme provisioner %s", p.GetID()),
}
},
"ok": func(t *testing.T) test {
p, err := generateACME()
assert.FatalError(t, err)
return test{
p: p,
cert: &x509.Certificate{},
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
assert.Nil(t, tt.err)
assert.Nil(t, tc.err)
}
})
}
}
func TestACME_AuthorizeSign(t *testing.T) {
p1, err := generateACME()
assert.FatalError(t, err)
tests := []struct {
name string
prov *ACME
method Method
err error
}{
{"fail/method", p1, SignSSHMethod, errors.New("unexpected method type 1 in context")},
{"ok", p1, SignMethod, nil},
type test struct {
p *ACME
token string
code int
err error
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContextWithMethod(context.Background(), tt.method)
if got, err := tt.prov.AuthorizeSign(ctx, ""); err != nil {
if assert.NotNil(t, tt.err) {
assert.HasPrefix(t, err.Error(), tt.err.Error())
tests := map[string]func(*testing.T) test{
"ok": func(t *testing.T) test {
p, err := generateACME()
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.NotNil(t, got) {
assert.Len(t, 4, got)
for _, o := range got {
if assert.Nil(t, tc.err) && assert.NotNil(t, opts) {
assert.Len(t, 4, opts)
for _, o := range opts {
switch v := o.(type) {
case *provisionerExtensionOption:
assert.Equals(t, v.Type, int(TypeACME))
assert.Equals(t, v.Name, tt.prov.GetName())
assert.Equals(t, v.Name, tc.p.GetName())
assert.Equals(t, v.CredentialID, "")
assert.Len(t, 0, v.KeyValuePairs)
case profileDefaultDuration:
assert.Equals(t, time.Duration(v), tt.prov.claimer.DefaultTLSCertDuration())
assert.Equals(t, time.Duration(v), tc.p.claimer.DefaultTLSCertDuration())
case defaultPublicKeyValidator:
case *validityValidator:
assert.Equals(t, v.min, tt.prov.claimer.MinTLSCertDuration())
assert.Equals(t, v.max, tt.prov.claimer.MaxTLSCertDuration())
assert.Equals(t, v.min, tc.p.claimer.MinTLSCertDuration())
assert.Equals(t, v.max, tc.p.claimer.MaxTLSCertDuration())
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}

View file

@ -16,6 +16,7 @@ import (
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
@ -271,7 +272,7 @@ func (p *AWS) Init(config Config) (err error) {
func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
payload, err := p.authorizeToken(token)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "aws.AuthorizeSign")
}
doc := payload.document
@ -305,7 +306,7 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
// certificate was configured to allow renewals.
func (p *AWS) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
return errs.Unauthorized(errors.Errorf("aws.AuthorizeRenew; renew is disabled for aws provisioner %s", p.GetID()))
}
return nil
}
@ -349,41 +350,41 @@ func (p *AWS) readURL(url string) ([]byte, error) {
func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
return nil, errs.Wrapf(http.StatusUnauthorized, err, "aws.authorizeToken; error parsing aws token")
}
if len(jwt.Headers) == 0 {
return nil, errors.New("error parsing token: header is missing")
return nil, errs.InternalServerError(errors.New("aws.authorizeToken; error parsing token, header is missing"))
}
var unsafeClaims awsPayload
if err := jwt.UnsafeClaimsWithoutVerification(&unsafeClaims); err != nil {
return nil, errors.Wrap(err, "error unmarshaling claims")
return nil, errs.Wrap(http.StatusUnauthorized, err, "aws.authorizeToken; error unmarshaling claims")
}
var payload awsPayload
if err := jwt.Claims(unsafeClaims.Amazon.Signature, &payload); err != nil {
return nil, errors.Wrap(err, "error verifying claims")
return nil, errs.Wrap(http.StatusUnauthorized, err, "aws.authorizeToken; error verifying claims")
}
// Validate identity document signature
if err := p.checkSignature(payload.Amazon.Document, payload.Amazon.Signature); err != nil {
return nil, err
return nil, errs.Wrap(http.StatusUnauthorized, err, "aws.authorizeToken; invalid aws token signature")
}
var doc awsInstanceIdentityDocument
if err := json.Unmarshal(payload.Amazon.Document, &doc); err != nil {
return nil, errors.Wrap(err, "error unmarshaling identity document")
return nil, errs.Wrap(http.StatusUnauthorized, err, "aws.authorizeToken; error unmarshaling aws identity document")
}
switch {
case doc.AccountID == "":
return nil, errors.New("identity document accountId cannot be empty")
return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document accountId cannot be empty"))
case doc.InstanceID == "":
return nil, errors.New("identity document instanceId cannot be empty")
return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document instanceId cannot be empty"))
case doc.PrivateIP == "":
return nil, errors.New("identity document privateIp cannot be empty")
return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document privateIp cannot be empty"))
case doc.Region == "":
return nil, errors.New("identity document region cannot be empty")
return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document region cannot be empty"))
}
// According to "rfc7519 JSON Web Token" acceptable skew should be no
@ -393,12 +394,12 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
Issuer: awsIssuer,
Time: now,
}, time.Minute); err != nil {
return nil, errors.Wrapf(err, "invalid token")
return nil, errs.Wrapf(http.StatusUnauthorized, err, "aws.authorizeToken; invalid aws token")
}
// validate audiences with the defaults
if !matchesAudience(payload.Audience, p.audiences.Sign) {
return nil, errors.New("invalid token: invalid audience claim (aud)")
return nil, errs.Unauthorized(errors.New("aws.authorizeToken; invalid token - invalid audience claim (aud)"))
}
// Validate subject, it has to be known if disableCustomSANs is enabled
@ -406,7 +407,7 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
if payload.Subject != doc.InstanceID &&
payload.Subject != doc.PrivateIP &&
payload.Subject != fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region) {
return nil, errors.New("invalid token: invalid subject claim (sub)")
return nil, errs.Unauthorized(errors.New("aws.authorizeToken; invalid token - invalid subject claim (sub)"))
}
}
@ -420,14 +421,14 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
}
}
if !found {
return nil, errors.New("invalid identity document: accountId is not valid")
return nil, errs.Unauthorized(errors.New("aws.authorizeToken; invalid aws identity document - accountId is not valid"))
}
}
// validate instance age
if d := p.InstanceAge.Value(); d > 0 {
if now.Sub(doc.PendingTime) > d {
return nil, errors.New("identity document pendingTime is too old")
return nil, errs.Unauthorized(errors.New("aws.authorizeToken; aws identity document pendingTime is too old"))
}
}
@ -438,18 +439,18 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
return nil, errs.Unauthorized(errors.Errorf("aws.AuthorizeSSHSign; ssh ca is disabled for aws provisioner %s", p.GetID()))
}
claims, err := p.authorizeToken(token)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "aws.AuthorizeSSHSign")
}
doc := claims.document
signOptions := []SignOption{
// set the key id to the token subject
sshCertificateKeyIDModifier(claims.Subject),
sshCertKeyIDModifier(claims.Subject),
}
// Default to host + known IPs/hostnames
@ -463,7 +464,7 @@ func (p *AWS) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
// Validate user options
signOptions = append(signOptions, sshCertificateOptionsValidator(defaults))
// Set defaults if not given as user options
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
signOptions = append(signOptions, sshCertDefaultsModifier(defaults))
return append(signOptions,
// Set the default extensions.

View file

@ -10,12 +10,15 @@ import (
"encoding/hex"
"encoding/pem"
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
@ -229,6 +232,213 @@ func TestAWS_Init(t *testing.T) {
}
}
func TestAWS_authorizeToken(t *testing.T) {
block, _ := pem.Decode([]byte(awsTestKey))
if block == nil || block.Type != "RSA PRIVATE KEY" {
t.Fatal("error decoding AWS key")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
assert.FatalError(t, err)
badKey, err := rsa.GenerateKey(rand.Reader, 1024)
assert.FatalError(t, err)
type test struct {
p *AWS
token string
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; error parsing aws token"),
}
},
"fail/cannot-validate-sig": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id",
"127.0.0.1", "us-west-1", time.Now(), badKey)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; invalid aws token signature"),
}
},
"fail/empty-account-id": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), "", "instance-id",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; aws identity document accountId cannot be empty"),
}
},
"fail/empty-instance-id": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), p.Accounts[0], "",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; aws identity document instanceId cannot be empty"),
}
},
"fail/empty-private-ip": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id",
"", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; aws identity document privateIp cannot be empty"),
}
},
"fail/empty-region": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id",
"127.0.0.1", "", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; aws identity document region cannot be empty"),
}
},
"fail/invalid-token-issuer": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", "bad-issuer", p.GetID(), p.Accounts[0], "instance-id",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; invalid aws token"),
}
},
"fail/invalid-audience": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, "bad-audience", p.Accounts[0], "instance-id",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; invalid token - invalid audience claim (aud)"),
}
},
"fail/invalid-subject-disabled-custom-SANs": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
p.DisableCustomSANs = true
tok, err := generateAWSToken(
"foo", awsIssuer, p.GetID(), p.Accounts[0], "instance-id",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; invalid token - invalid subject claim (sub)"),
}
},
"fail/invalid-account-id": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), "foo", "instance-id",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; invalid aws identity document - accountId is not valid"),
}
},
"fail/instance-age": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
p.InstanceAge = Duration{1 * time.Minute}
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id",
"127.0.0.1", "us-west-1", time.Now().Add(-1*time.Minute), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("aws.authorizeToken; aws identity document pendingTime is too old"),
}
},
"ok": func(t *testing.T) test {
p, err := generateAWS()
assert.FatalError(t, err)
tok, err := generateAWSToken(
"instance-id", awsIssuer, p.GetID(), p.Accounts[0], "instance-id",
"127.0.0.1", "us-west-1", time.Now(), key)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if claims, err := tc.p.authorizeToken(tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) && assert.NotNil(t, claims) {
assert.Equals(t, claims.Subject, "instance-id")
assert.Equals(t, claims.Issuer, awsIssuer)
assert.NotNil(t, claims.Amazon)
aud, err := generateSignAudience("https://ca.smallstep.com", tc.p.GetID())
assert.FatalError(t, err)
assert.Equals(t, claims.Audience[0], aud)
}
}
})
}
}
func TestAWS_AuthorizeSign(t *testing.T) {
p1, srv, err := generateAWSWithServer()
assert.FatalError(t, err)
@ -326,26 +536,27 @@ func TestAWS_AuthorizeSign(t *testing.T) {
aws *AWS
args args
wantLen int
code int
wantErr bool
}{
{"ok", p1, args{t1}, 5, false},
{"ok", p2, args{t2}, 7, false},
{"ok", p2, args{t2Hostname}, 7, false},
{"ok", p2, args{t2PrivateIP}, 7, false},
{"ok", p1, args{t4}, 5, false},
{"fail account", p3, args{t3}, 0, true},
{"fail token", p1, args{"token"}, 0, true},
{"fail subject", p1, args{failSubject}, 0, true},
{"fail issuer", p1, args{failIssuer}, 0, true},
{"fail audience", p1, args{failAudience}, 0, true},
{"fail account", p1, args{failAccount}, 0, true},
{"fail instanceID", p1, args{failInstanceID}, 0, true},
{"fail privateIP", p1, args{failPrivateIP}, 0, true},
{"fail region", p1, args{failRegion}, 0, true},
{"fail exp", p1, args{failExp}, 0, true},
{"fail nbf", p1, args{failNbf}, 0, true},
{"fail key", p1, args{failKey}, 0, true},
{"fail instance age", p2, args{failInstanceAge}, 0, true},
{"ok", p1, args{t1}, 5, http.StatusOK, false},
{"ok", p2, args{t2}, 7, http.StatusOK, false},
{"ok", p2, args{t2Hostname}, 7, http.StatusOK, false},
{"ok", p2, args{t2PrivateIP}, 7, http.StatusOK, false},
{"ok", p1, args{t4}, 5, http.StatusOK, false},
{"fail account", p3, args{t3}, 0, http.StatusUnauthorized, true},
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
{"fail subject", p1, args{failSubject}, 0, http.StatusUnauthorized, true},
{"fail issuer", p1, args{failIssuer}, 0, http.StatusUnauthorized, true},
{"fail audience", p1, args{failAudience}, 0, http.StatusUnauthorized, true},
{"fail account", p1, args{failAccount}, 0, http.StatusUnauthorized, true},
{"fail instanceID", p1, args{failInstanceID}, 0, http.StatusUnauthorized, true},
{"fail privateIP", p1, args{failPrivateIP}, 0, http.StatusUnauthorized, true},
{"fail region", p1, args{failRegion}, 0, http.StatusUnauthorized, true},
{"fail exp", p1, args{failExp}, 0, http.StatusUnauthorized, true},
{"fail nbf", p1, args{failNbf}, 0, http.StatusUnauthorized, true},
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
{"fail instance age", p2, args{failInstanceAge}, 0, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -354,8 +565,13 @@ func TestAWS_AuthorizeSign(t *testing.T) {
if (err != nil) != tt.wantErr {
t.Errorf("AWS.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
return
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
} else {
assert.Len(t, tt.wantLen, got)
}
assert.Len(t, tt.wantLen, got)
})
}
}
@ -368,6 +584,14 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) {
assert.FatalError(t, err)
defer srv.Close()
p2, err := generateAWS()
assert.FatalError(t, err)
// disable sshCA
disable := false
p2.Claims = &Claims{EnableSSHCA: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
t1, err := p1.GetIdentityToken("foo.local", "https://ca.smallstep.com")
assert.FatalError(t, err)
@ -407,30 +631,35 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) {
aws *AWS
args args
expected *SSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, false, false},
{"ok-principal-ip", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1"}}, pub}, expectedHostOptionsIP, false, false},
{"ok-principal-hostname", p1, args{t1, SSHOptions{Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptionsHostname, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, false, true},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal", "smallstep.com"}}, pub}, nil, false, true},
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principal-ip", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1"}}, pub}, expectedHostOptionsIP, http.StatusOK, false, false},
{"ok-principal-hostname", p1, args{t1, SSHOptions{Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptionsHostname, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContextWithMethod(context.Background(), SignSSHMethod)
got, err := tt.aws.AuthorizeSSHSign(ctx, tt.args.token)
got, err := tt.aws.AuthorizeSSHSign(context.Background(), tt.args.token)
if (err != nil) != tt.wantErr {
t.Errorf("AWS.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.Nil(t, got)
} else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
@ -447,6 +676,7 @@ func TestAWS_AuthorizeSSHSign(t *testing.T) {
})
}
}
func TestAWS_AuthorizeRenew(t *testing.T) {
p1, err := generateAWS()
assert.FatalError(t, err)
@ -466,44 +696,20 @@ func TestAWS_AuthorizeRenew(t *testing.T) {
name string
aws *AWS
args args
code int
wantErr bool
}{
{"ok", p1, args{nil}, false},
{"fail", p2, args{nil}, true},
{"ok", p1, args{nil}, http.StatusOK, false},
{"fail/renew-disabled", p2, args{nil}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.aws.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("AWS.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestAWS_AuthorizeRevoke(t *testing.T) {
p1, srv, err := generateAWSWithServer()
assert.FatalError(t, err)
defer srv.Close()
t1, err := p1.GetIdentityToken("foo.local", "https://ca.smallstep.com")
assert.FatalError(t, err)
type args struct {
token string
}
tests := []struct {
name string
aws *AWS
args args
wantErr bool
}{
{"ok", p1, args{t1}, true}, // revoke is disabled
{"fail", p1, args{"token"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.aws.AuthorizeRevoke(context.TODO(), tt.args.token); (err != nil) != tt.wantErr {
t.Errorf("AWS.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr)
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
}
})
}

View file

@ -13,6 +13,7 @@ import (
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
@ -209,14 +210,14 @@ func (p *Azure) Init(config Config) (err error) {
return nil
}
// parseToken returns the claims, name, group, error.
func (p *Azure) parseToken(token string) (*azurePayload, string, string, error) {
// authorizeToken returs the claims, name, group, error.
func (p *Azure) authorizeToken(token string) (*azurePayload, string, string, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, "", "", errors.Wrapf(err, "error parsing token")
return nil, "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; error parsing azure token")
}
if len(jwt.Headers) == 0 {
return nil, "", "", errors.New("error parsing token: header is missing")
return nil, "", "", errs.Unauthorized(errors.New("azure.authorizeToken; azure token missing header"))
}
var found bool
@ -229,7 +230,7 @@ func (p *Azure) parseToken(token string) (*azurePayload, string, string, error)
}
}
if !found {
return nil, "", "", errors.New("cannot validate token")
return nil, "", "", errs.Unauthorized(errors.New("azure.authorizeToken; cannot validate azure token"))
}
if err := claims.ValidateWithLeeway(jose.Expected{
@ -237,17 +238,17 @@ func (p *Azure) parseToken(token string) (*azurePayload, string, string, error)
Issuer: p.oidcConfig.Issuer,
Time: time.Now(),
}, 1*time.Minute); err != nil {
return nil, "", "", errors.Wrap(err, "failed to validate payload")
return nil, "", "", errs.Wrap(http.StatusUnauthorized, err, "azure.authorizeToken; failed to validate azure token payload")
}
// Validate TenantID
if claims.TenantID != p.TenantID {
return nil, "", "", errors.New("validation failed: invalid tenant id claim (tid)")
return nil, "", "", errs.Unauthorized(errors.New("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)"))
}
re := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID)
if len(re) != 4 {
return nil, "", "", errors.Errorf("error parsing xms_mirid claim: %s", claims.XMSMirID)
return nil, "", "", errs.Unauthorized(errors.Errorf("azure.authorizeToken; error parsing xms_mirid claim - %s", claims.XMSMirID))
}
group, name := re[2], re[3]
return &claims, name, group, nil
@ -256,9 +257,9 @@ func (p *Azure) parseToken(token string) (*azurePayload, string, string, error)
// AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation.
func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
_, name, group, err := p.parseToken(token)
_, name, group, err := p.authorizeToken(token)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSign")
}
// Filter by resource group
@ -271,7 +272,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
}
}
if !found {
return nil, errors.New("validation failed: invalid resource group")
return nil, errs.Unauthorized(errors.New("azure.AuthorizeSign; azure token validation failed - invalid resource group"))
}
}
@ -301,7 +302,7 @@ func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
// certificate was configured to allow renewals.
func (p *Azure) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
return errs.Unauthorized(errors.Errorf("azure.AuthorizeRenew; renew is disabled for azure provisioner %s", p.GetID()))
}
return nil
}
@ -309,16 +310,16 @@ func (p *Azure) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) erro
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
return nil, errs.Unauthorized(errors.Errorf("azure.AuthorizeSSHSign; sshCA is disabled for provisioner %s", p.GetID()))
}
_, name, _, err := p.parseToken(token)
_, name, _, err := p.authorizeToken(token)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "azure.AuthorizeSSHSign")
}
signOptions := []SignOption{
// set the key id to the token subject
sshCertificateKeyIDModifier(name),
sshCertKeyIDModifier(name),
}
// Default to host + known hostnames
@ -329,7 +330,7 @@ func (p *Azure) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOptio
// Validate user options
signOptions = append(signOptions, sshCertificateOptionsValidator(defaults))
// Set defaults if not given as user options
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
signOptions = append(signOptions, sshCertDefaultsModifier(defaults))
return append(signOptions,
// Set the default extensions.

View file

@ -15,7 +15,10 @@ import (
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
func TestAzure_Getters(t *testing.T) {
@ -209,6 +212,148 @@ func TestAzure_Init(t *testing.T) {
}
}
func TestAzure_authorizeToken(t *testing.T) {
type test struct {
p *Azure
token string
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
p, err := generateAzure()
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("azure.authorizeToken; error parsing azure token"),
}
},
"fail/cannot-validate-sig": func(t *testing.T) test {
p, srv, err := generateAzureWithServer()
assert.FatalError(t, err)
defer srv.Close()
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
tok, err := generateAzureToken("subject", p.oidcConfig.Issuer, azureDefaultAudience,
p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine",
time.Now(), jwk)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("azure.authorizeToken; cannot validate azure token"),
}
},
"fail/invalid-token-issuer": func(t *testing.T) test {
p, srv, err := generateAzureWithServer()
assert.FatalError(t, err)
defer srv.Close()
tok, err := generateAzureToken("subject", "bad-issuer", azureDefaultAudience,
p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("azure.authorizeToken; failed to validate azure token payload"),
}
},
"fail/invalid-tenant-id": func(t *testing.T) test {
p, srv, err := generateAzureWithServer()
assert.FatalError(t, err)
defer srv.Close()
tok, err := generateAzureToken("subject", p.oidcConfig.Issuer, azureDefaultAudience,
"foo", "subscriptionID", "resourceGroup", "virtualMachine",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)"),
}
},
"fail/invalid-xms-mir-id": func(t *testing.T) test {
p, srv, err := generateAzureWithServer()
assert.FatalError(t, err)
defer srv.Close()
jwk := &p.keyStore.keySet.Keys[0]
sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID),
)
assert.FatalError(t, err)
now := time.Now()
claims := azurePayload{
Claims: jose.Claims{
Subject: "subject",
Issuer: p.oidcConfig.Issuer,
IssuedAt: jose.NewNumericDate(now),
NotBefore: jose.NewNumericDate(now),
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
Audience: []string{azureDefaultAudience},
ID: "the-jti",
},
AppID: "the-appid",
AppIDAcr: "the-appidacr",
IdentityProvider: "the-idp",
ObjectID: "the-oid",
TenantID: p.TenantID,
Version: "the-version",
XMSMirID: "foo",
}
tok, err := jose.Signed(sig).Claims(claims).CompactSerialize()
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("azure.authorizeToken; error parsing xms_mirid claim - foo"),
}
},
"ok": func(t *testing.T) test {
p, srv, err := generateAzureWithServer()
assert.FatalError(t, err)
defer srv.Close()
tok, err := generateAzureToken("subject", p.oidcConfig.Issuer, azureDefaultAudience,
p.TenantID, "subscriptionID", "resourceGroup", "virtualMachine",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if claims, name, group, err := tc.p.authorizeToken(tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
assert.Equals(t, claims.Subject, "subject")
assert.Equals(t, claims.Issuer, tc.p.oidcConfig.Issuer)
assert.Equals(t, claims.Audience[0], azureDefaultAudience)
assert.Equals(t, name, "virtualMachine")
assert.Equals(t, group, "resourceGroup")
}
}
})
}
}
func TestAzure_AuthorizeSign(t *testing.T) {
p1, srv, err := generateAzureWithServer()
assert.FatalError(t, err)
@ -283,19 +428,20 @@ func TestAzure_AuthorizeSign(t *testing.T) {
azure *Azure
args args
wantLen int
code int
wantErr bool
}{
{"ok", p1, args{t1}, 4, false},
{"ok", p2, args{t2}, 6, false},
{"ok", p1, args{t11}, 4, false},
{"fail tenant", p3, args{t3}, 0, true},
{"fail resource group", p4, args{t4}, 0, true},
{"fail token", p1, args{"token"}, 0, true},
{"fail issuer", p1, args{failIssuer}, 0, true},
{"fail audience", p1, args{failAudience}, 0, true},
{"fail exp", p1, args{failExp}, 0, true},
{"fail nbf", p1, args{failNbf}, 0, true},
{"fail key", p1, args{failKey}, 0, true},
{"ok", p1, args{t1}, 4, http.StatusOK, false},
{"ok", p2, args{t2}, 6, http.StatusOK, false},
{"ok", p1, args{t11}, 4, http.StatusOK, false},
{"fail tenant", p3, args{t3}, 0, http.StatusUnauthorized, true},
{"fail resource group", p4, args{t4}, 0, http.StatusUnauthorized, true},
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
{"fail issuer", p1, args{failIssuer}, 0, http.StatusUnauthorized, true},
{"fail audience", p1, args{failAudience}, 0, http.StatusUnauthorized, true},
{"fail exp", p1, args{failExp}, 0, http.StatusUnauthorized, true},
{"fail nbf", p1, args{failNbf}, 0, http.StatusUnauthorized, true},
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -304,8 +450,51 @@ func TestAzure_AuthorizeSign(t *testing.T) {
if (err != nil) != tt.wantErr {
t.Errorf("Azure.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
return
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
} else {
assert.Len(t, tt.wantLen, got)
}
})
}
}
func TestAzure_AuthorizeRenew(t *testing.T) {
p1, err := generateAzure()
assert.FatalError(t, err)
p2, err := generateAzure()
assert.FatalError(t, err)
// disable renewal
disable := true
p2.Claims = &Claims{DisableRenewal: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
type args struct {
cert *x509.Certificate
}
tests := []struct {
name string
azure *Azure
args args
code int
wantErr bool
}{
{"ok", p1, args{nil}, http.StatusOK, false},
{"fail/renew-disabled", p2, args{nil}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.azure.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("Azure.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
}
assert.Len(t, tt.wantLen, got)
})
}
}
@ -318,6 +507,14 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) {
assert.FatalError(t, err)
defer srv.Close()
p2, err := generateAzure()
assert.FatalError(t, err)
// disable sshCA
disable := false
p2.Claims = &Claims{EnableSSHCA: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
t1, err := p1.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
@ -349,28 +546,33 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) {
azure *Azure
args args
expected *SSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, false, true},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine", "smallstep.com"}}, pub}, nil, false, true},
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContextWithMethod(context.Background(), SignSSHMethod)
got, err := tt.azure.AuthorizeSSHSign(ctx, tt.args.token)
got, err := tt.azure.AuthorizeSSHSign(context.Background(), tt.args.token)
if (err != nil) != tt.wantErr {
t.Errorf("Azure.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.Nil(t, got)
} else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
@ -388,68 +590,6 @@ func TestAzure_AuthorizeSSHSign(t *testing.T) {
}
}
func TestAzure_AuthorizeRenew(t *testing.T) {
p1, err := generateAzure()
assert.FatalError(t, err)
p2, err := generateAzure()
assert.FatalError(t, err)
// disable renewal
disable := true
p2.Claims = &Claims{DisableRenewal: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
type args struct {
cert *x509.Certificate
}
tests := []struct {
name string
azure *Azure
args args
wantErr bool
}{
{"ok", p1, args{nil}, false},
{"fail", p2, args{nil}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.azure.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("Azure.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestAzure_AuthorizeRevoke(t *testing.T) {
az, srv, err := generateAzureWithServer()
assert.FatalError(t, err)
defer srv.Close()
token, err := az.GetIdentityToken("subject", "caURL")
assert.FatalError(t, err)
type args struct {
token string
}
tests := []struct {
name string
azure *Azure
args args
wantErr bool
}{
{"ok token", az, args{token}, true}, // revoke is disabled
{"bad token", az, args{"bad token"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.azure.AuthorizeRevoke(context.TODO(), tt.args.token); (err != nil) != tt.wantErr {
t.Errorf("Azure.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestAzure_assertConfig(t *testing.T) {
p1, err := generateAzure()
assert.FatalError(t, err)

View file

@ -78,7 +78,7 @@ func (c *Collection) LoadByToken(token *jose.JSONWebToken, claims *jose.Claims)
// match with server audiences
if matchesAudience(claims.Audience, audiences) {
// Use fragment to get provisioner name (GCP, AWS)
// Use fragment to get provisioner name (GCP, AWS, SSHPOP)
if fragment != "" {
return c.Load(fragment)
}

View file

@ -14,6 +14,7 @@ import (
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
@ -210,7 +211,7 @@ func (p *GCP) Init(config Config) error {
func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := p.authorizeToken(token)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "gcp.AuthorizeSign")
}
ce := claims.Google.ComputeEngine
@ -239,10 +240,10 @@ func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
), nil
}
// AuthorizeRenewal returns an error if the renewal is disabled.
func (p *GCP) AuthorizeRenewal(ctx context.Context, cert *x509.Certificate) error {
// AuthorizeRenew returns an error if the renewal is disabled.
func (p *GCP) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
return errs.Unauthorized(errors.Errorf("gcp.AuthorizeRenew; renew is disabled for gcp provisioner %s", p.GetID()))
}
return nil
}
@ -260,10 +261,10 @@ func (p *GCP) assertConfig() {
func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
return nil, errs.Wrap(http.StatusUnauthorized, err, "gcp.authorizeToken; error parsing gcp token")
}
if len(jwt.Headers) == 0 {
return nil, errors.New("error parsing token: header is missing")
return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; error parsing gcp token - header is missing"))
}
var found bool
@ -277,7 +278,7 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
}
}
if !found {
return nil, errors.Errorf("failed to validate payload: cannot find key for kid %s", kid)
return nil, errs.Unauthorized(errors.Errorf("gcp.authorizeToken; failed to validate gcp token payload - cannot find key for kid %s", kid))
}
// According to "rfc7519 JSON Web Token" acceptable skew should be no
@ -287,12 +288,12 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
Issuer: "https://accounts.google.com",
Time: now,
}, time.Minute); err != nil {
return nil, errors.Wrapf(err, "invalid token")
return nil, errs.Wrap(http.StatusUnauthorized, err, "gcp.authorizeToken; invalid gcp token payload")
}
// validate audiences with the defaults
if !matchesAudience(claims.Audience, p.audiences.Sign) {
return nil, errors.New("invalid token: invalid audience claim (aud)")
return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; invalid gcp token - invalid audience claim (aud)"))
}
// validate subject (service account)
@ -305,7 +306,7 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
}
}
if !found {
return nil, errors.New("invalid token: invalid subject claim")
return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; invalid gcp token - invalid subject claim"))
}
}
@ -319,26 +320,26 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
}
}
if !found {
return nil, errors.New("invalid token: invalid project id")
return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; invalid gcp token - invalid project id"))
}
}
// validate instance age
if d := p.InstanceAge.Value(); d > 0 {
if now.Sub(claims.Google.ComputeEngine.InstanceCreationTimestamp.Time()) > d {
return nil, errors.New("token google.compute_engine.instance_creation_timestamp is too old")
return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; token google.compute_engine.instance_creation_timestamp is too old"))
}
}
switch {
case claims.Google.ComputeEngine.InstanceID == "":
return nil, errors.New("token google.compute_engine.instance_id cannot be empty")
return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_id cannot be empty"))
case claims.Google.ComputeEngine.InstanceName == "":
return nil, errors.New("token google.compute_engine.instance_name cannot be empty")
return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_name cannot be empty"))
case claims.Google.ComputeEngine.ProjectID == "":
return nil, errors.New("token google.compute_engine.project_id cannot be empty")
return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.project_id cannot be empty"))
case claims.Google.ComputeEngine.Zone == "":
return nil, errors.New("token google.compute_engine.zone cannot be empty")
return nil, errs.Unauthorized(errors.New("gcp.authorizeToken; gcp token google.compute_engine.zone cannot be empty"))
}
return &claims, nil
@ -347,18 +348,18 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
return nil, errs.Unauthorized(errors.Errorf("gcp.AuthorizeSSHSign; sshCA is disabled for gcp provisioner %s", p.GetID()))
}
claims, err := p.authorizeToken(token)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "gcp.AuthorizeSSHSign")
}
ce := claims.Google.ComputeEngine
signOptions := []SignOption{
// set the key id to the token subject
sshCertificateKeyIDModifier(ce.InstanceName),
sshCertKeyIDModifier(ce.InstanceName),
}
// Default to host + known hostnames
@ -372,7 +373,7 @@ func (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
// Validate user options
signOptions = append(signOptions, sshCertificateOptionsValidator(defaults))
// Set defaults if not given as user options
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
signOptions = append(signOptions, sshCertDefaultsModifier(defaults))
return append(signOptions,
// Set the default extensions

View file

@ -16,7 +16,10 @@ import (
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
func TestGCP_Getters(t *testing.T) {
@ -211,6 +214,202 @@ func TestGCP_Init(t *testing.T) {
}
}
func TestGCP_authorizeToken(t *testing.T) {
type test struct {
p *GCP
token string
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; error parsing gcp token"),
}
},
"fail/cannot-validate-sig": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
tok, err := generateGCPToken(p.ServiceAccounts[0],
"https://accounts.google.com", p.GetID(),
"instance-id", "instance-name", "project-id", "zone",
time.Now(), jwk)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; failed to validate gcp token payload - cannot find key for kid "),
}
},
"fail/invalid-issuer": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
tok, err := generateGCPToken(p.ServiceAccounts[0],
"https://foo.bar.zap", p.GetID(),
"instance-id", "instance-name", "project-id", "zone",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; invalid gcp token payload"),
}
},
"fail/invalid-serviceAccount": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
tok, err := generateGCPToken("foo",
"https://accounts.google.com", p.GetID(),
"instance-id", "instance-name", "project-id", "zone",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; invalid gcp token - invalid subject claim"),
}
},
"fail/invalid-projectID": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
p.ProjectIDs = []string{"foo", "bar"}
tok, err := generateGCPToken(p.ServiceAccounts[0],
"https://accounts.google.com", p.GetID(),
"instance-id", "instance-name", "project-id", "zone",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; invalid gcp token - invalid project id"),
}
},
"fail/instance-age": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
p.InstanceAge = Duration{1 * time.Minute}
tok, err := generateGCPToken(p.ServiceAccounts[0],
"https://accounts.google.com", p.GetID(),
"instance-id", "instance-name", "project-id", "zone",
time.Now().Add(-1*time.Minute), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; token google.compute_engine.instance_creation_timestamp is too old"),
}
},
"fail/empty-instance-id": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
tok, err := generateGCPToken(p.ServiceAccounts[0],
"https://accounts.google.com", p.GetID(),
"", "instance-name", "project-id", "zone",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_id cannot be empty"),
}
},
"fail/empty-instance-name": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
tok, err := generateGCPToken(p.ServiceAccounts[0],
"https://accounts.google.com", p.GetID(),
"instance-id", "", "project-id", "zone",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; gcp token google.compute_engine.instance_name cannot be empty"),
}
},
"fail/empty-project-id": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
tok, err := generateGCPToken(p.ServiceAccounts[0],
"https://accounts.google.com", p.GetID(),
"instance-id", "instance-name", "", "zone",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; gcp token google.compute_engine.project_id cannot be empty"),
}
},
"fail/empty-zone": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
tok, err := generateGCPToken(p.ServiceAccounts[0],
"https://accounts.google.com", p.GetID(),
"instance-id", "instance-name", "project-id", "",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("gcp.authorizeToken; gcp token google.compute_engine.zone cannot be empty"),
}
},
"ok": func(t *testing.T) test {
p, err := generateGCP()
assert.FatalError(t, err)
tok, err := generateGCPToken(p.ServiceAccounts[0],
"https://accounts.google.com", p.GetID(),
"instance-id", "instance-name", "project-id", "zone",
time.Now(), &p.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if claims, err := tc.p.authorizeToken(tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) && assert.NotNil(t, claims) {
assert.Equals(t, claims.Subject, tc.p.ServiceAccounts[0])
assert.Equals(t, claims.Issuer, "https://accounts.google.com")
assert.NotNil(t, claims.Google)
aud, err := generateSignAudience("https://ca.smallstep.com", tc.p.GetID())
assert.FatalError(t, err)
assert.Equals(t, claims.Audience[0], aud)
}
}
})
}
}
func TestGCP_AuthorizeSign(t *testing.T) {
p1, err := generateGCP()
assert.FatalError(t, err)
@ -313,24 +512,25 @@ func TestGCP_AuthorizeSign(t *testing.T) {
gcp *GCP
args args
wantLen int
code int
wantErr bool
}{
{"ok", p1, args{t1}, 4, false},
{"ok", p2, args{t2}, 6, false},
{"ok", p3, args{t3}, 4, false},
{"fail token", p1, args{"token"}, 0, true},
{"fail key", p1, args{failKey}, 0, true},
{"fail iss", p1, args{failIss}, 0, true},
{"fail aud", p1, args{failAud}, 0, true},
{"fail exp", p1, args{failExp}, 0, true},
{"fail nbf", p1, args{failNbf}, 0, true},
{"fail service account", p1, args{failServiceAccount}, 0, true},
{"fail invalid project id", p3, args{failInvalidProjectID}, 0, true},
{"fail invalid instance age", p3, args{failInvalidInstanceAge}, 0, true},
{"fail instance id", p1, args{failInstanceID}, 0, true},
{"fail instance name", p1, args{failInstanceName}, 0, true},
{"fail project id", p1, args{failProjectID}, 0, true},
{"fail zone", p1, args{failZone}, 0, true},
{"ok", p1, args{t1}, 4, http.StatusOK, false},
{"ok", p2, args{t2}, 6, http.StatusOK, false},
{"ok", p3, args{t3}, 4, http.StatusOK, false},
{"fail token", p1, args{"token"}, 0, http.StatusUnauthorized, true},
{"fail key", p1, args{failKey}, 0, http.StatusUnauthorized, true},
{"fail iss", p1, args{failIss}, 0, http.StatusUnauthorized, true},
{"fail aud", p1, args{failAud}, 0, http.StatusUnauthorized, true},
{"fail exp", p1, args{failExp}, 0, http.StatusUnauthorized, true},
{"fail nbf", p1, args{failNbf}, 0, http.StatusUnauthorized, true},
{"fail service account", p1, args{failServiceAccount}, 0, http.StatusUnauthorized, true},
{"fail invalid project id", p3, args{failInvalidProjectID}, 0, http.StatusUnauthorized, true},
{"fail invalid instance age", p3, args{failInvalidInstanceAge}, 0, http.StatusUnauthorized, true},
{"fail instance id", p1, args{failInstanceID}, 0, http.StatusUnauthorized, true},
{"fail instance name", p1, args{failInstanceName}, 0, http.StatusUnauthorized, true},
{"fail project id", p1, args{failProjectID}, 0, http.StatusUnauthorized, true},
{"fail zone", p1, args{failZone}, 0, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -339,8 +539,13 @@ func TestGCP_AuthorizeSign(t *testing.T) {
if (err != nil) != tt.wantErr {
t.Errorf("GCP.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
return
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
} else {
assert.Len(t, tt.wantLen, got)
}
assert.Len(t, tt.wantLen, got)
})
}
}
@ -352,6 +557,14 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
p1, err := generateGCP()
assert.FatalError(t, err)
p2, err := generateGCP()
assert.FatalError(t, err)
// disable sshCA
disable := false
p2.Claims = &Claims{EnableSSHCA: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
t1, err := generateGCPToken(p1.ServiceAccounts[0],
"https://accounts.google.com", p1.GetID(),
"instance-id", "instance-name", "project-id", "zone",
@ -394,30 +607,35 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
gcp *GCP
args args
expected *SSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, false, false},
{"ok-principal1", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal1, false, false},
{"ok-principal2", p1, args{t1, SSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal2, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, false, true},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}, pub}, nil, false, true},
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"ok-principal1", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal1, http.StatusOK, false, false},
{"ok-principal2", p1, args{t1, SSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal2, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, http.StatusOK, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}, pub}, nil, http.StatusOK, false, true},
{"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
{"fail-invalid-token", p1, args{"foo", SSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContextWithMethod(context.Background(), SignSSHMethod)
got, err := tt.gcp.AuthorizeSSHSign(ctx, tt.args.token)
got, err := tt.gcp.AuthorizeSSHSign(context.Background(), tt.args.token)
if (err != nil) != tt.wantErr {
t.Errorf("GCP.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.Nil(t, got)
} else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
@ -435,7 +653,7 @@ func TestGCP_AuthorizeSSHSign(t *testing.T) {
}
}
func TestGCP_AuthorizeRenewal(t *testing.T) {
func TestGCP_AuthorizeRenew(t *testing.T) {
p1, err := generateGCP()
assert.FatalError(t, err)
p2, err := generateGCP()
@ -454,46 +672,20 @@ func TestGCP_AuthorizeRenewal(t *testing.T) {
name string
prov *GCP
args args
code int
wantErr bool
}{
{"ok", p1, args{nil}, false},
{"fail", p2, args{nil}, true},
{"ok", p1, args{nil}, http.StatusOK, false},
{"fail/renewal-disabled", p2, args{nil}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.prov.AuthorizeRenewal(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("GCP.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestGCP_AuthorizeRevoke(t *testing.T) {
p1, err := generateGCP()
assert.FatalError(t, err)
t1, err := generateGCPToken(p1.ServiceAccounts[0],
"https://accounts.google.com", p1.GetID(),
"instance-id", "instance-name", "project-id", "zone",
time.Now(), &p1.keyStore.keySet.Keys[0])
assert.FatalError(t, err)
type args struct {
token string
}
tests := []struct {
name string
gcp *GCP
args args
wantErr bool
}{
{"ok", p1, args{t1}, true}, // revoke is disabled
{"fail", p1, args{"token"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.gcp.AuthorizeRevoke(context.TODO(), tt.args.token); (err != nil) != tt.wantErr {
t.Errorf("GCP.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr)
if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("GCP.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
}
})
}

View file

@ -3,9 +3,11 @@ package provisioner
import (
"context"
"crypto/x509"
"net/http"
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/jose"
)
@ -99,12 +101,12 @@ func (p *JWK) Init(config Config) (err error) {
func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
return nil, errs.Wrap(http.StatusUnauthorized, err, "jwk.authorizeToken; error parsing jwk token")
}
var claims jwtPayload
if err = jwt.Claims(p.Key, &claims); err != nil {
return nil, errors.Wrap(err, "error parsing claims")
return nil, errs.Wrap(http.StatusUnauthorized, err, "jwk.authorizeToken; error parsing jwk claims")
}
// According to "rfc7519 JSON Web Token" acceptable skew should be no
@ -113,17 +115,17 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err
Issuer: p.Name,
Time: time.Now().UTC(),
}, time.Minute); err != nil {
return nil, errors.Wrapf(err, "invalid token")
return nil, errs.Wrapf(http.StatusUnauthorized, err, "jwk.authorizeToken; invalid jwk claims")
}
// validate audiences with the defaults
if !matchesAudience(claims.Audience, audiences) {
return nil, errors.Errorf("invalid token: invalid audience claim (aud); want %s, but got %s",
audiences, claims.Audience)
return nil, errs.Unauthorized(errors.Errorf("jwk.authorizeToken; invalid jwk token audience claim (aud); want %s, but got %s",
audiences, claims.Audience))
}
if claims.Subject == "" {
return nil, errors.New("token subject cannot be empty")
return nil, errs.Unauthorized(errors.New("jwk.authorizeToken; jwk token subject cannot be empty"))
}
return &claims, nil
@ -133,14 +135,14 @@ func (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, err
// revoke the certificate with serial number in the `sub` property.
func (p *JWK) AuthorizeRevoke(ctx context.Context, token string) error {
_, err := p.authorizeToken(token, p.audiences.Revoke)
return err
return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeRevoke")
}
// AuthorizeSign validates the given token.
func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := p.authorizeToken(token, p.audiences.Sign)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSign")
}
// NOTE: This is for backwards compatibility with older versions of cli
@ -171,7 +173,7 @@ func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
// certificate was configured to allow renewals.
func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
return errs.Unauthorized(errors.Errorf("jwk.AuthorizeRenew; renew is disabled for jwk provisioner %s", p.GetID()))
}
return nil
}
@ -179,14 +181,14 @@ func (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
return nil, errs.Unauthorized(errors.Errorf("jwk.AuthorizeSSHSign; sshCA is disabled for jwk provisioner %s", p.GetID()))
}
claims, err := p.authorizeToken(token, p.audiences.SSHSign)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSSHSign")
}
if claims.Step == nil || claims.Step.SSH == nil {
return nil, errors.New("authorization token must be an SSH provisioning token")
return nil, errs.Unauthorized(errors.New("jwk.AuthorizeSSHSign; jwk token must be an SSH provisioning token"))
}
opts := claims.Step.SSH
@ -205,19 +207,19 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
signOptions = append(signOptions, sshCertPrincipalsModifier(opts.Principals))
}
if !opts.ValidAfter.IsZero() {
signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix()))
signOptions = append(signOptions, sshCertValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix()))
}
if !opts.ValidBefore.IsZero() {
signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix()))
signOptions = append(signOptions, sshCertValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix()))
}
if opts.KeyID != "" {
signOptions = append(signOptions, sshCertificateKeyIDModifier(opts.KeyID))
signOptions = append(signOptions, sshCertKeyIDModifier(opts.KeyID))
} else {
signOptions = append(signOptions, sshCertificateKeyIDModifier(claims.Subject))
signOptions = append(signOptions, sshCertKeyIDModifier(claims.Subject))
}
// Default to a user certificate with no principals if not set
signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert})
signOptions = append(signOptions, sshCertDefaultsModifier{CertType: SSHUserCert})
return append(signOptions,
// Set the default extensions.
@ -238,5 +240,5 @@ func (p *JWK) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
// AuthorizeSSHRevoke returns nil if the token is valid, false otherwise.
func (p *JWK) AuthorizeSSHRevoke(ctx context.Context, token string) error {
_, err := p.authorizeToken(token, p.audiences.SSHRevoke)
return err
return errs.Wrap(http.StatusInternalServerError, err, "jwk.AuthorizeSSHRevoke")
}

View file

@ -7,12 +7,14 @@ import (
"crypto/rsa"
"crypto/x509"
"net"
"net/http"
"strings"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
@ -162,25 +164,29 @@ func TestJWK_authorizeToken(t *testing.T) {
name string
prov *JWK
args args
code int
err error
}{
{"fail-token", p1, args{failTok}, errors.New("error parsing token")},
{"fail-key", p1, args{failKey}, errors.New("error parsing claims")},
{"fail-claims", p1, args{failClaims}, errors.New("error parsing claims")},
{"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")},
{"fail-issuer", p1, args{failIss}, errors.New("invalid token: square/go-jose/jwt: validation failed, invalid issuer claim (iss)")},
{"fail-expired", p1, args{failExp}, errors.New("invalid token: square/go-jose/jwt: validation failed, token is expired (exp)")},
{"fail-not-before", p1, args{failNbf}, errors.New("invalid token: square/go-jose/jwt: validation failed, token not valid yet (nbf)")},
{"fail-audience", p1, args{failAud}, errors.New("invalid token: invalid audience claim (aud)")},
{"fail-subject", p1, args{failSub}, errors.New("token subject cannot be empty")},
{"ok", p1, args{t1}, nil},
{"ok-no-encrypted-key", p2, args{t2}, nil},
{"ok-no-sans", p1, args{t3}, nil},
{"fail-token", p1, args{failTok}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk token")},
{"fail-key", p1, args{failKey}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims")},
{"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims")},
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; error parsing jwk claims: square/go-jose: error in cryptographic primitive")},
{"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: square/go-jose/jwt: validation failed, invalid issuer claim (iss)")},
{"fail-expired", p1, args{failExp}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: square/go-jose/jwt: validation failed, token is expired (exp)")},
{"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk claims: square/go-jose/jwt: validation failed, token not valid yet (nbf)")},
{"fail-audience", p1, args{failAud}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; invalid jwk token audience claim (aud)")},
{"fail-subject", p1, args{failSub}, http.StatusUnauthorized, errors.New("jwk.authorizeToken; jwk token subject cannot be empty")},
{"ok", p1, args{t1}, http.StatusOK, nil},
{"ok-no-encrypted-key", p2, args{t2}, http.StatusOK, nil},
{"ok-no-sans", p1, args{t3}, http.StatusOK, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, err := tt.prov.authorizeToken(tt.args.token, testAudiences.Sign); err != nil {
if assert.NotNil(t, tt.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
} else {
@ -208,15 +214,19 @@ func TestJWK_AuthorizeRevoke(t *testing.T) {
name string
prov *JWK
args args
code int
err error
}{
{"fail-signature", p1, args{failSig}, errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")},
{"ok", p1, args{t1}, nil},
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, errors.New("jwk.AuthorizeRevoke: jwk.authorizeToken; error parsing jwk claims: square/go-jose: error in cryptographic primitive")},
{"ok", p1, args{t1}, http.StatusOK, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.prov.AuthorizeRevoke(context.TODO(), tt.args.token); err != nil {
if assert.NotNil(t, tt.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
}
@ -246,20 +256,24 @@ func TestJWK_AuthorizeSign(t *testing.T) {
name string
prov *JWK
args args
code int
err error
dns []string
emails []string
ips []net.IP
}{
{name: "fail-signature", prov: p1, args: args{failSig}, err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive")},
{"ok-sans", p1, args{t1}, nil, []string{"foo"}, []string{"max@smallstep.com"}, []net.IP{net.ParseIP("127.0.0.1")}},
{"ok-no-sans", p1, args{t2}, nil, []string{"subject"}, []string{}, []net.IP{}},
{name: "fail-signature", prov: p1, args: args{failSig}, code: http.StatusUnauthorized, err: errors.New("jwk.AuthorizeSign: jwk.authorizeToken; error parsing jwk claims: square/go-jose: error in cryptographic primitive")},
{"ok-sans", p1, args{t1}, http.StatusOK, nil, []string{"foo"}, []string{"max@smallstep.com"}, []net.IP{net.ParseIP("127.0.0.1")}},
{"ok-no-sans", p1, args{t2}, http.StatusOK, nil, []string{"subject"}, []string{}, []net.IP{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContextWithMethod(context.Background(), SignMethod)
if got, err := tt.prov.AuthorizeSign(ctx, tt.args.token); err != nil {
if assert.NotNil(t, tt.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.HasPrefix(t, err.Error(), tt.err.Error())
}
} else {
@ -315,15 +329,20 @@ func TestJWK_AuthorizeRenew(t *testing.T) {
name string
prov *JWK
args args
code int
wantErr bool
}{
{"ok", p1, args{nil}, false},
{"fail", p2, args{nil}, true},
{"ok", p1, args{nil}, http.StatusOK, false},
{"fail/renew-disabled", p2, args{nil}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("JWK.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
}
})
}
@ -335,6 +354,14 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
p1, err := generateJWK()
assert.FatalError(t, err)
p2, err := generateJWK()
assert.FatalError(t, err)
// disable sshCA
disable := false
p2.Claims = &Claims{EnableSSHCA: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
jwk, err := decryptJSONWebKey(p1.EncryptedKey)
assert.FatalError(t, err)
@ -382,30 +409,34 @@ func TestJWK_AuthorizeSSHSign(t *testing.T) {
prov *JWK
args args
expected *SSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"user", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, false, false},
{"user-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, false, false},
{"user-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, false, false},
{"user-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, expectedUserOptions, false, false},
{"user-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, false, false},
{"host", p1, args{t2, SSHOptions{}, pub}, expectedHostOptions, false, false},
{"host-type", p1, args{t2, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false},
{"host-principals", p1, args{t2, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false},
{"host-options", p1, args{t2, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false},
{"fail-signature", p1, args{failSig, SSHOptions{}, pub}, nil, true, false},
{"rail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, false, true},
{"user", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"user-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false},
{"user-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"user-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"user-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"host", p1, args{t2, SSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-type", p1, args{t2, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-principals", p1, args{t2, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"host-options", p1, args{t2, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, http.StatusOK, false, false},
{"fail-sshCA-disabled", p2, args{"foo", SSHOptions{}, pub}, expectedUserOptions, http.StatusUnauthorized, true, false},
{"fail-signature", p1, args{failSig, SSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
{"rail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContextWithMethod(context.Background(), SignSSHMethod)
got, err := tt.prov.AuthorizeSSHSign(ctx, tt.args.token)
got, err := tt.prov.AuthorizeSSHSign(context.Background(), tt.args.token)
if (err != nil) != tt.wantErr {
t.Errorf("JWK.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.Nil(t, got)
} else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
@ -511,10 +542,9 @@ func TestJWK_AuthorizeSign_SSHOptions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContextWithMethod(context.Background(), SignSSHMethod)
token, err := generateSSHToken(tt.args.sub, tt.args.iss, tt.args.aud, tt.args.iat, tt.args.tokSSHOpts, tt.args.jwk)
assert.FatalError(t, err)
if got, err := tt.prov.AuthorizeSSHSign(ctx, token); (err != nil) != tt.wantErr {
if got, err := tt.prov.AuthorizeSSHSign(context.Background(), token); (err != nil) != tt.wantErr {
t.Errorf("JWK.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr)
} else if !tt.wantErr && assert.NotNil(t, got) {
var opts SSHOptions
@ -535,3 +565,52 @@ func TestJWK_AuthorizeSign_SSHOptions(t *testing.T) {
})
}
}
func TestJWK_AuthorizeSSHRevoke(t *testing.T) {
type test struct {
p *JWK
token string
code int
err error
}
tests := map[string]func(*testing.T) test{
"fail/invalid-token": func(t *testing.T) test {
p, err := generateJWK()
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("jwk.AuthorizeSSHRevoke: jwk.authorizeToken; error parsing jwk token"),
}
},
"ok": func(t *testing.T) test {
p, err := generateJWK()
assert.FatalError(t, err)
jwk, err := decryptJSONWebKey(p.EncryptedKey)
assert.FatalError(t, err)
tok, err := generateToken("subject", p.Name, testAudiences.SSHRevoke[0], "name@smallstep.com", []string{"127.0.0.1", "max@smallstep.com", "foo"}, time.Now(), jwk)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
assert.Nil(t, tc.err)
}
})
}
}

View file

@ -6,8 +6,10 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"net/http"
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ed25519"
@ -138,7 +140,8 @@ func (p *K8sSA) Init(config Config) (err error) {
func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
return nil, errs.Wrap(http.StatusUnauthorized, err,
"k8ssa.authorizeToken; error parsing k8sSA token")
}
var (
@ -146,7 +149,7 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload,
claims k8sSAPayload
)
if p.pubKeys == nil {
return nil, errors.New("TokenReview API integration not implemented")
return nil, errs.Unauthorized(errors.New("k8ssa.authorizeToken; k8sSA TokenReview API integration not implemented"))
/* NOTE: We plan to support the TokenReview API in a future release.
Below is some code that should be useful when we prioritize
this integration.
@ -174,7 +177,7 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload,
}
}
if !valid {
return nil, errors.New("error validating token and extracting claims")
return nil, errs.Unauthorized(errors.New("k8ssa.authorizeToken; error validating k8sSA token and extracting claims"))
}
// According to "rfc7519 JSON Web Token" acceptable skew should be no
@ -182,11 +185,11 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload,
if err = claims.Validate(jose.Expected{
Issuer: k8sSAIssuer,
}); err != nil {
return nil, errors.Wrapf(err, "invalid token claims")
return nil, errs.Wrap(http.StatusUnauthorized, err, "k8ssa.authorizeToken; invalid k8sSA token claims")
}
if claims.Subject == "" {
return nil, errors.New("token subject cannot be empty")
return nil, errs.Unauthorized(errors.New("k8ssa.authorizeToken; k8sSA token subject cannot be empty"))
}
return &claims, nil
@ -196,14 +199,13 @@ func (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload,
// revoke the certificate with serial number in the `sub` property.
func (p *K8sSA) AuthorizeRevoke(ctx context.Context, token string) error {
_, err := p.authorizeToken(token, p.audiences.Revoke)
return err
return errs.Wrap(http.StatusInternalServerError, err, "k8ssa.AuthorizeRevoke")
}
// AuthorizeSign validates the given token.
func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
_, err := p.authorizeToken(token, p.audiences.Sign)
if err != nil {
return nil, err
if _, err := p.authorizeToken(token, p.audiences.Sign); err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "k8ssa.AuthorizeSign")
}
return []SignOption{
@ -219,7 +221,7 @@ func (p *K8sSA) AuthorizeSign(ctx context.Context, token string) ([]SignOption,
// AuthorizeRenew returns an error if the renewal is disabled.
func (p *K8sSA) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
return errs.Unauthorized(errors.Errorf("k8ssa.AuthorizeRenew; renew is disabled for k8sSA provisioner %s", p.GetID()))
}
return nil
}
@ -227,17 +229,14 @@ func (p *K8sSA) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) erro
// AuthorizeSSHSign validates an request for an SSH certificate.
func (p *K8sSA) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("authorizeSSHSign: ssh ca is disabled for provisioner %s", p.GetID())
return nil, errs.Unauthorized(errors.Errorf("k8ssa.AuthorizeSSHSign; sshCA is disabled for k8sSA provisioner %s", p.GetID()))
}
_, err := p.authorizeToken(token, p.audiences.SSHSign)
if err != nil {
return nil, errors.Wrap(err, "authorizeSSHSign")
if _, err := p.authorizeToken(token, p.audiences.SSHSign); err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "k8ssa.AuthorizeSSHSign")
}
// Default to a user certificate with no principals if not set
signOptions := []SignOption{
sshCertificateDefaultsModifier{CertType: SSHUserCert},
}
signOptions := []SignOption{sshCertDefaultsModifier{CertType: SSHUserCert}}
return append(signOptions,
// Set the default extensions.

View file

@ -3,11 +3,13 @@ package provisioner
import (
"context"
"crypto/x509"
"net/http"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
@ -36,6 +38,7 @@ func TestK8sSA_authorizeToken(t *testing.T) {
p *K8sSA
token string
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
@ -44,7 +47,24 @@ func TestK8sSA_authorizeToken(t *testing.T) {
return test{
p: p,
token: "foo",
err: errors.New("error parsing token"),
code: http.StatusUnauthorized,
err: errors.New("k8ssa.authorizeToken; error parsing k8sSA token"),
}
},
"fail/not-implemented": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
p, err := generateK8sSA(nil)
assert.FatalError(t, err)
tok, err := generateToken("", p.Name, testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk)
p.pubKeys = nil
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("k8ssa.authorizeToken; k8sSA TokenReview API integration not implemented"),
code: http.StatusUnauthorized,
}
},
"fail/error-validating-token": func(t *testing.T) test {
@ -58,7 +78,8 @@ func TestK8sSA_authorizeToken(t *testing.T) {
return test{
p: p,
token: tok,
err: errors.New("error validating token and extracting claims"),
err: errors.New("k8ssa.authorizeToken; error validating k8sSA token and extracting claims"),
code: http.StatusUnauthorized,
}
},
"fail/invalid-issuer": func(t *testing.T) test {
@ -73,7 +94,8 @@ func TestK8sSA_authorizeToken(t *testing.T) {
return test{
p: p,
token: tok,
err: errors.New("invalid token claims: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"),
code: http.StatusUnauthorized,
err: errors.New("k8ssa.authorizeToken; invalid k8sSA token claims: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"),
}
},
"ok": func(t *testing.T) test {
@ -94,6 +116,9 @@ func TestK8sSA_authorizeToken(t *testing.T) {
tc := tt(t)
if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
@ -105,12 +130,12 @@ func TestK8sSA_authorizeToken(t *testing.T) {
}
}
func TestK8sSA_AuthorizeSign(t *testing.T) {
func TestK8sSA_AuthorizeRevoke(t *testing.T) {
type test struct {
p *K8sSA
token string
ctx context.Context
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/invalid-token": func(t *testing.T) test {
@ -119,21 +144,8 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
return test{
p: p,
token: "foo",
err: errors.New("error parsing token"),
}
},
"fail/ssh-unimplemented": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
p, err := generateK8sSA(jwk.Public().Key)
assert.FatalError(t, err)
tok, err := generateK8sSAToken(jwk, nil)
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignSSHMethod),
token: tok,
err: errors.Errorf("ssh certificates not enabled for k8s ServiceAccount provisioners"),
code: http.StatusUnauthorized,
err: errors.New("k8ssa.AuthorizeRevoke: k8ssa.authorizeToken; error parsing k8sSA token"),
}
},
"ok": func(t *testing.T) test {
@ -145,7 +157,6 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignMethod),
token: tok,
}
},
@ -153,10 +164,110 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if opts, err := tc.p.AuthorizeSign(tc.ctx, tc.token); err != nil {
if err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
func TestK8sSA_AuthorizeRenew(t *testing.T) {
type test struct {
p *K8sSA
cert *x509.Certificate
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/renew-disabled": func(t *testing.T) test {
p, err := generateK8sSA(nil)
assert.FatalError(t, err)
// disable renewal
disable := true
p.Claims = &Claims{DisableRenewal: &disable}
p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
return test{
p: p,
cert: &x509.Certificate{},
code: http.StatusUnauthorized,
err: errors.Errorf("k8ssa.AuthorizeRenew; renew is disabled for k8sSA provisioner %s", p.GetID()),
}
},
"ok": func(t *testing.T) test {
p, err := generateK8sSA(nil)
assert.FatalError(t, err)
return test{
p: p,
cert: &x509.Certificate{},
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
func TestK8sSA_AuthorizeSign(t *testing.T) {
type test struct {
p *K8sSA
token string
code int
err error
}
tests := map[string]func(*testing.T) test{
"fail/invalid-token": func(t *testing.T) test {
p, err := generateK8sSA(nil)
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("k8ssa.AuthorizeSign: k8ssa.authorizeToken; error parsing k8sSA token"),
}
},
"ok": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
p, err := generateK8sSA(jwk.Public().Key)
assert.FatalError(t, err)
tok, err := generateK8sSAToken(jwk, nil)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
if assert.NotNil(t, opts) {
@ -187,20 +298,37 @@ func TestK8sSA_AuthorizeSign(t *testing.T) {
}
}
func TestK8sSA_AuthorizeRevoke(t *testing.T) {
func TestK8sSA_AuthorizeSSHSign(t *testing.T) {
type test struct {
p *K8sSA
token string
code int
err error
}
tests := map[string]func(*testing.T) test{
"fail/sshCA-disabled": func(t *testing.T) test {
p, err := generateK8sSA(nil)
assert.FatalError(t, err)
// disable sshCA
disable := false
p.Claims = &Claims{EnableSSHCA: &disable}
p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.Errorf("k8ssa.AuthorizeSSHSign; sshCA is disabled for k8sSA provisioner %s", p.GetID()),
}
},
"fail/invalid-token": func(t *testing.T) test {
p, err := generateK8sSA(nil)
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
err: errors.New("error parsing token"),
code: http.StatusUnauthorized,
err: errors.New("k8ssa.AuthorizeSSHSign: k8ssa.authorizeToken; error parsing k8sSA token"),
}
},
"ok": func(t *testing.T) test {
@ -219,45 +347,36 @@ func TestK8sSA_AuthorizeRevoke(t *testing.T) {
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if err := tc.p.AuthorizeRevoke(context.TODO(), tc.token); err != nil {
if opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
func TestK8sSA_AuthorizeRenew(t *testing.T) {
p1, err := generateK8sSA(nil)
assert.FatalError(t, err)
p2, err := generateK8sSA(nil)
assert.FatalError(t, err)
// disable renewal
disable := true
p2.Claims = &Claims{DisableRenewal: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
type args struct {
cert *x509.Certificate
}
tests := []struct {
name string
prov *K8sSA
args args
wantErr bool
}{
{"ok", p1, args{nil}, false},
{"fail", p2, args{nil}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("X5C.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
if assert.Nil(t, tc.err) {
if assert.NotNil(t, opts) {
tot := 0
for _, o := range opts {
switch v := o.(type) {
case sshCertDefaultsModifier:
assert.Equals(t, v.CertType, SSHUserCert)
case *sshDefaultExtensionModifier:
case *sshCertificateValidityValidator:
assert.Equals(t, v.Claimer, tc.p.claimer)
case *sshDefaultPublicKeyValidator:
case *sshCertificateDefaultValidator:
case *sshDefaultDuration:
assert.Equals(t, v.Claimer, tc.p.claimer)
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}
tot++
}
assert.Equals(t, tot, 6)
}
}
}
})
}

View file

@ -16,14 +16,16 @@ const (
SignMethod Method = iota
// RevokeMethod is the method used to revoke X.509 certificates.
RevokeMethod
// SignSSHMethod is the method used to sign SSH certificates.
SignSSHMethod
// RenewSSHMethod is the method used to renew SSH certificates.
RenewSSHMethod
// RevokeSSHMethod is the method used to revoke SSH certificates.
RevokeSSHMethod
// RekeySSHMethod is the method used to rekey SSH certificates.
RekeySSHMethod
// RenewMethod is the method used to renew X.509 certificates.
RenewMethod
// SSHSignMethod is the method used to sign SSH certificates.
SSHSignMethod
// SSHRenewMethod is the method used to renew SSH certificates.
SSHRenewMethod
// SSHRevokeMethod is the method used to revoke SSH certificates.
SSHRevokeMethod
// SSHRekeyMethod is the method used to rekey SSH certificates.
SSHRekeyMethod
)
// String returns a string representation of the context method.
@ -33,14 +35,16 @@ func (m Method) String() string {
return "sign-method"
case RevokeMethod:
return "revoke-method"
case SignSSHMethod:
return "sign-ssh-method"
case RenewSSHMethod:
return "renew-ssh-method"
case RevokeSSHMethod:
return "revoke-ssh-method"
case RekeySSHMethod:
return "rekey-ssh-method"
case RenewMethod:
return "renew-method"
case SSHSignMethod:
return "ssh-sign-method"
case SSHRenewMethod:
return "ssh-renew-method"
case SSHRevokeMethod:
return "ssh-revoke-method"
case SSHRekeyMethod:
return "ssh-rekey-method"
default:
return "unknown"
}

View file

@ -12,6 +12,7 @@ import (
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
@ -189,17 +190,17 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error {
Audience: jose.Audience{o.ClientID},
Time: time.Now().UTC(),
}, time.Minute); err != nil {
return errors.Wrap(err, "failed to validate payload")
return errs.Wrap(http.StatusUnauthorized, err, "validatePayload: failed to validate oidc token payload")
}
// Validate azp if present
if p.AuthorizedParty != "" && p.AuthorizedParty != o.ClientID {
return errors.New("failed to validate payload: invalid azp")
return errs.Unauthorized(errors.New("validatePayload: failed to validate oidc token payload: invalid azp"))
}
// Enforce an email claim
if p.Email == "" {
return errors.New("failed to validate payload: email not found")
return errs.Unauthorized(errors.New("validatePayload: failed to validate oidc token payload: email not found"))
}
// Validate domains (case-insensitive)
@ -213,7 +214,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error {
}
}
if !found {
return errors.New("failed to validate payload: email is not allowed")
return errs.Unauthorized(errors.New("validatePayload: failed to validate oidc token payload: email is not allowed"))
}
}
@ -229,7 +230,7 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error {
}
}
if !found {
return errors.New("validation failed: invalid group")
return errs.Unauthorized(errors.New("validatePayload: oidc token payload validation failed: invalid group"))
}
}
@ -241,13 +242,15 @@ func (o *OIDC) ValidatePayload(p openIDPayload) error {
func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
return nil, errs.Wrap(http.StatusUnauthorized, err,
"oidc.AuthorizeToken; error parsing oidc token")
}
// Parse claims to get the kid
var claims openIDPayload
if err := jwt.UnsafeClaimsWithoutVerification(&claims); err != nil {
return nil, errors.Wrap(err, "error parsing claims")
return nil, errs.Wrap(http.StatusUnauthorized, err,
"oidc.AuthorizeToken; error parsing oidc token claims")
}
found := false
@ -260,11 +263,11 @@ func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) {
}
}
if !found {
return nil, errors.New("cannot validate token")
return nil, errs.Unauthorized(errors.New("oidc.AuthorizeToken; cannot validate oidc token"))
}
if err := o.ValidatePayload(claims); err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeToken")
}
return &claims, nil
@ -276,21 +279,21 @@ func (o *OIDC) authorizeToken(token string) (*openIDPayload, error) {
func (o *OIDC) AuthorizeRevoke(ctx context.Context, token string) error {
claims, err := o.authorizeToken(token)
if err != nil {
return err
return errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeRevoke")
}
// Only admins can revoke certificates.
if o.IsAdmin(claims.Email) {
return nil
}
return errors.New("cannot revoke with non-admin token")
return errs.Unauthorized(errors.New("oidc.AuthorizeRevoke; cannot revoke with non-admin oidc token"))
}
// AuthorizeSign validates the given token.
func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := o.authorizeToken(token)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSign")
}
so := []SignOption{
@ -315,7 +318,7 @@ func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, e
// certificate was configured to allow renewals.
func (o *OIDC) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if o.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", o.GetID())
return errs.Unauthorized(errors.Errorf("oidc.AuthorizeRenew; renew is disabled for oidc provisioner %s", o.GetID()))
}
return nil
}
@ -323,22 +326,22 @@ func (o *OIDC) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !o.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", o.GetID())
return nil, errs.Unauthorized(errors.Errorf("oidc.AuthorizeSSHSign; sshCA is disabled for oidc provisioner %s", o.GetID()))
}
claims, err := o.authorizeToken(token)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSSHSign")
}
signOptions := []SignOption{
// set the key id to the token email
sshCertificateKeyIDModifier(claims.Email),
sshCertKeyIDModifier(claims.Email),
}
// Get the identity using either the default identityFunc or one injected
// externally.
iden, err := o.getIdentityFunc(o, claims.Email)
if err != nil {
return nil, errors.Wrap(err, "authorizeSSHSign")
return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSSHSign")
}
defaults := SSHOptions{
CertType: SSHUserCert,
@ -354,7 +357,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
// Default to a user certificate with usernames as principals if those options
// are not set.
signOptions = append(signOptions, sshCertificateDefaultsModifier(defaults))
signOptions = append(signOptions, sshCertDefaultsModifier(defaults))
return append(signOptions,
// Set the default extensions
@ -374,14 +377,14 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
func (o *OIDC) AuthorizeSSHRevoke(ctx context.Context, token string) error {
claims, err := o.authorizeToken(token)
if err != nil {
return err
return errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSSHRevoke")
}
// Only admins can revoke certificates.
if o.IsAdmin(claims.Email) {
return nil
if !o.IsAdmin(claims.Email) {
return errs.Unauthorized(errors.New("oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token"))
}
return errors.New("cannot revoke with non-admin token")
return nil
}
func getAndDecode(uri string, v interface{}) error {

View file

@ -7,12 +7,14 @@ import (
"crypto/rsa"
"crypto/x509"
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
)
@ -206,20 +208,21 @@ func TestOIDC_authorizeToken(t *testing.T) {
name string
prov *OIDC
args args
code int
wantErr bool
}{
{"ok1", p1, args{t1}, false},
{"ok2", p2, args{t2}, false},
{"fail-email", p3, args{failEmail}, true},
{"fail-domain", p3, args{failDomain}, true},
{"fail-key", p1, args{failKey}, true},
{"fail-token", p1, args{failTok}, true},
{"fail-claims", p1, args{failClaims}, true},
{"fail-issuer", p1, args{failIss}, true},
{"fail-audience", p1, args{failAud}, true},
{"fail-signature", p1, args{failSig}, true},
{"fail-expired", p1, args{failExp}, true},
{"fail-not-before", p1, args{failNbf}, true},
{"ok1", p1, args{t1}, http.StatusOK, false},
{"ok2", p2, args{t2}, http.StatusOK, false},
{"fail-email", p3, args{failEmail}, http.StatusUnauthorized, true},
{"fail-domain", p3, args{failDomain}, http.StatusUnauthorized, true},
{"fail-key", p1, args{failKey}, http.StatusUnauthorized, true},
{"fail-token", p1, args{failTok}, http.StatusUnauthorized, true},
{"fail-claims", p1, args{failClaims}, http.StatusUnauthorized, true},
{"fail-issuer", p1, args{failIss}, http.StatusUnauthorized, true},
{"fail-audience", p1, args{failAud}, http.StatusUnauthorized, true},
{"fail-signature", p1, args{failSig}, http.StatusUnauthorized, true},
{"fail-expired", p1, args{failExp}, http.StatusUnauthorized, true},
{"fail-not-before", p1, args{failNbf}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -230,6 +233,9 @@ func TestOIDC_authorizeToken(t *testing.T) {
return
}
if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.Nil(t, got)
} else {
assert.NotNil(t, got)
@ -282,21 +288,24 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
name string
prov *OIDC
args args
code int
wantErr bool
}{
{"ok1", p1, args{t1}, false},
{"admin", p3, args{okAdmin}, false},
{"fail-email", p3, args{failEmail}, true},
{"ok1", p1, args{t1}, http.StatusOK, false},
{"admin", p3, args{okAdmin}, http.StatusOK, false},
{"fail-email", p3, args{failEmail}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContextWithMethod(context.Background(), SignMethod)
got, err := tt.prov.AuthorizeSign(ctx, tt.args.token)
got, err := tt.prov.AuthorizeSign(context.Background(), tt.args.token)
if (err != nil) != tt.wantErr {
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.Nil(t, got)
} else {
if assert.NotNil(t, got) {
@ -330,6 +339,107 @@ func TestOIDC_AuthorizeSign(t *testing.T) {
}
}
func TestOIDC_AuthorizeRevoke(t *testing.T) {
srv := generateJWKServer(2)
defer srv.Close()
var keys jose.JSONWebKeySet
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
// Create test provisioners
p1, err := generateOIDC()
assert.FatalError(t, err)
p3, err := generateOIDC()
assert.FatalError(t, err)
// Admin + Domains
p3.Admins = []string{"name@smallstep.com", "root@example.com"}
p3.Domains = []string{"smallstep.com"}
// Update configuration endpoints and initialize
config := Config{Claims: globalProvisionerClaims}
p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
p3.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
assert.FatalError(t, p1.Init(config))
assert.FatalError(t, p3.Init(config))
t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0])
assert.FatalError(t, err)
// Admin email not in domains
okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
// Invalid email
failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
type args struct {
token string
}
tests := []struct {
name string
prov *OIDC
args args
code int
wantErr bool
}{
{"ok1", p1, args{t1}, http.StatusUnauthorized, true},
{"admin", p3, args{okAdmin}, http.StatusOK, false},
{"fail-email", p3, args{failEmail}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prov.AuthorizeRevoke(context.Background(), tt.args.token)
if (err != nil) != tt.wantErr {
fmt.Println(tt)
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
return
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
}
})
}
}
func TestOIDC_AuthorizeRenew(t *testing.T) {
p1, err := generateOIDC()
assert.FatalError(t, err)
p2, err := generateOIDC()
assert.FatalError(t, err)
// disable renewal
disable := true
p2.Claims = &Claims{DisableRenewal: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
type args struct {
cert *x509.Certificate
}
tests := []struct {
name string
prov *OIDC
args args
code int
wantErr bool
}{
{"ok", p1, args{nil}, http.StatusOK, false},
{"fail/renew-disabled", p2, args{nil}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prov.AuthorizeRenew(context.Background(), tt.args.cert)
if (err != nil) != tt.wantErr {
t.Errorf("OIDC.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
}
})
}
}
func TestOIDC_AuthorizeSSHSign(t *testing.T) {
tm, fn := mockNow()
defer fn()
@ -351,9 +461,16 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
assert.FatalError(t, err)
p5, err := generateOIDC()
assert.FatalError(t, err)
p6, err := generateOIDC()
assert.FatalError(t, err)
// Admin + Domains
p3.Admins = []string{"name@smallstep.com", "root@example.com"}
p3.Domains = []string{"smallstep.com"}
// disable sshCA
disable := false
p6.Claims = &Claims{EnableSSHCA: &disable}
p6.claimer, err = NewClaimer(p6.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
// Update configuration endpoints and initialize
config := Config{Claims: globalProvisionerClaims}
@ -425,48 +542,53 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
prov *OIDC
args args
expected *SSHOptions
code int
wantErr bool
wantSignErr bool
}{
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, false, false},
{"ok-user", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, false, false},
{"ok", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false},
{"ok-user", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, http.StatusOK, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"name"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"ok-principals-getIdentity", p4, args{okGetIdentityToken, SSHOptions{Principals: []string{"mariano"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"mariano"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"ok-emptyPrincipals-getIdentity", p4, args{okGetIdentityToken, SSHOptions{}, pub},
&SSHOptions{CertType: "user", Principals: []string{"max", "mariano"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"name"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false},
{"admin", p3, args{okAdmin, SSHOptions{}, pub}, expectedAdminOptions, false, false},
{"admin-user", p3, args{okAdmin, SSHOptions{CertType: "user"}, pub}, expectedAdminOptions, false, false},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"admin", p3, args{okAdmin, SSHOptions{}, pub}, expectedAdminOptions, http.StatusOK, false, false},
{"admin-user", p3, args{okAdmin, SSHOptions{CertType: "user"}, pub}, expectedAdminOptions, http.StatusOK, false, false},
{"admin-principals", p3, args{okAdmin, SSHOptions{Principals: []string{"root"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"root"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"admin-options", p3, args{okAdmin, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub},
&SSHOptions{CertType: "user", Principals: []string{"name"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, false, false},
{"admin-host", p3, args{okAdmin, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, false, true},
{"fail-user-host", p1, args{t1, SSHOptions{CertType: "host"}, pub}, nil, false, true},
{"fail-user-principals", p1, args{t1, SSHOptions{Principals: []string{"root"}}, pub}, nil, false, true},
{"fail-email", p3, args{failEmail, SSHOptions{}, pub}, nil, true, false},
{"fail-getIdentity", p5, args{failGetIdentityToken, SSHOptions{}, pub}, nil, true, false},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"admin-host", p3, args{okAdmin, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub},
expectedHostOptions, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},
{"fail-user-host", p1, args{t1, SSHOptions{CertType: "host"}, pub}, nil, http.StatusOK, false, true},
{"fail-user-principals", p1, args{t1, SSHOptions{Principals: []string{"root"}}, pub}, nil, http.StatusOK, false, true},
{"fail-email", p3, args{failEmail, SSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
{"fail-getIdentity", p5, args{failGetIdentityToken, SSHOptions{}, pub}, nil, http.StatusInternalServerError, true, false},
{"fail-sshCA-disabled", p6, args{"foo", SSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := NewContextWithMethod(context.Background(), SignSSHMethod)
got, err := tt.prov.AuthorizeSSHSign(ctx, tt.args.token)
got, err := tt.prov.AuthorizeSSHSign(context.Background(), tt.args.token)
if (err != nil) != tt.wantErr {
t.Errorf("OIDC.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
assert.Nil(t, got)
} else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
@ -484,36 +606,32 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
}
}
func TestOIDC_AuthorizeRevoke(t *testing.T) {
func TestOIDC_AuthorizeSSHRevoke(t *testing.T) {
p1, err := generateOIDC()
assert.FatalError(t, err)
p2, err := generateOIDC()
assert.FatalError(t, err)
p2.Admins = []string{"root@example.com"}
srv := generateJWKServer(2)
defer srv.Close()
var keys jose.JSONWebKeySet
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
// Create test provisioners
p1, err := generateOIDC()
assert.FatalError(t, err)
p3, err := generateOIDC()
assert.FatalError(t, err)
// Admin + Domains
p3.Admins = []string{"name@smallstep.com", "root@example.com"}
p3.Domains = []string{"smallstep.com"}
// Update configuration endpoints and initialize
config := Config{Claims: globalProvisionerClaims}
p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
p3.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
p2.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
assert.FatalError(t, p1.Init(config))
assert.FatalError(t, p3.Init(config))
assert.FatalError(t, p2.Init(config))
t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0])
// Invalid email
failEmail, err := generateToken("subject", "the-issuer", p1.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
// Admin email not in domains
okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0])
noAdmin, err := generateToken("subject", "the-issuer", p1.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
// Invalid email
failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
// Admin email in domains
okAdmin, err := generateToken("subject", "the-issuer", p2.ClientID, "root@example.com", []string{"test.smallstep.com"}, time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
type args struct {
@ -523,52 +641,22 @@ func TestOIDC_AuthorizeRevoke(t *testing.T) {
name string
prov *OIDC
args args
code int
wantErr bool
}{
{"ok1", p1, args{t1}, true},
{"admin", p3, args{okAdmin}, false},
{"fail-email", p3, args{failEmail}, true},
{"ok", p2, args{okAdmin}, http.StatusOK, false},
{"fail/invalid-token", p1, args{failEmail}, http.StatusUnauthorized, true},
{"fail/not-admin", p1, args{noAdmin}, http.StatusUnauthorized, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.prov.AuthorizeRevoke(context.TODO(), tt.args.token)
err := tt.prov.AuthorizeSSHRevoke(context.Background(), tt.args.token)
if (err != nil) != tt.wantErr {
fmt.Println(tt)
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestOIDC_AuthorizeRenew(t *testing.T) {
p1, err := generateOIDC()
assert.FatalError(t, err)
p2, err := generateOIDC()
assert.FatalError(t, err)
// disable renewal
disable := true
p2.Claims = &Claims{DisableRenewal: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
type args struct {
cert *x509.Certificate
}
tests := []struct {
name string
prov *OIDC
args args
wantErr bool
}{
{"ok", p1, args{nil}, false},
{"fail", p2, args{nil}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("OIDC.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("OIDC.AuthorizeSSHRevoke() error = %v, wantErr %v", err, tt.wantErr)
} else if err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.code)
}
})
}

View file

@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
"golang.org/x/crypto/ssh"
)
@ -283,43 +284,43 @@ type base struct{}
// AuthorizeSign returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for signing x509 Certificates.
func (b *base) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
return nil, errors.New("not implemented; provisioner does not implement AuthorizeSign")
return nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSign not implemented"))
}
// AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for revoking x509 Certificates.
func (b *base) AuthorizeRevoke(ctx context.Context, token string) error {
return errors.New("not implemented; provisioner does not implement AuthorizeRevoke")
return errs.Unauthorized(errors.New("provisioner.AuthorizeRevoke not implemented"))
}
// AuthorizeRenew returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for renewing x509 Certificates.
func (b *base) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
return errors.New("not implemented; provisioner does not implement AuthorizeRenew")
return errs.Unauthorized(errors.New("provisioner.AuthorizeRenew not implemented"))
}
// AuthorizeSSHSign returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for signing SSH Certificates.
func (b *base) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
return nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHSign")
return nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSSHSign not implemented"))
}
// AuthorizeRevoke returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for revoking SSH Certificates.
func (b *base) AuthorizeSSHRevoke(ctx context.Context, token string) error {
return errors.New("not implemented; provisioner does not implement AuthorizeSSHRevoke")
return errs.Unauthorized(errors.New("provisioner.AuthorizeSSHRevoke not implemented"))
}
// AuthorizeSSHRenew returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for renewing SSH Certificates.
func (b *base) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
return nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRenew")
return nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSSHRenew not implemented"))
}
// AuthorizeSSHRekey returns an unimplmented error. Provisioners should overwrite
// this method if they will support authorizing tokens for rekeying SSH Certificates.
func (b *base) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) {
return nil, nil, errors.New("not implemented; provisioner does not implement AuthorizeSSHRekey")
return nil, nil, errs.Unauthorized(errors.New("provisioner.AuthorizeSSHRekey not implemented"))
}
// Identity is the type representing an externally supplied identity that is used

View file

@ -1,10 +1,14 @@
package provisioner
import (
"context"
"net/http"
"testing"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/errs"
"golang.org/x/crypto/ssh"
)
func TestType_String(t *testing.T) {
@ -101,3 +105,93 @@ func TestDefaultIdentityFunc(t *testing.T) {
})
}
}
func TestUnimplementedMethods(t *testing.T) {
tests := []struct {
name string
p Interface
method Method
}{
{"jwk/sshRekey", &JWK{}, SSHRekeyMethod},
{"jwk/sshRenew", &JWK{}, SSHRenewMethod},
{"aws/revoke", &AWS{}, RevokeMethod},
{"aws/sshRenew", &AWS{}, SSHRenewMethod},
{"aws/rekey", &AWS{}, SSHRekeyMethod},
{"aws/sshRevoke", &AWS{}, SSHRevokeMethod},
{"azure/revoke", &Azure{}, RevokeMethod},
{"azure/sshRenew", &Azure{}, SSHRenewMethod},
{"azure/sshRekey", &Azure{}, SSHRekeyMethod},
{"azure/sshRevoke", &Azure{}, SSHRevokeMethod},
{"gcp/revoke", &GCP{}, RevokeMethod},
{"gcp/sshRenew", &GCP{}, SSHRenewMethod},
{"gcp/sshRekey", &GCP{}, SSHRekeyMethod},
{"gcp/sshRevoke", &GCP{}, SSHRevokeMethod},
{"oidc/sshRenew", &OIDC{}, SSHRenewMethod},
{"oidc/sshRekey", &OIDC{}, SSHRekeyMethod},
{"x5c/sshRenew", &X5C{}, SSHRenewMethod},
{"x5c/sshRekey", &X5C{}, SSHRekeyMethod},
{"x5c/sshRevoke", &X5C{}, SSHRekeyMethod},
{"acme/revoke", &ACME{}, RevokeMethod},
{"acme/sshSign", &ACME{}, SSHSignMethod},
{"acme/sshRekey", &ACME{}, SSHRekeyMethod},
{"acme/sshRenew", &ACME{}, SSHRenewMethod},
{"acme/sshRevoke", &ACME{}, SSHRevokeMethod},
{"sshpop/sign", &SSHPOP{}, SignMethod},
{"sshpop/renew", &SSHPOP{}, RenewMethod},
{"sshpop/revoke", &SSHPOP{}, RevokeMethod},
{"sshpop/sshSign", &SSHPOP{}, SSHSignMethod},
{"k8ssa/sshRekey", &K8sSA{}, SSHRekeyMethod},
{"k8ssa/sshRenew", &K8sSA{}, SSHRenewMethod},
{"k8ssa/sshRevoke", &K8sSA{}, SSHRevokeMethod},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
err error
msg string
)
switch tt.method {
case SignMethod:
var signOpts []SignOption
signOpts, err = tt.p.AuthorizeSign(context.Background(), "")
assert.Nil(t, signOpts)
msg = "provisioner.AuthorizeSign not implemented"
case RenewMethod:
err = tt.p.AuthorizeRenew(context.Background(), nil)
msg = "provisioner.AuthorizeRenew not implemented"
case RevokeMethod:
err = tt.p.AuthorizeRevoke(context.Background(), "")
msg = "provisioner.AuthorizeRevoke not implemented"
case SSHSignMethod:
var signOpts []SignOption
signOpts, err = tt.p.AuthorizeSSHSign(context.Background(), "")
assert.Nil(t, signOpts)
msg = "provisioner.AuthorizeSSHSign not implemented"
case SSHRenewMethod:
var cert *ssh.Certificate
cert, err = tt.p.AuthorizeSSHRenew(context.Background(), "")
assert.Nil(t, cert)
msg = "provisioner.AuthorizeSSHRenew not implemented"
case SSHRekeyMethod:
var (
cert *ssh.Certificate
signOpts []SignOption
)
cert, signOpts, err = tt.p.AuthorizeSSHRekey(context.Background(), "")
assert.Nil(t, cert)
assert.Nil(t, signOpts)
msg = "provisioner.AuthorizeSSHRekey not implemented"
case SSHRevokeMethod:
err = tt.p.AuthorizeSSHRevoke(context.Background(), "")
msg = "provisioner.AuthorizeSSHRevoke not implemented"
default:
t.Errorf("unexpected method %s", tt.method)
}
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), http.StatusUnauthorized)
assert.Equals(t, err.Error(), msg)
})
}
}

View file

@ -30,7 +30,7 @@ type SignOption interface{}
// CertificateValidator is the interface used to validate a X.509 certificate.
type CertificateValidator interface {
SignOption
Valid(crt *x509.Certificate) error
Valid(cert *x509.Certificate, o Options) error
}
// CertificateRequestValidator is the interface used to validate a X.509
@ -106,7 +106,7 @@ func (v commonNameValidator) Valid(req *x509.CertificateRequest) error {
return errors.New("certificate request cannot contain an empty common name")
}
if req.Subject.CommonName != string(v) {
return errors.Errorf("certificate request does not contain the valid common name, got %s, want %s", req.Subject.CommonName, v)
return errors.Errorf("certificate request does not contain the valid common name; requested common name = %s, token subject = %s", req.Subject.CommonName, v)
}
return nil
}
@ -265,33 +265,30 @@ func newValidityValidator(min, max time.Duration) *validityValidator {
// Valid validates the certificate validity settings (notBefore/notAfter) and
// and total duration.
func (v *validityValidator) Valid(crt *x509.Certificate) error {
func (v *validityValidator) Valid(cert *x509.Certificate, o Options) error {
var (
na = crt.NotAfter.Truncate(time.Second)
nb = crt.NotBefore.Truncate(time.Second)
na = cert.NotAfter.Truncate(time.Second)
nb = cert.NotBefore.Truncate(time.Second)
now = time.Now().Truncate(time.Second)
)
// To not take into account the backdate, time.Now() will be used to
// calculate the duration if NotBefore is in the past.
var d time.Duration
if now.After(nb) {
d = na.Sub(now)
} else {
d = na.Sub(nb)
}
d := na.Sub(nb)
if na.Before(now) {
return errors.Errorf("NotAfter: %v cannot be in the past", na)
return errors.Errorf("notAfter cannot be in the past; na=%v", na)
}
if na.Before(nb) {
return errors.Errorf("NotAfter: %v cannot be before NotBefore: %v", na, nb)
return errors.Errorf("notAfter cannot be before notBefore; na=%v, nb=%v", na, nb)
}
if d < v.min {
return errors.Errorf("requested duration of %v is less than the authorized minimum certificate duration of %v",
d, v.min)
}
if d > v.max {
// NOTE: this check is not "technically correct". We're allowing the max
// duration of a cert to be "max + backdate" and not all certificates will
// be backdated (e.g. if a user passes the NotBefore value then we do not
// apply a backdate). This is good enough.
if d > v.max+o.Backdate {
return errors.Errorf("requested duration of %v is more than the authorized maximum certificate duration of %v",
d, v.max)
}

View file

@ -3,9 +3,10 @@ package provisioner
import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"net"
"net/url"
"reflect"
"strings"
"testing"
"time"
@ -48,22 +49,22 @@ func Test_emailOnlyIdentity_Valid(t *testing.T) {
}
func Test_defaultPublicKeyValidator_Valid(t *testing.T) {
_shortRSA, err := pemutil.Read("./testdata/short-rsa.csr")
_shortRSA, err := pemutil.Read("./testdata/certs/short-rsa.csr")
assert.FatalError(t, err)
shortRSA, ok := _shortRSA.(*x509.CertificateRequest)
assert.Fatal(t, ok)
_rsa, err := pemutil.Read("./testdata/rsa.csr")
_rsa, err := pemutil.Read("./testdata/certs/rsa.csr")
assert.FatalError(t, err)
rsaCSR, ok := _rsa.(*x509.CertificateRequest)
assert.Fatal(t, ok)
_ecdsa, err := pemutil.Read("./testdata/ecdsa.csr")
_ecdsa, err := pemutil.Read("./testdata/certs/ecdsa.csr")
assert.FatalError(t, err)
ecdsaCSR, ok := _ecdsa.(*x509.CertificateRequest)
assert.Fatal(t, ok)
_ed25519, err := pemutil.Read("./testdata/ed25519.csr")
_ed25519, err := pemutil.Read("./testdata/certs/ed25519.csr")
assert.FatalError(t, err)
ed25519CSR, ok := _ed25519.(*x509.CertificateRequest)
assert.Fatal(t, ok)
@ -246,30 +247,191 @@ func Test_ipAddressesValidator_Valid(t *testing.T) {
}
func Test_validityValidator_Valid(t *testing.T) {
type fields struct {
min time.Duration
max time.Duration
type test struct {
cert *x509.Certificate
opts Options
vv *validityValidator
err error
}
type args struct {
crt *x509.Certificate
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := &validityValidator{
min: tt.fields.min,
max: tt.fields.max,
tests := map[string]func() test{
"fail/notAfter-past": func() test {
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotAfter: time.Now().Add(-5 * time.Minute)},
opts: Options{},
err: errors.New("notAfter cannot be in the past"),
}
if err := v.Valid(tt.args.crt); (err != nil) != tt.wantErr {
t.Errorf("validityValidator.Valid() error = %v, wantErr %v", err, tt.wantErr)
},
"fail/notBefore-after-notAfter": func() test {
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotBefore: time.Now().Add(10 * time.Minute),
NotAfter: time.Now().Add(5 * time.Minute)},
opts: Options{},
err: errors.New("notAfter cannot be before notBefore"),
}
},
"fail/duration-too-short": func() test {
n := now()
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotBefore: n,
NotAfter: n.Add(3 * time.Minute)},
opts: Options{},
err: errors.New("is less than the authorized minimum certificate duration of "),
}
},
"ok/duration-exactly-min": func() test {
n := now()
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotBefore: n,
NotAfter: n.Add(5 * time.Minute)},
opts: Options{},
}
},
"fail/duration-too-great": func() test {
n := now()
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotBefore: n,
NotAfter: n.Add(24*time.Hour + time.Second)},
err: errors.New("is more than the authorized maximum certificate duration of "),
}
},
"ok/duration-exactly-max": func() test {
n := time.Now()
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: &x509.Certificate{NotBefore: n,
NotAfter: n.Add(24 * time.Hour)},
}
},
"ok/duration-exact-min-with-backdate": func() test {
now := time.Now()
cert := &x509.Certificate{NotBefore: now, NotAfter: now.Add(5 * time.Minute)}
time.Sleep(time.Second)
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: cert,
opts: Options{Backdate: time.Second},
}
},
"ok/duration-exact-max-with-backdate": func() test {
backdate := time.Second
now := time.Now()
cert := &x509.Certificate{NotBefore: now, NotAfter: now.Add(24*time.Hour + backdate)}
time.Sleep(backdate)
return test{
vv: &validityValidator{5 * time.Minute, 24 * time.Hour},
cert: cert,
opts: Options{Backdate: backdate},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
if err := tt.vv.Valid(tt.cert, tt.opts); err != nil {
if assert.NotNil(t, tt.err, fmt.Sprintf("expected no error, but got err = %s", err.Error())) {
assert.True(t, strings.Contains(err.Error(), tt.err.Error()),
fmt.Sprintf("want err = %s, but got err = %s", tt.err.Error(), err.Error()))
}
} else {
assert.Nil(t, tt.err, fmt.Sprintf("expected err = %s, but not <nil>", tt.err))
}
})
}
}
func Test_profileDefaultDuration_Option(t *testing.T) {
type test struct {
so Options
pdd profileDefaultDuration
cert *x509.Certificate
valid func(*x509.Certificate)
}
tests := map[string]func() test{
"ok/notBefore-notAfter-duration-empty": func() test {
return test{
pdd: profileDefaultDuration(0),
so: Options{},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
n := now()
assert.True(t, n.After(cert.NotBefore))
assert.True(t, n.Add(-1*time.Minute).Before(cert.NotBefore))
assert.True(t, n.Add(24*time.Hour).After(cert.NotAfter))
assert.True(t, n.Add(24*time.Hour).Add(-1*time.Minute).Before(cert.NotAfter))
},
}
},
"ok/notBefore-set": func() test {
nb := time.Now().Add(5 * time.Minute).UTC()
return test{
pdd: profileDefaultDuration(0),
so: Options{NotBefore: NewTimeDuration(nb)},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, nb)
assert.Equals(t, cert.NotAfter, nb.Add(24*time.Hour))
},
}
},
"ok/duration-set": func() test {
d := 4 * time.Hour
return test{
pdd: profileDefaultDuration(d),
so: Options{Backdate: time.Second},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
n := now()
assert.True(t, n.After(cert.NotBefore), fmt.Sprintf("expected now = %s to be after cert.NotBefore = %s", n, cert.NotBefore))
assert.True(t, n.Add(-1*time.Minute).Before(cert.NotBefore))
assert.True(t, n.Add(d).After(cert.NotAfter))
assert.True(t, n.Add(d).Add(-1*time.Minute).Before(cert.NotAfter))
},
}
},
"ok/notAfter-set": func() test {
na := now().Add(10 * time.Minute).UTC()
return test{
pdd: profileDefaultDuration(0),
so: Options{NotAfter: NewTimeDuration(na)},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
n := now()
assert.True(t, n.After(cert.NotBefore), fmt.Sprintf("expected now = %s to be after cert.NotBefore = %s", n, cert.NotBefore))
assert.True(t, n.Add(-1*time.Minute).Before(cert.NotBefore))
assert.Equals(t, cert.NotAfter, na)
},
}
},
"ok/notBefore-and-notAfter-set": func() test {
nb := time.Now().Add(5 * time.Minute).UTC()
na := time.Now().Add(10 * time.Minute).UTC()
d := 4 * time.Hour
return test{
pdd: profileDefaultDuration(d),
so: Options{NotBefore: NewTimeDuration(nb), NotAfter: NewTimeDuration(na)},
cert: new(x509.Certificate),
valid: func(cert *x509.Certificate) {
assert.Equals(t, cert.NotBefore, nb)
assert.Equals(t, cert.NotAfter, na)
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tt := run()
prof := &x509util.Leaf{}
prof.SetSubject(tt.cert)
assert.FatalError(t, tt.pdd.Option(tt.so)(prof), "unexpected error")
tt.valid(prof.Subject())
})
}
}
@ -381,43 +543,3 @@ func Test_profileLimitDuration_Option(t *testing.T) {
})
}
}
func Test_profileDefaultDuration_Option(t *testing.T) {
tm, fn := mockNow()
defer fn()
v := profileDefaultDuration(24 * time.Hour)
type args struct {
so Options
}
tests := []struct {
name string
v profileDefaultDuration
args args
want *x509.Certificate
}{
{"default", v, args{Options{}}, &x509.Certificate{NotBefore: tm, NotAfter: tm.Add(24 * time.Hour)}},
{"backdate", v, args{Options{Backdate: 1 * time.Minute}}, &x509.Certificate{NotBefore: tm.Add(-1 * time.Minute), NotAfter: tm.Add(24 * time.Hour)}},
{"notBefore", v, args{Options{NotBefore: NewTimeDuration(tm.Add(10 * time.Second))}}, &x509.Certificate{NotBefore: tm.Add(10 * time.Second), NotAfter: tm.Add(24*time.Hour + 10*time.Second)}},
{"notAfter", v, args{Options{NotAfter: NewTimeDuration(tm.Add(1 * time.Hour))}}, &x509.Certificate{NotBefore: tm, NotAfter: tm.Add(1 * time.Hour)}},
{"notBefore and notAfter", v, args{Options{NotBefore: NewTimeDuration(tm.Add(10 * time.Second)), NotAfter: NewTimeDuration(tm.Add(1 * time.Hour))}},
&x509.Certificate{NotBefore: tm.Add(10 * time.Second), NotAfter: tm.Add(1 * time.Hour)}},
{"notBefore and backdate", v, args{Options{Backdate: 1 * time.Minute, NotBefore: NewTimeDuration(tm.Add(10 * time.Second))}},
&x509.Certificate{NotBefore: tm.Add(10 * time.Second), NotAfter: tm.Add(24*time.Hour + 10*time.Second)}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cert := &x509.Certificate{}
profile := &x509util.Leaf{}
profile.SetSubject(cert)
fn := tt.v.Option(tt.args.so)
if err := fn(profile); err != nil {
t.Errorf("profileDefaultDuration.Option() error = %v", err)
}
if !reflect.DeepEqual(cert, tt.want) {
t.Errorf("profileDefaultDuration.Option() = %v, \nwant %v", cert, tt.want)
}
})
}
}

View file

@ -78,7 +78,7 @@ func (o SSHOptions) Modify(cert *ssh.Certificate) error {
case SSHHostCert:
cert.CertType = ssh.HostCert
default:
return errors.Errorf("ssh certificate has an unknown type: %s", o.CertType)
return errors.Errorf("ssh certificate has an unknown type - %s", o.CertType)
}
cert.KeyId = o.KeyID
@ -126,11 +126,11 @@ func (o sshCertPrincipalsModifier) Modify(cert *ssh.Certificate) error {
return nil
}
// sshCertificateKeyIDModifier is an SSHCertificateModifier that sets the given
// sshCertKeyIDModifier is an SSHCertificateModifier that sets the given
// Key ID in the SSH certificate.
type sshCertificateKeyIDModifier string
type sshCertKeyIDModifier string
func (m sshCertificateKeyIDModifier) Modify(cert *ssh.Certificate) error {
func (m sshCertKeyIDModifier) Modify(cert *ssh.Certificate) error {
cert.KeyId = string(m)
return nil
}
@ -145,30 +145,30 @@ func (m sshCertTypeModifier) Modify(cert *ssh.Certificate) error {
return nil
}
// sshCertificateValidAfterModifier is an SSHCertificateModifier that sets the
// sshCertValidAfterModifier is an SSHCertificateModifier that sets the
// ValidAfter in the SSH certificate.
type sshCertificateValidAfterModifier uint64
type sshCertValidAfterModifier uint64
func (m sshCertificateValidAfterModifier) Modify(cert *ssh.Certificate) error {
func (m sshCertValidAfterModifier) Modify(cert *ssh.Certificate) error {
cert.ValidAfter = uint64(m)
return nil
}
// sshCertificateValidBeforeModifier is an SSHCertificateModifier that sets the
// sshCertValidBeforeModifier is an SSHCertificateModifier that sets the
// ValidBefore in the SSH certificate.
type sshCertificateValidBeforeModifier uint64
type sshCertValidBeforeModifier uint64
func (m sshCertificateValidBeforeModifier) Modify(cert *ssh.Certificate) error {
func (m sshCertValidBeforeModifier) Modify(cert *ssh.Certificate) error {
cert.ValidBefore = uint64(m)
return nil
}
// sshCertificateDefaultModifier implements a SSHCertificateModifier that
// sshCertDefaultsModifier implements a SSHCertificateModifier that
// modifies the certificate with the given options if they are not set.
type sshCertificateDefaultsModifier SSHOptions
type sshCertDefaultsModifier SSHOptions
// Modify implements the SSHCertificateModifier interface.
func (m sshCertificateDefaultsModifier) Modify(cert *ssh.Certificate) error {
func (m sshCertDefaultsModifier) Modify(cert *ssh.Certificate) error {
if cert.CertType == 0 {
cert.CertType = sshCertTypeUInt32(m.CertType)
}

View file

@ -38,6 +38,457 @@ func TestSSHOptions_Type(t *testing.T) {
}
}
func TestSSHOptions_Modify(t *testing.T) {
type test struct {
so *SSHOptions
cert *ssh.Certificate
valid func(*ssh.Certificate)
err error
}
tests := map[string](func() test){
"fail/unexpected-cert-type": func() test {
return test{
so: &SSHOptions{CertType: "foo"},
cert: new(ssh.Certificate),
err: errors.Errorf("ssh certificate has an unknown type - foo"),
}
},
"fail/validAfter-greater-validBefore": func() test {
return test{
so: &SSHOptions{CertType: "user"},
cert: &ssh.Certificate{ValidAfter: uint64(15), ValidBefore: uint64(10)},
err: errors.Errorf("ssh certificate valid after cannot be greater than valid before"),
}
},
"ok/user-cert": func() test {
return test{
so: &SSHOptions{CertType: "user"},
cert: new(ssh.Certificate),
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.CertType, uint32(ssh.UserCert))
},
}
},
"ok/host-cert": func() test {
return test{
so: &SSHOptions{CertType: "host"},
cert: new(ssh.Certificate),
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.CertType, uint32(ssh.HostCert))
},
}
},
"ok": func() test {
va := time.Now().Add(5 * time.Minute)
vb := time.Now().Add(1 * time.Hour)
so := &SSHOptions{CertType: "host", KeyID: "foo", Principals: []string{"foo", "bar"},
ValidAfter: NewTimeDuration(va), ValidBefore: NewTimeDuration(vb)}
return test{
so: so,
cert: new(ssh.Certificate),
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.CertType, uint32(ssh.HostCert))
assert.Equals(t, cert.KeyId, so.KeyID)
assert.Equals(t, cert.ValidPrincipals, so.Principals)
assert.Equals(t, cert.ValidAfter, uint64(so.ValidAfter.RelativeTime(time.Now()).Unix()))
assert.Equals(t, cert.ValidBefore, uint64(so.ValidBefore.RelativeTime(time.Now()).Unix()))
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run()
if err := tc.so.Modify(tc.cert); err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
tc.valid(tc.cert)
}
}
})
}
}
func TestSSHOptions_Match(t *testing.T) {
type test struct {
so SSHOptions
cmp SSHOptions
err error
}
tests := map[string](func() test){
"fail/cert-type": func() test {
return test{
so: SSHOptions{CertType: "foo"},
cmp: SSHOptions{CertType: "bar"},
err: errors.Errorf("ssh certificate type does not match - got bar, want foo"),
}
},
"fail/pricipals": func() test {
return test{
so: SSHOptions{Principals: []string{"foo"}},
cmp: SSHOptions{Principals: []string{"bar"}},
err: errors.Errorf("ssh certificate principals does not match - got [bar], want [foo]"),
}
},
"fail/validAfter": func() test {
return test{
so: SSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute))},
cmp: SSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(5 * time.Minute))},
err: errors.Errorf("ssh certificate valid after does not match"),
}
},
"fail/validBefore": func() test {
return test{
so: SSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(1 * time.Minute))},
cmp: SSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute))},
err: errors.Errorf("ssh certificate valid before does not match"),
}
},
"ok/original-empty": func() test {
return test{
so: SSHOptions{},
cmp: SSHOptions{
CertType: "foo",
Principals: []string{"foo"},
ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute)),
ValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute)),
},
}
},
"ok/cmp-empty": func() test {
return test{
cmp: SSHOptions{},
so: SSHOptions{
CertType: "foo",
Principals: []string{"foo"},
ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute)),
ValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute)),
},
}
},
"ok/equal": func() test {
n := time.Now()
va := NewTimeDuration(n.Add(1 * time.Minute))
vb := NewTimeDuration(n.Add(5 * time.Minute))
return test{
cmp: SSHOptions{
CertType: "foo",
Principals: []string{"foo"},
ValidAfter: va,
ValidBefore: vb,
},
so: SSHOptions{
CertType: "foo",
Principals: []string{"foo"},
ValidAfter: va,
ValidBefore: vb,
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run()
if err := tc.so.match(tc.cmp); err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
func Test_sshCertPrincipalsModifier_Modify(t *testing.T) {
type test struct {
modifier sshCertPrincipalsModifier
cert *ssh.Certificate
expected []string
}
tests := map[string](func() test){
"ok": func() test {
a := []string{"foo", "bar"}
return test{
modifier: sshCertPrincipalsModifier(a),
cert: new(ssh.Certificate),
expected: a,
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run()
if assert.Nil(t, tc.modifier.Modify(tc.cert)) {
assert.Equals(t, tc.cert.ValidPrincipals, tc.expected)
}
})
}
}
func Test_sshCertKeyIDModifier_Modify(t *testing.T) {
type test struct {
modifier sshCertKeyIDModifier
cert *ssh.Certificate
expected string
}
tests := map[string](func() test){
"ok": func() test {
a := "foo"
return test{
modifier: sshCertKeyIDModifier(a),
cert: new(ssh.Certificate),
expected: a,
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run()
if assert.Nil(t, tc.modifier.Modify(tc.cert)) {
assert.Equals(t, tc.cert.KeyId, tc.expected)
}
})
}
}
func Test_sshCertTypeModifier_Modify(t *testing.T) {
type test struct {
modifier sshCertTypeModifier
cert *ssh.Certificate
expected uint32
}
tests := map[string](func() test){
"ok/user": func() test {
return test{
modifier: sshCertTypeModifier("user"),
cert: new(ssh.Certificate),
expected: ssh.UserCert,
}
},
"ok/host": func() test {
return test{
modifier: sshCertTypeModifier("host"),
cert: new(ssh.Certificate),
expected: ssh.HostCert,
}
},
"ok/default": func() test {
return test{
modifier: sshCertTypeModifier("foo"),
cert: new(ssh.Certificate),
expected: 0,
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run()
if assert.Nil(t, tc.modifier.Modify(tc.cert)) {
assert.Equals(t, tc.cert.CertType, uint32(tc.expected))
}
})
}
}
func Test_sshCertValidAfterModifier_Modify(t *testing.T) {
type test struct {
modifier sshCertValidAfterModifier
cert *ssh.Certificate
expected uint64
}
tests := map[string](func() test){
"ok": func() test {
return test{
modifier: sshCertValidAfterModifier(15),
cert: new(ssh.Certificate),
expected: 15,
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run()
if assert.Nil(t, tc.modifier.Modify(tc.cert)) {
assert.Equals(t, tc.cert.ValidAfter, tc.expected)
}
})
}
}
func Test_sshCertDefaultsModifier_Modify(t *testing.T) {
type test struct {
modifier sshCertDefaultsModifier
cert *ssh.Certificate
valid func(*ssh.Certificate)
}
tests := map[string](func() test){
"ok/changes": func() test {
n := time.Now()
va := NewTimeDuration(n.Add(1 * time.Minute))
vb := NewTimeDuration(n.Add(5 * time.Minute))
so := SSHOptions{
Principals: []string{"foo", "bar"},
CertType: "host",
ValidAfter: va,
ValidBefore: vb,
}
return test{
modifier: sshCertDefaultsModifier(so),
cert: new(ssh.Certificate),
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.ValidPrincipals, so.Principals)
assert.Equals(t, cert.CertType, uint32(ssh.HostCert))
assert.Equals(t, cert.ValidAfter, uint64(so.ValidAfter.RelativeTime(time.Now()).Unix()))
assert.Equals(t, cert.ValidBefore, uint64(so.ValidBefore.RelativeTime(time.Now()).Unix()))
},
}
},
"ok/no-changes": func() test {
n := time.Now()
so := SSHOptions{
Principals: []string{"foo", "bar"},
CertType: "host",
ValidAfter: NewTimeDuration(n.Add(15 * time.Minute)),
ValidBefore: NewTimeDuration(n.Add(25 * time.Minute)),
}
return test{
modifier: sshCertDefaultsModifier(so),
cert: &ssh.Certificate{
CertType: uint32(ssh.UserCert),
ValidPrincipals: []string{"zap", "zoop"},
ValidAfter: 15,
ValidBefore: 25,
},
valid: func(cert *ssh.Certificate) {
assert.Equals(t, cert.ValidPrincipals, []string{"zap", "zoop"})
assert.Equals(t, cert.CertType, uint32(ssh.UserCert))
assert.Equals(t, cert.ValidAfter, uint64(15))
assert.Equals(t, cert.ValidBefore, uint64(25))
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run()
if assert.Nil(t, tc.modifier.Modify(tc.cert)) {
tc.valid(tc.cert)
}
})
}
}
func Test_sshDefaultExtensionModifier_Modify(t *testing.T) {
type test struct {
modifier sshDefaultExtensionModifier
cert *ssh.Certificate
valid func(*ssh.Certificate)
err error
}
tests := map[string](func() test){
"fail/unexpected-cert-type": func() test {
cert := &ssh.Certificate{CertType: 3}
return test{
modifier: sshDefaultExtensionModifier{},
cert: cert,
err: errors.New("ssh certificate type has not been set or is invalid"),
}
},
"ok/host": func() test {
cert := &ssh.Certificate{CertType: ssh.HostCert}
return test{
modifier: sshDefaultExtensionModifier{},
cert: cert,
valid: func(cert *ssh.Certificate) {
assert.Len(t, 0, cert.Extensions)
},
}
},
"ok/user/extensions-exists": func() test {
cert := &ssh.Certificate{CertType: ssh.UserCert, Permissions: ssh.Permissions{Extensions: map[string]string{
"foo": "bar",
}}}
return test{
modifier: sshDefaultExtensionModifier{},
cert: cert,
valid: func(cert *ssh.Certificate) {
val, ok := cert.Extensions["foo"]
assert.True(t, ok)
assert.Equals(t, val, "bar")
val, ok = cert.Extensions["permit-X11-forwarding"]
assert.True(t, ok)
assert.Equals(t, val, "")
val, ok = cert.Extensions["permit-agent-forwarding"]
assert.True(t, ok)
assert.Equals(t, val, "")
val, ok = cert.Extensions["permit-port-forwarding"]
assert.True(t, ok)
assert.Equals(t, val, "")
val, ok = cert.Extensions["permit-pty"]
assert.True(t, ok)
assert.Equals(t, val, "")
val, ok = cert.Extensions["permit-user-rc"]
assert.True(t, ok)
assert.Equals(t, val, "")
},
}
},
"ok/user/no-extensions": func() test {
return test{
modifier: sshDefaultExtensionModifier{},
cert: &ssh.Certificate{CertType: ssh.UserCert},
valid: func(cert *ssh.Certificate) {
_, ok := cert.Extensions["foo"]
assert.False(t, ok)
val, ok := cert.Extensions["permit-X11-forwarding"]
assert.True(t, ok)
assert.Equals(t, val, "")
val, ok = cert.Extensions["permit-agent-forwarding"]
assert.True(t, ok)
assert.Equals(t, val, "")
val, ok = cert.Extensions["permit-port-forwarding"]
assert.True(t, ok)
assert.Equals(t, val, "")
val, ok = cert.Extensions["permit-pty"]
assert.True(t, ok)
assert.Equals(t, val, "")
val, ok = cert.Extensions["permit-user-rc"]
assert.True(t, ok)
assert.Equals(t, val, "")
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run()
if err := tc.modifier.Modify(tc.cert); err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
tc.valid(tc.cert)
}
}
})
}
}
func Test_sshCertificateDefaultValidator_Valid(t *testing.T) {
pub, _, err := keys.GenerateDefaultKeyPair()
assert.FatalError(t, err)
@ -505,7 +956,7 @@ func Test_sshDefaultDuration_Option(t *testing.T) {
{"host backdate", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert}},
&ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(30 * 24 * time.Hour)}, false},
{"user validAfter", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(1 * time.Hour)}},
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(time.Minute), ValidBefore: unix(17 * time.Hour)}, false},
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(time.Hour), ValidBefore: unix(17 * time.Hour)}, false},
{"user validBefore", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidBefore: unix(1 * time.Hour)}},
&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(time.Hour)}, false},
{"host validAfter validBefore", fields{newClaimer(nil)}, args{SSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(1 * time.Minute), ValidBefore: unix(2 * time.Minute)}},

View file

@ -3,11 +3,13 @@ package provisioner
import (
"context"
"encoding/base64"
"net/http"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ssh"
)
@ -99,33 +101,31 @@ func (p *SSHPOP) Init(config Config) error {
// claims for case specific downstream parsing.
// e.g. a Sign request will auth/validate different fields than a Revoke request.
func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayload, error) {
sshCert, err := ExtractSSHPOPCert(token)
sshCert, jwt, err := ExtractSSHPOPCert(token)
if err != nil {
return nil, errors.Wrap(err, "authorizeToken ssh-pop")
return nil, errs.Wrap(http.StatusUnauthorized, err,
"sshpop.authorizeToken; error extracting sshpop header from token")
}
// Check for revocation.
if isRevoked, err := p.db.IsSSHRevoked(strconv.FormatUint(sshCert.Serial, 10)); err != nil {
return nil, errors.Wrap(err, "authorizeToken ssh-pop")
return nil, errs.Wrap(http.StatusInternalServerError, err,
"sshpop.authorizeToken; error checking checking sshpop cert revocation")
} else if isRevoked {
return nil, errors.New("authorizeToken ssh-pop: ssh certificate has been revoked")
return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop certificate is revoked"))
}
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
}
// Check validity period of the certificate.
n := time.Now()
if sshCert.ValidAfter != 0 && time.Unix(int64(sshCert.ValidAfter), 0).After(n) {
return nil, errors.New("sshpop certificate validAfter is in the future")
return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop certificate validAfter is in the future"))
}
if sshCert.ValidBefore != 0 && time.Unix(int64(sshCert.ValidBefore), 0).Before(n) {
return nil, errors.New("sshpop certificate validBefore is in the past")
return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop certificate validBefore is in the past"))
}
sshCryptoPubKey, ok := sshCert.Key.(ssh.CryptoPublicKey)
if !ok {
return nil, errors.New("ssh public key could not be cast to ssh CryptoPublicKey")
return nil, errs.InternalServerError(errors.New("sshpop.authorizeToken; sshpop public key could not be cast to ssh CryptoPublicKey"))
}
pubKey := sshCryptoPubKey.CryptoPublicKey()
@ -146,7 +146,7 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa
}
}
if !found {
return nil, errors.New("error: provisioner could could not verify the sshpop header certificate")
return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; could not find valid ca signer to verify sshpop certificate"))
}
// Using the ssh certificates key to validate the claims accomplishes two
@ -156,7 +156,7 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa
// 2. Asserts that the claims are valid - have not been tampered with.
var claims sshPOPPayload
if err = jwt.Claims(pubKey, &claims); err != nil {
return nil, errors.Wrap(err, "error parsing claims")
return nil, errs.Wrap(http.StatusUnauthorized, err, "sshpop.authorizeToken; error parsing sshpop token claims")
}
// According to "rfc7519 JSON Web Token" acceptable skew should be no
@ -165,16 +165,17 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa
Issuer: p.Name,
Time: time.Now().UTC(),
}, time.Minute); err != nil {
return nil, errors.Wrapf(err, "invalid token")
return nil, errs.Wrap(http.StatusUnauthorized, err, "sshpop.authorizeToken; invalid sshpop token")
}
// validate audiences with the defaults
if !matchesAudience(claims.Audience, audiences) {
return nil, errors.New("invalid token: invalid audience claim (aud)")
return nil, errs.Unauthorized(errors.Errorf("sshpop.authorizeToken; sshpop token has invalid audience "+
"claim (aud): expected %s, but got %s", audiences, claims.Audience))
}
if claims.Subject == "" {
return nil, errors.New("token subject cannot be empty")
return nil, errs.Unauthorized(errors.New("sshpop.authorizeToken; sshpop token subject cannot be empty"))
}
claims.sshCert = sshCert
@ -186,12 +187,13 @@ func (p *SSHPOP) authorizeToken(token string, audiences []string) (*sshPOPPayloa
func (p *SSHPOP) AuthorizeSSHRevoke(ctx context.Context, token string) error {
claims, err := p.authorizeToken(token, p.audiences.SSHRevoke)
if err != nil {
return err
return errs.Wrap(http.StatusInternalServerError, err, "sshpop.AuthorizeSSHRevoke")
}
if claims.Subject != strconv.FormatUint(claims.sshCert.Serial, 10) {
return errors.New("token subject must be equivalent to certificate serial number")
return errs.BadRequest(errors.New("sshpop.AuthorizeSSHRevoke; sshpop token subject " +
"must be equivalent to sshpop certificate serial number"))
}
return err
return nil
}
// AuthorizeSSHRenew validates the authorization token and extracts/validates
@ -199,10 +201,10 @@ func (p *SSHPOP) AuthorizeSSHRevoke(ctx context.Context, token string) error {
func (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
claims, err := p.authorizeToken(token, p.audiences.SSHRenew)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "sshpop.AuthorizeSSHRenew")
}
if claims.sshCert.CertType != ssh.HostCert {
return nil, errors.New("sshpop AuthorizeSSHRenew: sshpop certificate must be a host ssh certificate")
return nil, errs.BadRequest(errors.New("sshpop.AuthorizeSSHRenew; sshpop certificate must be a host ssh certificate"))
}
return claims.sshCert, nil
@ -214,10 +216,10 @@ func (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Cert
func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) {
claims, err := p.authorizeToken(token, p.audiences.SSHRekey)
if err != nil {
return nil, nil, err
return nil, nil, errs.Wrap(http.StatusInternalServerError, err, "sshpop.AuthorizeSSHRekey")
}
if claims.sshCert.CertType != ssh.HostCert {
return nil, nil, errors.New("sshpop AuthorizeSSHRekey: sshpop certificate must be a host ssh certificate")
return nil, nil, errs.BadRequest(errors.New("sshpop.AuthorizeSSHRekey; sshpop certificate must be a host ssh certificate"))
}
return claims.sshCert, []SignOption{
// Validate public key
@ -232,33 +234,34 @@ func (p *SSHPOP) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Cert
// ExtractSSHPOPCert parses a JWT and extracts and loads the SSH Certificate
// in the sshpop header. If the header is missing, an error is returned.
func ExtractSSHPOPCert(token string) (*ssh.Certificate, error) {
func ExtractSSHPOPCert(token string) (*ssh.Certificate, *jose.JSONWebToken, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
return nil, nil, errors.Wrapf(err, "extractSSHPOPCert; error parsing token")
}
encodedSSHCert, ok := jwt.Headers[0].ExtraHeaders["sshpop"]
if !ok {
return nil, errors.New("token missing sshpop header")
return nil, nil, errors.New("extractSSHPOPCert; token missing sshpop header")
}
encodedSSHCertStr, ok := encodedSSHCert.(string)
if !ok {
return nil, errors.New("error unexpected type for sshpop header")
return nil, nil, errors.Errorf("extractSSHPOPCert; error unexpected type for sshpop header: "+
"want 'string', but got '%T'", encodedSSHCert)
}
sshCertBytes, err := base64.StdEncoding.DecodeString(encodedSSHCertStr)
if err != nil {
return nil, errors.Wrap(err, "error decoding sshpop header")
return nil, nil, errors.Wrap(err, "extractSSHPOPCert; error base64 decoding sshpop header")
}
sshPub, err := ssh.ParsePublicKey(sshCertBytes)
if err != nil {
return nil, errors.Wrap(err, "error parsing ssh public key")
return nil, nil, errors.Wrap(err, "extractSSHPOPCert; error parsing ssh public key")
}
sshCert, ok := sshPub.(*ssh.Certificate)
if !ok {
return nil, errors.New("error converting ssh public key to ssh certificate")
return nil, nil, errors.New("extractSSHPOPCert; error converting ssh public key to ssh certificate")
}
return sshCert, nil
return sshCert, jwt, nil
}
func bytesForSigning(cert *ssh.Certificate) []byte {

View file

@ -0,0 +1,684 @@
package provisioner
import (
"context"
"crypto"
"crypto/rand"
"encoding/base64"
"net/http"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ssh"
)
func TestSSHPOP_Getters(t *testing.T) {
p, err := generateSSHPOP()
assert.FatalError(t, err)
id := "sshpop/" + p.Name
if got := p.GetID(); got != id {
t.Errorf("SSHPOP.GetID() = %v, want %v", got, id)
}
if got := p.GetName(); got != p.Name {
t.Errorf("SSHPOP.GetName() = %v, want %v", got, p.Name)
}
if got := p.GetType(); got != TypeSSHPOP {
t.Errorf("SSHPOP.GetType() = %v, want %v", got, TypeSSHPOP)
}
kid, key, ok := p.GetEncryptedKey()
if kid != "" || key != "" || ok == true {
t.Errorf("SSHPOP.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)",
kid, key, ok, "", "", false)
}
}
func createSSHCert(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certificate, *jose.JSONWebKey, error) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "foo", 0)
if err != nil {
return nil, nil, err
}
cert.Key, err = ssh.NewPublicKey(jwk.Public().Key)
if err != nil {
return nil, nil, err
}
if err = cert.SignCert(rand.Reader, signer); err != nil {
return nil, nil, err
}
return cert, jwk, nil
}
func generateSSHPOPToken(p Interface, cert *ssh.Certificate, jwk *jose.JSONWebKey) (string, error) {
return generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
}
func TestSSHPOP_authorizeToken(t *testing.T) {
key, err := pemutil.Read("./testdata/secrets/ssh_user_ca_key")
assert.FatalError(t, err)
signer, ok := key.(crypto.Signer)
assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer")
sshSigner, err := ssh.NewSignerFromSigner(signer)
assert.FatalError(t, err)
type test struct {
p *SSHPOP
token string
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "),
}
},
"fail/error-revoked-db-check": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, errors.New("force")
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
assert.FatalError(t, err)
tok, err := generateSSHPOPToken(p, cert, jwk)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusInternalServerError,
err: errors.New("sshpop.authorizeToken; error checking checking sshpop cert revocation: force"),
}
},
"fail/cert-already-revoked": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return true, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
assert.FatalError(t, err)
tok, err := generateSSHPOPToken(p, cert, jwk)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("sshpop.authorizeToken; sshpop certificate is revoked"),
}
},
"fail/cert-not-yet-valid": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{
CertType: ssh.UserCert,
ValidAfter: uint64(time.Now().Add(time.Minute).Unix()),
}, sshSigner)
assert.FatalError(t, err)
tok, err := generateSSHPOPToken(p, cert, jwk)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("sshpop.authorizeToken; sshpop certificate validAfter is in the future"),
}
},
"fail/cert-past-validity": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{
CertType: ssh.UserCert,
ValidBefore: uint64(time.Now().Add(-time.Minute).Unix()),
}, sshSigner)
assert.FatalError(t, err)
tok, err := generateSSHPOPToken(p, cert, jwk)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("sshpop.authorizeToken; sshpop certificate validBefore is in the past"),
}
},
"fail/no-signer-found": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner)
assert.FatalError(t, err)
tok, err := generateSSHPOPToken(p, cert, jwk)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("sshpop.authorizeToken; could not find valid ca signer to verify sshpop certificate"),
}
},
"fail/error-parsing-claims-bad-sig": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, _, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
assert.FatalError(t, err)
otherJWK, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
tok, err := generateSSHPOPToken(p, cert, otherJWK)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("sshpop.authorizeToken; error parsing sshpop token claims"),
}
},
"fail/invalid-claims-issuer": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
assert.FatalError(t, err)
tok, err := generateToken("foo", "bar", testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("sshpop.authorizeToken; invalid sshpop token"),
}
},
"fail/invalid-audience": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), "invalid-aud", "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("sshpop.authorizeToken; sshpop token has invalid audience claim (aud)"),
}
},
"fail/empty-subject": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
assert.FatalError(t, err)
tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("sshpop.authorizeToken; sshpop token subject cannot be empty"),
}
},
"ok": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
assert.FatalError(t, err)
tok, err := generateSSHPOPToken(p, cert, jwk)
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
assert.NotNil(t, claims)
}
}
})
}
}
func TestSSHPOP_AuthorizeSSHRevoke(t *testing.T) {
key, err := pemutil.Read("./testdata/secrets/ssh_user_ca_key")
assert.FatalError(t, err)
signer, ok := key.(crypto.Signer)
assert.Fatal(t, ok, "could not cast ssh signing key to crypto signer")
sshSigner, err := ssh.NewSignerFromSigner(signer)
assert.FatalError(t, err)
type test struct {
p *SSHPOP
token string
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("sshpop.AuthorizeSSHRevoke: sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "),
}
},
"fail/subject-not-equal-serial": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRevoke[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusBadRequest,
err: errors.New("sshpop.AuthorizeSSHRevoke; sshpop token subject must be equivalent to sshpop certificate serial number"),
}
},
"ok": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.UserCert}, sshSigner)
assert.FatalError(t, err)
tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRevoke[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
func TestSSHPOP_AuthorizeSSHRenew(t *testing.T) {
key, err := pemutil.Read("./testdata/secrets/ssh_user_ca_key")
assert.FatalError(t, err)
userSigner, ok := key.(crypto.Signer)
assert.Fatal(t, ok, "could not cast ssh user signing key to crypto signer")
sshUserSigner, err := ssh.NewSignerFromSigner(userSigner)
assert.FatalError(t, err)
hostKey, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key")
assert.FatalError(t, err)
hostSigner, ok := hostKey.(crypto.Signer)
assert.Fatal(t, ok, "could not cast ssh host signing key to crypto signer")
sshHostSigner, err := ssh.NewSignerFromSigner(hostSigner)
assert.FatalError(t, err)
type test struct {
p *SSHPOP
token string
cert *ssh.Certificate
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("sshpop.AuthorizeSSHRenew: sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "),
}
},
"fail/not-host-cert": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRenew[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusBadRequest,
err: errors.New("sshpop.AuthorizeSSHRenew; sshpop certificate must be a host ssh certificate"),
}
},
"ok": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner)
assert.FatalError(t, err)
tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRenew[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
cert: cert,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if cert, err := tc.p.AuthorizeSSHRenew(context.Background(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
assert.Equals(t, tc.cert.Nonce, cert.Nonce)
}
}
})
}
}
func TestSSHPOP_AuthorizeSSHRekey(t *testing.T) {
key, err := pemutil.Read("./testdata/secrets/ssh_user_ca_key")
assert.FatalError(t, err)
userSigner, ok := key.(crypto.Signer)
assert.Fatal(t, ok, "could not cast ssh user signing key to crypto signer")
sshUserSigner, err := ssh.NewSignerFromSigner(userSigner)
assert.FatalError(t, err)
hostKey, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key")
assert.FatalError(t, err)
hostSigner, ok := hostKey.(crypto.Signer)
assert.Fatal(t, ok, "could not cast ssh host signing key to crypto signer")
sshHostSigner, err := ssh.NewSignerFromSigner(hostSigner)
assert.FatalError(t, err)
type test struct {
p *SSHPOP
token string
cert *ssh.Certificate
err error
code int
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("sshpop.AuthorizeSSHRekey: sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: "),
}
},
"fail/not-host-cert": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.SSHRekey[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusBadRequest,
err: errors.New("sshpop.AuthorizeSSHRekey; sshpop certificate must be a host ssh certificate"),
}
},
"ok": func(t *testing.T) test {
p, err := generateSSHPOP()
assert.FatalError(t, err)
p.db = &db.MockAuthDB{
MIsSSHRevoked: func(sn string) (bool, error) {
return false, nil
},
}
cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner)
assert.FatalError(t, err)
tok, err := generateToken("123455", p.GetName(), testAudiences.SSHRekey[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
cert: cert,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if cert, opts, err := tc.p.AuthorizeSSHRekey(context.Background(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
assert.Len(t, 3, opts)
for _, o := range opts {
switch v := o.(type) {
case *sshDefaultPublicKeyValidator:
case *sshCertificateDefaultValidator:
case *sshCertificateValidityValidator:
assert.Equals(t, v.Claimer, tc.p.claimer)
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}
}
assert.Equals(t, tc.cert.Nonce, cert.Nonce)
}
}
})
}
}
func TestSSHPOP_ExtractSSHPOPCert(t *testing.T) {
hostKey, err := pemutil.Read("./testdata/secrets/ssh_host_ca_key")
assert.FatalError(t, err)
hostSigner, ok := hostKey.(crypto.Signer)
assert.Fatal(t, ok, "could not cast ssh host signing key to crypto signer")
sshHostSigner, err := ssh.NewSignerFromSigner(hostSigner)
assert.FatalError(t, err)
type test struct {
token string
cert *ssh.Certificate
jwk *jose.JSONWebKey
err error
}
tests := map[string]func(*testing.T) test{
"fail/bad-token": func(t *testing.T) test {
return test{
token: "foo",
err: errors.New("extractSSHPOPCert; error parsing token"),
}
},
"fail/sshpop-missing": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
tok, err := generateToken("sub", "sshpop-provisioner", testAudiences.SSHRekey[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk)
assert.FatalError(t, err)
return test{
token: tok,
err: errors.New("extractSSHPOPCert; token missing sshpop header"),
}
},
"fail/wrong-sshpop-type": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
tok, err := generateToken("123455", "sshpop-provisioner", testAudiences.SSHRekey[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, func(so *jose.SignerOptions) error {
so.WithHeader("sshpop", 12345)
return nil
})
assert.FatalError(t, err)
return test{
token: tok,
err: errors.New("extractSSHPOPCert; error unexpected type for sshpop header: "),
}
},
"fail/base64decode-error": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
tok, err := generateToken("123455", "sshpop-provisioner", testAudiences.SSHRekey[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, func(so *jose.SignerOptions) error {
so.WithHeader("sshpop", "!@#$%^&*")
return nil
})
assert.FatalError(t, err)
return test{
token: tok,
err: errors.New("extractSSHPOPCert; error base64 decoding sshpop header: illegal base64"),
}
},
"fail/parsing-sshpop-pubkey": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
tok, err := generateToken("123455", "sshpop-provisioner", testAudiences.SSHRekey[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, func(so *jose.SignerOptions) error {
so.WithHeader("sshpop", base64.StdEncoding.EncodeToString([]byte("foo")))
return nil
})
assert.FatalError(t, err)
return test{
token: tok,
err: errors.New("extractSSHPOPCert; error parsing ssh public key"),
}
},
"ok": func(t *testing.T) test {
cert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner)
assert.FatalError(t, err)
tok, err := generateToken("123455", "sshpop-provisioner", testAudiences.SSHRekey[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk, withSSHPOPFile(cert))
assert.FatalError(t, err)
return test{
token: tok,
jwk: jwk,
cert: cert,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if cert, jwt, err := ExtractSSHPOPCert(tc.token); err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
assert.Equals(t, tc.cert.Nonce, cert.Nonce)
assert.Equals(t, tc.jwk.KeyID, jwt.Headers[0].KeyID)
}
}
})
}
}

View file

@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJj80EJXJR9vxefhdqOLSdzRzBw24t9YKPxb+eCYLf7BU50pJQnB/jK2ZM3qLFbieLaYjngZ86T4DzHxlPAnlAY=

View file

@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ8einS88ZaWpcTZG27D5N9JDKfGv0rzjDByLGsZzMsLYl3XcsN9IWKXB6b+5GJ3UaoZf/pFxzRzIdDIh7Ypw3Y=

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIHzAUYu3h8e1gL5ONGZo+lghJJa9rl1TvP2UlqDXazxvoAoGCCqGSM49
AwEHoUQDQgAEOLScS+1Yzmqdyots9lSC0tzTSXUXEgyOD9wYrQ0BqnVZtBXlQw1p
m3fnF/7Ehl6bD1YZWjrF1t+IBZQMq1uBBw==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINWGD2xneE43YeytQzORItISxv6d/oH+9TXvDKHo6TyXoAoGCCqGSM49
AwEHoUQDQgAEVK/EtXgVV7+7ppnQSjCtI5qb/gIGnQUF4i//F/JKKho7kRNyMDSn
BP3kndiv8Yfxg4PsyIRY5ZofbEo5eJE6bg==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKZCgb5pTSSCbr/xcHCOkl9O6tQtZmNahr3Ap3/c2nBLoAoGCCqGSM49
AwEHoUQDQgAEmPzQQlclH2/F5+F2o4tJ3NHMHDbi31go/Fv54Jgt/sFTnSklCcH+
MrZkzeosVuJ4tpiOeBnzpPgPMfGU8CeUBg==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDuzykyPM6rLnSoyF4jnOpPAlyKZERqtaB8PTh179DMgoAoGCCqGSM49
AwEHoUQDQgAEnx6KdLzxlpalxNkbbsPk30kMp8a/SvOMMHIsaxnMywtiXddyw30h
YpcHpv7kYndRqhl/+kXHNHMh0MiHtinDdg==
-----END EC PRIVATE KEY-----

View file

@ -19,6 +19,7 @@ import (
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/randutil"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ssh"
)
var (
@ -47,24 +48,6 @@ var (
}
)
func provisionerClaims() *Claims {
ddr := false
des := true
return &Claims{
MinTLSDur: &Duration{5 * time.Minute},
MaxTLSDur: &Duration{24 * time.Hour},
DefaultTLSDur: &Duration{24 * time.Hour},
DisableRenewal: &ddr,
MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &Duration{Duration: 4 * time.Hour},
MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &des,
}
}
const awsTestCertificate = `-----BEGIN CERTIFICATE-----
MIICFTCCAX6gAwIBAgIRAKmbVVYAl/1XEqRfF3eJ97MwDQYJKoZIhvcNAQELBQAw
GDEWMBQGA1UEAxMNQVdTIFRlc3QgQ2VydDAeFw0xOTA0MjQyMjU3MzlaFw0yOTA0
@ -204,7 +187,7 @@ func generateJWK() (*JWK, error) {
}
func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) {
fooPubB, err := ioutil.ReadFile("./testdata/foo.pub")
fooPubB, err := ioutil.ReadFile("./testdata/certs/foo.pub")
if err != nil {
return nil, err
}
@ -212,7 +195,7 @@ func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) {
if err != nil {
return nil, err
}
barPubB, err := ioutil.ReadFile("./testdata/bar.pub")
barPubB, err := ioutil.ReadFile("./testdata/certs/bar.pub")
if err != nil {
return nil, err
}
@ -240,6 +223,46 @@ func generateK8sSA(inputPubKey interface{}) (*K8sSA, error) {
}, nil
}
func generateSSHPOP() (*SSHPOP, error) {
name, err := randutil.Alphanumeric(10)
if err != nil {
return nil, err
}
claimer, err := NewClaimer(nil, globalProvisionerClaims)
if err != nil {
return nil, err
}
userB, err := ioutil.ReadFile("./testdata/certs/ssh_user_ca_key.pub")
if err != nil {
return nil, err
}
userKey, _, _, _, err := ssh.ParseAuthorizedKey(userB)
if err != nil {
return nil, err
}
hostB, err := ioutil.ReadFile("./testdata/certs/ssh_host_ca_key.pub")
if err != nil {
return nil, err
}
hostKey, _, _, _, err := ssh.ParseAuthorizedKey(hostB)
if err != nil {
return nil, err
}
return &SSHPOP{
Name: name,
Type: "SSHPOP",
Claims: &globalProvisionerClaims,
audiences: testAudiences,
claimer: claimer,
sshPubKeys: &SSHKeys{
UserKeys: []ssh.PublicKey{userKey},
HostKeys: []ssh.PublicKey{hostKey},
},
}, nil
}
func generateX5C(root []byte) (*X5C, error) {
if root == nil {
root = []byte(`-----BEGIN CERTIFICATE-----
@ -589,6 +612,13 @@ func withX5CHdr(certs []*x509.Certificate) tokOption {
}
}
func withSSHPOPFile(cert *ssh.Certificate) tokOption {
return func(so *jose.SignerOptions) error {
so.WithHeader("sshpop", base64.StdEncoding.EncodeToString(cert.Marshal()))
return nil
}
}
func generateToken(sub, iss, aud string, email string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")
@ -630,6 +660,24 @@ func generateToken(sub, iss, aud string, email string, sans []string, iat time.T
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func generateX5CSSHToken(jwk *jose.JSONWebKey, claims *x5cPayload, tokOpts ...tokOption) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader("kid", jwk.KeyID)
for _, o := range tokOpts {
if err := o(so); err != nil {
return "", err
}
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)
if err != nil {
return "", err
}
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func getK8sSAPayload() *k8sSAPayload {
return &k8sSAPayload{
Claims: jose.Claims{

View file

@ -4,9 +4,11 @@ import (
"context"
"crypto/x509"
"encoding/pem"
"net/http"
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/jose"
)
@ -121,19 +123,20 @@ func (p *X5C) Init(config Config) error {
func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, error) {
jwt, err := jose.ParseSigned(token)
if err != nil {
return nil, errors.Wrapf(err, "error parsing token")
return nil, errs.Wrap(http.StatusUnauthorized, err, "x5c.authorizeToken; error parsing x5c token")
}
verifiedChains, err := jwt.Headers[0].Certificates(x509.VerifyOptions{
Roots: p.rootPool,
})
if err != nil {
return nil, errors.Wrap(err, "error verifying x5c certificate chain")
return nil, errs.Wrap(http.StatusUnauthorized, err,
"x5c.authorizeToken; error verifying x5c certificate chain in token")
}
leaf := verifiedChains[0][0]
if leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
return nil, errors.New("certificate used to sign x5c token cannot be used for digital signature")
return nil, errs.Unauthorized(errors.New("x5c.authorizeToken; certificate used to sign x5c token cannot be used for digital signature"))
}
// Using the leaf certificates key to validate the claims accomplishes two
@ -143,7 +146,7 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err
// 2. Asserts that the claims are valid - have not been tampered with.
var claims x5cPayload
if err = jwt.Claims(leaf.PublicKey, &claims); err != nil {
return nil, errors.Wrap(err, "error parsing claims")
return nil, errs.Wrap(http.StatusUnauthorized, err, "x5c.authorizeToken; error parsing x5c claims")
}
// According to "rfc7519 JSON Web Token" acceptable skew should be no
@ -152,16 +155,17 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err
Issuer: p.Name,
Time: time.Now().UTC(),
}, time.Minute); err != nil {
return nil, errors.Wrapf(err, "invalid token")
return nil, errs.Wrapf(http.StatusUnauthorized, err, "x5c.authorizeToken; invalid x5c claims")
}
// validate audiences with the defaults
if !matchesAudience(claims.Audience, audiences) {
return nil, errors.New("invalid token: invalid audience claim (aud)")
return nil, errs.Unauthorized(errors.Errorf("x5c.authorizeToken; x5c token has invalid audience "+
"claim (aud); expected %s, but got %s", audiences, claims.Audience))
}
if claims.Subject == "" {
return nil, errors.New("token subject cannot be empty")
return nil, errs.Unauthorized(errors.New("x5c.authorizeToken; x5c token subject cannot be empty"))
}
// Save the verified chains on the x5c payload object.
@ -173,14 +177,14 @@ func (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, err
// revoke the certificate with serial number in the `sub` property.
func (p *X5C) AuthorizeRevoke(ctx context.Context, token string) error {
_, err := p.authorizeToken(token, p.audiences.Revoke)
return err
return errs.Wrap(http.StatusInternalServerError, err, "x5c.AuthorizeRevoke")
}
// AuthorizeSign validates the given token.
func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := p.authorizeToken(token, p.audiences.Sign)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "x5c.AuthorizeSign")
}
// NOTE: This is for backwards compatibility with older versions of cli
@ -209,7 +213,7 @@ func (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er
// AuthorizeRenew returns an error if the renewal is disabled.
func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
if p.claimer.IsDisableRenewal() {
return errors.Errorf("renew is disabled for provisioner %s", p.GetID())
return errs.Unauthorized(errors.Errorf("x5c.AuthorizeRenew; renew is disabled for x5c provisioner %s", p.GetID()))
}
return nil
}
@ -217,16 +221,16 @@ func (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
if !p.claimer.IsSSHCAEnabled() {
return nil, errors.Errorf("ssh ca is disabled for provisioner %s", p.GetID())
return nil, errs.Unauthorized(errors.Errorf("x5c.AuthorizeSSHSign; sshCA is disabled for x5c provisioner %s", p.GetID()))
}
claims, err := p.authorizeToken(token, p.audiences.SSHSign)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "x5c.AuthorizeSSHSign")
}
if claims.Step == nil || claims.Step.SSH == nil {
return nil, errors.New("authorization token must be an SSH provisioning token")
return nil, errs.Unauthorized(errors.New("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token"))
}
opts := claims.Step.SSH
@ -245,18 +249,18 @@ func (p *X5C) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption,
}
t := now()
if !opts.ValidAfter.IsZero() {
signOptions = append(signOptions, sshCertificateValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix()))
signOptions = append(signOptions, sshCertValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix()))
}
if !opts.ValidBefore.IsZero() {
signOptions = append(signOptions, sshCertificateValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix()))
signOptions = append(signOptions, sshCertValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix()))
}
// Make sure to define the the KeyID
if opts.KeyID == "" {
signOptions = append(signOptions, sshCertificateKeyIDModifier(claims.Subject))
signOptions = append(signOptions, sshCertKeyIDModifier(claims.Subject))
}
// Default to a user certificate with no principals if not set
signOptions = append(signOptions, sshCertificateDefaultsModifier{CertType: SSHUserCert})
signOptions = append(signOptions, sshCertDefaultsModifier{CertType: SSHUserCert})
return append(signOptions,
// Set the default extensions.

View file

@ -2,14 +2,16 @@ package provisioner
import (
"context"
"crypto/x509"
"net"
"net/http"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/randutil"
"github.com/smallstep/cli/jose"
)
@ -151,9 +153,15 @@ M46l92gdOozT
}
func TestX5C_authorizeToken(t *testing.T) {
x5cCerts, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt")
assert.FatalError(t, err)
x5cJWK, err := jose.ParseKey("./testdata/secrets/x5c-leaf.key")
assert.FatalError(t, err)
type test struct {
p *X5C
token string
code int
err error
}
tests := map[string]func(*testing.T) test{
@ -163,7 +171,8 @@ func TestX5C_authorizeToken(t *testing.T) {
return test{
p: p,
token: "foo",
err: errors.New("error parsing token"),
code: http.StatusUnauthorized,
err: errors.New("x5c.authorizeToken; error parsing x5c token"),
}
},
"fail/invalid-cert-chain": func(t *testing.T) test {
@ -190,7 +199,8 @@ a5wpg+9s6QIgHIW6L60F8klQX+EO3o0SBqLeNcaskA4oSZsKjEdpSGo=
return test{
p: p,
token: tok,
err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"),
code: http.StatusUnauthorized,
err: errors.New("x5c.authorizeToken; error verifying x5c certificate chain in token"),
}
},
"fail/doubled-up-self-signed-cert": func(t *testing.T) test {
@ -228,7 +238,8 @@ EXAHTA9L
return test{
p: p,
token: tok,
err: errors.New("error verifying x5c certificate chain: x509: certificate signed by unknown authority"),
code: http.StatusUnauthorized,
err: errors.New("x5c.authorizeToken; error verifying x5c certificate chain in token"),
}
},
"fail/digital-signature-ext-required": func(t *testing.T) test {
@ -269,7 +280,8 @@ lgsqsR63is+0YQ==
return test{
p: p,
token: tok,
err: errors.New("certificate used to sign x5c token cannot be used for digital signature"),
code: http.StatusUnauthorized,
err: errors.New("x5c.authorizeToken; certificate used to sign x5c token cannot be used for digital signature"),
}
},
"fail/signature-does-not-match-x5c-pub-key": func(t *testing.T) test {
@ -309,74 +321,58 @@ lgsqsR63is+0YQ==
return test{
p: p,
token: tok,
err: errors.New("error parsing claims: square/go-jose: error in cryptographic primitive"),
code: http.StatusUnauthorized,
err: errors.New("x5c.authorizeToken; error parsing x5c claims"),
}
},
"fail/invalid-issuer": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", "foobar", testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
withX5CHdr(x5cCerts))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("invalid token: square/go-jose/jwt: validation failed, invalid issuer claim (iss)"),
code: http.StatusUnauthorized,
err: errors.New("x5c.authorizeToken; invalid x5c claims"),
}
},
"fail/invalid-audience": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", p.GetName(), "foobar", "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
withX5CHdr(x5cCerts))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("invalid token: invalid audience claim (aud)"),
code: http.StatusUnauthorized,
err: errors.New("x5c.authorizeToken; x5c token has invalid audience claim (aud)"),
}
},
"fail/empty-subject": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
withX5CHdr(x5cCerts))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
err: errors.New("token subject cannot be empty"),
code: http.StatusUnauthorized,
err: errors.New("x5c.authorizeToken; x5c token subject cannot be empty"),
}
},
"ok": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
withX5CHdr(x5cCerts))
assert.FatalError(t, err)
return test{
p: p,
@ -389,6 +385,9 @@ lgsqsR63is+0YQ==
tc := tt(t)
if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
@ -402,10 +401,15 @@ lgsqsR63is+0YQ==
}
func TestX5C_AuthorizeSign(t *testing.T) {
certs, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/secrets/x5c-leaf.key")
assert.FatalError(t, err)
type test struct {
p *X5C
token string
ctx context.Context
code int
err error
dns []string
emails []string
@ -418,56 +422,11 @@ func TestX5C_AuthorizeSign(t *testing.T) {
return test{
p: p,
token: "foo",
ctx: NewContextWithMethod(context.Background(), SignMethod),
err: errors.New("error parsing token"),
}
},
"fail/ssh/disabled": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
p.claimer.claims = provisionerClaims()
*p.claimer.claims.EnableSSHCA = false
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignSSHMethod),
token: tok,
err: errors.Errorf("ssh ca is disabled for provisioner x5c/%s", p.GetName()),
}
},
"fail/ssh/invalid-token": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
[]string{"test.smallstep.com"}, time.Now(), jwk,
withX5CHdr(certs))
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignSSHMethod),
token: tok,
err: errors.New("authorization token must be an SSH provisioning token"),
code: http.StatusUnauthorized,
err: errors.New("x5c.AuthorizeSign: x5c.authorizeToken; error parsing x5c token"),
}
},
"ok/empty-sans": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
@ -476,7 +435,6 @@ func TestX5C_AuthorizeSign(t *testing.T) {
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignMethod),
token: tok,
dns: []string{"foo"},
emails: []string{},
@ -484,11 +442,6 @@ func TestX5C_AuthorizeSign(t *testing.T) {
}
},
"ok/multi-sans": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
@ -497,7 +450,6 @@ func TestX5C_AuthorizeSign(t *testing.T) {
assert.FatalError(t, err)
return test{
p: p,
ctx: NewContextWithMethod(context.Background(), SignMethod),
token: tok,
dns: []string{"foo"},
emails: []string{"max@smallstep.com"},
@ -508,8 +460,11 @@ func TestX5C_AuthorizeSign(t *testing.T) {
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if opts, err := tc.p.AuthorizeSign(tc.ctx, tc.token); err != nil {
if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
@ -554,126 +509,11 @@ func TestX5C_AuthorizeSign(t *testing.T) {
}
}
func TestX5C_AuthorizeSSHSign(t *testing.T) {
_, fn := mockNow()
defer fn()
type test struct {
p *X5C
token string
claims *x5cPayload
err error
}
tests := map[string]func(*testing.T) test{
"fail/no-Step-claim": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return test{
p: p,
claims: new(x5cPayload),
err: errors.New("authorization token must be an SSH provisioning token"),
}
},
"fail/no-SSH-subattribute-in-claims": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return test{
p: p,
claims: &x5cPayload{Step: new(stepPayload)},
err: errors.New("authorization token must be an SSH provisioning token"),
}
},
"ok/with-claims": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
return test{
p: p,
claims: &x5cPayload{
Step: &stepPayload{SSH: &SSHOptions{
CertType: SSHHostCert,
Principals: []string{"max", "mariano", "alan"},
ValidAfter: TimeDuration{d: 5 * time.Minute},
ValidBefore: TimeDuration{d: 10 * time.Minute},
}},
Claims: jose.Claims{Subject: "foo"},
chains: [][]*x509.Certificate{certs},
},
}
},
"ok/without-claims": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
assert.FatalError(t, err)
return test{
p: p,
claims: &x5cPayload{
Step: &stepPayload{SSH: &SSHOptions{}},
Claims: jose.Claims{Subject: "foo"},
chains: [][]*x509.Certificate{certs},
},
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if opts, err := tc.p.AuthorizeSSHSign(context.TODO(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
if assert.NotNil(t, opts) {
tot := 0
nw := now()
for _, o := range opts {
switch v := o.(type) {
case sshCertificateOptionsValidator:
tc.claims.Step.SSH.ValidAfter.t = time.Time{}
tc.claims.Step.SSH.ValidBefore.t = time.Time{}
assert.Equals(t, SSHOptions(v), *tc.claims.Step.SSH)
case sshCertificateKeyIDModifier:
assert.Equals(t, string(v), "foo")
case sshCertTypeModifier:
assert.Equals(t, string(v), tc.claims.Step.SSH.CertType)
case sshCertPrincipalsModifier:
assert.Equals(t, []string(v), tc.claims.Step.SSH.Principals)
case sshCertificateValidAfterModifier:
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix())
case sshCertificateValidBeforeModifier:
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix())
case sshCertificateDefaultsModifier:
assert.Equals(t, SSHOptions(v), SSHOptions{CertType: SSHUserCert})
case *sshLimitDuration:
assert.Equals(t, v.Claimer, tc.p.claimer)
assert.Equals(t, v.NotAfter, tc.claims.chains[0][0].NotAfter)
case *sshCertificateValidityValidator:
assert.Equals(t, v.Claimer, tc.p.claimer)
case *sshDefaultExtensionModifier, *sshDefaultPublicKeyValidator,
*sshCertificateDefaultValidator:
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}
tot++
}
if len(tc.claims.Step.SSH.CertType) > 0 {
assert.Equals(t, tot, 12)
} else {
assert.Equals(t, tot, 8)
}
}
}
}
})
}
}
func TestX5C_AuthorizeRevoke(t *testing.T) {
type test struct {
p *X5C
token string
code int
err error
}
tests := map[string]func(*testing.T) test{
@ -683,13 +523,14 @@ func TestX5C_AuthorizeRevoke(t *testing.T) {
return test{
p: p,
token: "foo",
err: errors.New("error parsing token"),
code: http.StatusUnauthorized,
err: errors.New("x5c.AuthorizeRevoke: x5c.authorizeToken; error parsing x5c token"),
}
},
"ok": func(t *testing.T) test {
certs, err := pemutil.ReadCertificateBundle("./testdata/x5c-leaf.crt")
certs, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt")
assert.FatalError(t, err)
jwk, err := jose.ParseKey("./testdata/x5c-leaf.key")
jwk, err := jose.ParseKey("./testdata/secrets/x5c-leaf.key")
assert.FatalError(t, err)
p, err := generateX5C(nil)
@ -709,6 +550,9 @@ func TestX5C_AuthorizeRevoke(t *testing.T) {
tc := tt(t)
if err := tc.p.AuthorizeRevoke(context.TODO(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
@ -719,33 +563,248 @@ func TestX5C_AuthorizeRevoke(t *testing.T) {
}
func TestX5C_AuthorizeRenew(t *testing.T) {
p1, err := generateX5C(nil)
assert.FatalError(t, err)
p2, err := generateX5C(nil)
assert.FatalError(t, err)
// disable renewal
disable := true
p2.Claims = &Claims{DisableRenewal: &disable}
p2.claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
type args struct {
cert *x509.Certificate
type test struct {
p *X5C
code int
err error
}
tests := []struct {
name string
prov *X5C
args args
wantErr bool
}{
{"ok", p1, args{nil}, false},
{"fail", p2, args{nil}, true},
tests := map[string]func(*testing.T) test{
"fail/renew-disabled": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
// disable renewal
disable := true
p.Claims = &Claims{DisableRenewal: &disable}
p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
return test{
p: p,
code: http.StatusUnauthorized,
err: errors.Errorf("x5c.AuthorizeRenew; renew is disabled for x5c provisioner %s", p.GetID()),
}
},
"ok": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return test{
p: p,
}
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.prov.AuthorizeRenew(context.TODO(), tt.args.cert); (err != nil) != tt.wantErr {
t.Errorf("X5C.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if err := tc.p.AuthorizeRenew(context.TODO(), nil); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
func TestX5C_AuthorizeSSHSign(t *testing.T) {
x5cCerts, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt")
assert.FatalError(t, err)
x5cJWK, err := jose.ParseKey("./testdata/secrets/x5c-leaf.key")
assert.FatalError(t, err)
_, fn := mockNow()
defer fn()
type test struct {
p *X5C
token string
claims *x5cPayload
code int
err error
}
tests := map[string]func(*testing.T) test{
"fail/sshCA-disabled": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
// disable sshCA
enable := false
p.Claims = &Claims{EnableSSHCA: &enable}
p.claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.Errorf("x5c.AuthorizeSSHSign; sshCA is disabled for x5c provisioner %s", p.GetID()),
}
},
"fail/invalid-token": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
return test{
p: p,
token: "foo",
code: http.StatusUnauthorized,
err: errors.New("x5c.AuthorizeSSHSign: x5c.authorizeToken; error parsing x5c token"),
}
},
"fail/no-Step-claim": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
tok, err := generateToken("foo", p.GetName(), testAudiences.SSHSign[0], "",
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
withX5CHdr(x5cCerts))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token"),
}
},
"fail/no-SSH-subattribute-in-claims": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
id, err := randutil.ASCII(64)
assert.FatalError(t, err)
now := time.Now()
claims := &x5cPayload{
Claims: jose.Claims{
ID: id,
Subject: "foo",
Issuer: p.GetName(),
IssuedAt: jose.NewNumericDate(now),
NotBefore: jose.NewNumericDate(now),
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
Audience: []string{testAudiences.SSHSign[0]},
},
Step: &stepPayload{},
}
tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))
assert.FatalError(t, err)
return test{
p: p,
token: tok,
code: http.StatusUnauthorized,
err: errors.New("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token"),
}
},
"ok/with-claims": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
id, err := randutil.ASCII(64)
assert.FatalError(t, err)
now := time.Now()
claims := &x5cPayload{
Claims: jose.Claims{
ID: id,
Subject: "foo",
Issuer: p.GetName(),
IssuedAt: jose.NewNumericDate(now),
NotBefore: jose.NewNumericDate(now),
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
Audience: []string{testAudiences.SSHSign[0]},
},
Step: &stepPayload{SSH: &SSHOptions{
CertType: SSHHostCert,
Principals: []string{"max", "mariano", "alan"},
ValidAfter: TimeDuration{d: 5 * time.Minute},
ValidBefore: TimeDuration{d: 10 * time.Minute},
}},
}
tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))
assert.FatalError(t, err)
return test{
p: p,
claims: claims,
token: tok,
}
},
"ok/without-claims": func(t *testing.T) test {
p, err := generateX5C(nil)
assert.FatalError(t, err)
id, err := randutil.ASCII(64)
assert.FatalError(t, err)
now := time.Now()
claims := &x5cPayload{
Claims: jose.Claims{
ID: id,
Subject: "foo",
Issuer: p.GetName(),
IssuedAt: jose.NewNumericDate(now),
NotBefore: jose.NewNumericDate(now),
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
Audience: []string{testAudiences.SSHSign[0]},
},
Step: &stepPayload{SSH: &SSHOptions{}},
}
tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))
assert.FatalError(t, err)
return test{
p: p,
claims: claims,
token: tok,
}
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
tc := tt(t)
if opts, err := tc.p.AuthorizeSSHSign(context.TODO(), tc.token); err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
if assert.NotNil(t, opts) {
tot := 0
nw := now()
for _, o := range opts {
switch v := o.(type) {
case sshCertificateOptionsValidator:
tc.claims.Step.SSH.ValidAfter.t = time.Time{}
tc.claims.Step.SSH.ValidBefore.t = time.Time{}
assert.Equals(t, SSHOptions(v), *tc.claims.Step.SSH)
case sshCertKeyIDModifier:
assert.Equals(t, string(v), "foo")
case sshCertTypeModifier:
assert.Equals(t, string(v), tc.claims.Step.SSH.CertType)
case sshCertPrincipalsModifier:
assert.Equals(t, []string(v), tc.claims.Step.SSH.Principals)
case sshCertValidAfterModifier:
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix())
case sshCertValidBeforeModifier:
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix())
case sshCertDefaultsModifier:
assert.Equals(t, SSHOptions(v), SSHOptions{CertType: SSHUserCert})
case *sshLimitDuration:
assert.Equals(t, v.Claimer, tc.p.claimer)
assert.Equals(t, v.NotAfter, x5cCerts[0].NotAfter)
case *sshCertificateValidityValidator:
assert.Equals(t, v.Claimer, tc.p.claimer)
case *sshDefaultExtensionModifier, *sshDefaultPublicKeyValidator,
*sshCertificateDefaultValidator:
case sshCertKeyIDValidator:
assert.Equals(t, string(v), "foo")
default:
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
}
tot++
}
if len(tc.claims.Step.SSH.CertType) > 0 {
assert.Equals(t, tot, 13)
} else {
assert.Equals(t, tot, 9)
}
}
}
}
})
}

View file

@ -122,10 +122,7 @@ func (a *Authority) GetSSHFederation() (*SSHKeys, error) {
// GetSSHConfig returns rendered templates for clients (user) or servers (host).
func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error) {
if a.sshCAUserCertSignKey == nil && a.sshCAHostCertSignKey == nil {
return nil, &apiError{
err: errors.New("getSSHConfig: ssh is not configured"),
code: http.StatusNotFound,
}
return nil, errs.NotFound(errors.New("getSSHConfig: ssh is not configured"))
}
var ts []templates.Template
@ -139,10 +136,7 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template
ts = a.config.Templates.SSH.Host
}
default:
return nil, &apiError{
err: errors.Errorf("getSSHConfig: type %s is not valid", typ),
code: http.StatusBadRequest,
}
return nil, errs.BadRequest(errors.Errorf("getSSHConfig: type %s is not valid", typ))
}
// Merge user and default data
@ -174,7 +168,8 @@ func (a *Authority) GetSSHConfig(typ string, data map[string]string) ([]template
// hostname.
func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error) {
if a.sshBastionFunc != nil {
return a.sshBastionFunc(user, hostname)
bs, err := a.sshBastionFunc(user, hostname)
return bs, errs.Wrap(http.StatusInternalServerError, err, "authority.GetSSHBastion")
}
if a.config.SSH != nil {
if a.config.SSH.Bastion != nil && a.config.SSH.Bastion.Hostname != "" {
@ -182,26 +177,7 @@ func (a *Authority) GetSSHBastion(user string, hostname string) (*Bastion, error
}
return nil, nil
}
return nil, &apiError{
err: errors.New("getSSHBastion: ssh is not configured"),
code: http.StatusNotFound,
}
}
// authorizeSSHSign loads the provisioner from the token, checks that it has not
// been used again and calls the provisioner AuthorizeSSHSign method. Returns a
// list of methods to apply to the signing flow.
func (a *Authority) authorizeSSHSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {
var errContext = apiCtx{"ott": ott}
p, err := a.authorizeToken(ctx, ott)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext}
}
opts, err := p.AuthorizeSSHSign(ctx, ott)
if err != nil {
return nil, &apiError{errors.Wrap(err, "authorizeSSHSign"), http.StatusUnauthorized, errContext}
}
return opts, nil
return nil, errs.NotFound(errors.New("authority.GetSSHBastion; ssh is not configured"))
}
// SignSSH creates a signed SSH certificate with the given public key and options.
@ -226,27 +202,21 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign
// validate the given SSHOptions
case provisioner.SSHCertificateOptionsValidator:
if err := o.Valid(opts); err != nil {
return nil, &apiError{err: err, code: http.StatusForbidden}
return nil, errs.Forbidden(err)
}
default:
return nil, &apiError{
err: errors.Errorf("signSSH: invalid extra option type %T", o),
code: http.StatusInternalServerError,
}
return nil, errs.InternalServerError(errors.Errorf("signSSH: invalid extra option type %T", o))
}
}
nonce, err := randutil.ASCII(32)
if err != nil {
return nil, &apiError{err: err, code: http.StatusInternalServerError}
return nil, errs.InternalServerError(err)
}
var serial uint64
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
return nil, &apiError{
err: errors.Wrap(err, "signSSH: error reading random number"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error reading random number")
}
// Build base certificate with the key and some random values
@ -258,13 +228,13 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign
// Use opts to modify the certificate
if err := opts.Modify(cert); err != nil {
return nil, &apiError{err: err, code: http.StatusForbidden}
return nil, errs.Forbidden(err)
}
// Use provisioner modifiers
for _, m := range mods {
if err := m.Modify(cert); err != nil {
return nil, &apiError{err: err, code: http.StatusForbidden}
return nil, errs.Forbidden(err)
}
}
@ -273,25 +243,16 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign
switch cert.CertType {
case ssh.UserCert:
if a.sshCAUserCertSignKey == nil {
return nil, &apiError{
err: errors.New("signSSH: user certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
return nil, errs.NotImplemented(errors.New("signSSH: user certificate signing is not enabled"))
}
signer = a.sshCAUserCertSignKey
case ssh.HostCert:
if a.sshCAHostCertSignKey == nil {
return nil, &apiError{
err: errors.New("signSSH: host certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
return nil, errs.NotImplemented(errors.New("signSSH: host certificate signing is not enabled"))
}
signer = a.sshCAHostCertSignKey
default:
return nil, &apiError{
err: errors.Errorf("signSSH: unexpected ssh certificate type: %d", cert.CertType),
code: http.StatusInternalServerError,
}
return nil, errs.InternalServerError(errors.Errorf("signSSH: unexpected ssh certificate type: %d", cert.CertType))
}
cert.SignatureKey = signer.PublicKey()
@ -302,71 +263,38 @@ func (a *Authority) SignSSH(key ssh.PublicKey, opts provisioner.SSHOptions, sign
// Sign the certificate
sig, err := signer.Sign(rand.Reader, data)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "signSSH: error signing certificate"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error signing certificate")
}
cert.Signature = sig
// User provisioners validators
for _, v := range validators {
if err := v.Valid(cert); err != nil {
return nil, &apiError{err: err, code: http.StatusForbidden}
return nil, errs.Forbidden(err)
}
}
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
return nil, &apiError{
err: errors.Wrap(err, "signSSH: error storing certificate in db"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSH: error storing certificate in db")
}
return cert, nil
}
// authorizeSSHRenew authorizes an SSH certificate renewal request, by
// validating the contents of an SSHPOP token.
func (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
errContext := map[string]interface{}{"ott": token}
p, err := a.authorizeToken(ctx, token)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "authorizeSSHRenew"),
code: http.StatusUnauthorized,
context: errContext,
}
}
cert, err := p.AuthorizeSSHRenew(ctx, token)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "authorizeSSHRenew"),
code: http.StatusUnauthorized,
context: errContext,
}
}
return cert, nil
}
// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template.
func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error) {
nonce, err := randutil.ASCII(32)
if err != nil {
return nil, &apiError{err: err, code: http.StatusInternalServerError}
return nil, errs.InternalServerError(err)
}
var serial uint64
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
return nil, &apiError{
err: errors.Wrap(err, "renewSSH: error reading random number"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error reading random number")
}
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
return nil, errors.New("rewnewSSH: cannot renew certificate without validity period")
return nil, errs.BadRequest(errors.New("rewnewSSH: cannot renew certificate without validity period"))
}
backdate := a.config.AuthorityConfig.Backdate.Duration
@ -393,25 +321,16 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error)
switch cert.CertType {
case ssh.UserCert:
if a.sshCAUserCertSignKey == nil {
return nil, &apiError{
err: errors.New("renewSSH: user certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
return nil, errs.NotImplemented(errors.New("renewSSH: user certificate signing is not enabled"))
}
signer = a.sshCAUserCertSignKey
case ssh.HostCert:
if a.sshCAHostCertSignKey == nil {
return nil, &apiError{
err: errors.New("renewSSH: host certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
return nil, errs.NotImplemented(errors.New("renewSSH: host certificate signing is not enabled"))
}
signer = a.sshCAHostCertSignKey
default:
return nil, &apiError{
err: errors.Errorf("renewSSH: unexpected ssh certificate type: %d", cert.CertType),
code: http.StatusInternalServerError,
}
return nil, errs.InternalServerError(errors.Errorf("renewSSH: unexpected ssh certificate type: %d", cert.CertType))
}
cert.SignatureKey = signer.PublicKey()
@ -422,47 +341,17 @@ func (a *Authority) RenewSSH(oldCert *ssh.Certificate) (*ssh.Certificate, error)
// Sign the certificate
sig, err := signer.Sign(rand.Reader, data)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "renewSSH: error signing certificate"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error signing certificate")
}
cert.Signature = sig
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
return nil, &apiError{
err: errors.Wrap(err, "renewSSH: error storing certificate in db"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "renewSSH: error storing certificate in db")
}
return cert, nil
}
// authorizeSSHRekey authorizes an SSH certificate rekey request, by
// validating the contents of an SSHPOP token.
func (a *Authority) authorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) {
errContext := map[string]interface{}{"ott": token}
p, err := a.authorizeToken(ctx, token)
if err != nil {
return nil, nil, &apiError{
err: errors.Wrap(err, "authorizeSSHRenew"),
code: http.StatusUnauthorized,
context: errContext,
}
}
cert, opts, err := p.AuthorizeSSHRekey(ctx, token)
if err != nil {
return nil, nil, &apiError{
err: errors.Wrap(err, "authorizeSSHRekey"),
code: http.StatusUnauthorized,
context: errContext,
}
}
return cert, opts, nil
}
// RekeySSH creates a signed SSH certificate using the old SSH certificate as a template.
func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {
var validators []provisioner.SSHCertificateValidator
@ -473,28 +362,22 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp
case provisioner.SSHCertificateValidator:
validators = append(validators, o)
default:
return nil, &apiError{
err: errors.Errorf("rekeySSH: invalid extra option type %T", o),
code: http.StatusInternalServerError,
}
return nil, errs.InternalServerError(errors.Errorf("rekeySSH; invalid extra option type %T", o))
}
}
nonce, err := randutil.ASCII(32)
if err != nil {
return nil, &apiError{err: err, code: http.StatusInternalServerError}
return nil, errs.InternalServerError(err)
}
var serial uint64
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
return nil, &apiError{
err: errors.Wrap(err, "rekeySSH: error reading random number"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error reading random number")
}
if oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {
return nil, errors.New("rekeySSH: cannot rekey certificate without validity period")
return nil, errs.BadRequest(errors.New("rekeySSH; cannot rekey certificate without validity period"))
}
backdate := a.config.AuthorityConfig.Backdate.Duration
@ -521,25 +404,16 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp
switch cert.CertType {
case ssh.UserCert:
if a.sshCAUserCertSignKey == nil {
return nil, &apiError{
err: errors.New("rekeySSH: user certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
return nil, errs.NotImplemented(errors.New("rekeySSH; user certificate signing is not enabled"))
}
signer = a.sshCAUserCertSignKey
case ssh.HostCert:
if a.sshCAHostCertSignKey == nil {
return nil, &apiError{
err: errors.New("rekeySSH: host certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
return nil, errs.NotImplemented(errors.New("rekeySSH; host certificate signing is not enabled"))
}
signer = a.sshCAHostCertSignKey
default:
return nil, &apiError{
err: errors.Errorf("rekeySSH: unexpected ssh certificate type: %d", cert.CertType),
code: http.StatusInternalServerError,
}
return nil, errs.BadRequest(errors.Errorf("rekeySSH; unexpected ssh certificate type: %d", cert.CertType))
}
cert.SignatureKey = signer.PublicKey()
@ -547,80 +421,47 @@ func (a *Authority) RekeySSH(oldCert *ssh.Certificate, pub ssh.PublicKey, signOp
data := cert.Marshal()
data = data[:len(data)-4]
// Sign the certificate
// Sign the certificate.
sig, err := signer.Sign(rand.Reader, data)
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "rekeySSH: error signing certificate"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error signing certificate")
}
cert.Signature = sig
// User provisioners validators
// Apply validators from provisioner..
for _, v := range validators {
if err := v.Valid(cert); err != nil {
return nil, &apiError{err: err, code: http.StatusForbidden}
return nil, errs.Forbidden(err)
}
}
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
return nil, &apiError{
err: errors.Wrap(err, "rekeySSH: error storing certificate in db"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "rekeySSH; error storing certificate in db")
}
return cert, nil
}
// authorizeSSHRevoke authorizes an SSH certificate revoke request, by
// validating the contents of an SSHPOP token.
func (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error {
errContext := map[string]interface{}{"ott": token}
p, err := a.authorizeToken(ctx, token)
if err != nil {
return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext}
}
if err = p.AuthorizeSSHRevoke(ctx, token); err != nil {
return &apiError{errors.Wrap(err, "authorizeSSHRevoke"), http.StatusUnauthorized, errContext}
}
return nil
}
// SignSSHAddUser signs a certificate that provisions a new user in a server.
func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) {
if a.sshCAUserCertSignKey == nil {
return nil, &apiError{
err: errors.New("signSSHAddUser: user certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
return nil, errs.NotImplemented(errors.New("signSSHAddUser: user certificate signing is not enabled"))
}
if subject.CertType != ssh.UserCert {
return nil, &apiError{
err: errors.New("signSSHAddUser: certificate is not a user certificate"),
code: http.StatusForbidden,
}
return nil, errs.Forbidden(errors.New("signSSHAddUser: certificate is not a user certificate"))
}
if len(subject.ValidPrincipals) != 1 {
return nil, &apiError{
err: errors.New("signSSHAddUser: certificate does not have only one principal"),
code: http.StatusForbidden,
}
return nil, errs.Forbidden(errors.New("signSSHAddUser: certificate does not have only one principal"))
}
nonce, err := randutil.ASCII(32)
if err != nil {
return nil, &apiError{err: err, code: http.StatusInternalServerError}
return nil, errs.InternalServerError(err)
}
var serial uint64
if err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {
return nil, &apiError{
err: errors.Wrap(err, "signSSHAddUser: error reading random number"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error reading random number")
}
signer := a.sshCAUserCertSignKey
@ -656,10 +497,7 @@ func (a *Authority) SignSSHAddUser(key ssh.PublicKey, subject *ssh.Certificate)
cert.Signature = sig
if err = a.db.StoreSSHCertificate(cert); err != nil && err != db.ErrNotImplemented {
return nil, &apiError{
err: errors.Wrap(err, "signSSHAddUser: error storing certificate in db"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "signSSHAddUser: error storing certificate in db")
}
return cert, nil
@ -691,14 +529,12 @@ func (a *Authority) CheckSSHHost(ctx context.Context, principal string, token st
// GetSSHHosts returns a list of valid host principals.
func (a *Authority) GetSSHHosts(cert *x509.Certificate) ([]sshutil.Host, error) {
if a.sshGetHostsFunc != nil {
return a.sshGetHostsFunc(cert)
hosts, err := a.sshGetHostsFunc(cert)
return hosts, errs.Wrap(http.StatusInternalServerError, err, "getSSHHosts")
}
hostnames, err := a.db.GetSSHHostPrincipals()
if err != nil {
return nil, &apiError{
err: errors.Wrap(err, "getSSHHosts"),
code: http.StatusInternalServerError,
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "getSSHHosts")
}
hosts := make([]sshutil.Host, len(hostnames))

View file

@ -5,8 +5,10 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"fmt"
"net/http"
"reflect"
"testing"
"time"
@ -15,6 +17,8 @@ import (
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/certificates/sshutil"
"github.com/smallstep/certificates/templates"
"github.com/smallstep/cli/jose"
"golang.org/x/crypto/ssh"
@ -498,8 +502,8 @@ func TestAuthority_CheckSSHHost(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := testAuthority(t)
a.db = &MockAuthDB{
isSSHHost: func(_ string) (bool, error) {
a.db = &db.MockAuthDB{
MIsSSHHost: func(_ string) (bool, error) {
return tt.fields.exists, tt.fields.err
},
}
@ -640,6 +644,9 @@ func TestAuthority_GetSSHBastion(t *testing.T) {
if (err != nil) != tt.wantErr {
t.Errorf("Authority.GetSSHBastion() error = %v, wantErr %v", err, tt.wantErr)
return
} else if err != nil {
_, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Authority.GetSSHBastion() = %v, want %v", got, tt.want)
@ -647,3 +654,266 @@ func TestAuthority_GetSSHBastion(t *testing.T) {
})
}
}
func TestAuthority_GetSSHHosts(t *testing.T) {
a := testAuthority(t)
type test struct {
getHostsFunc func(*x509.Certificate) ([]sshutil.Host, error)
auth *Authority
cert *x509.Certificate
cmp func(got []sshutil.Host)
err error
code int
}
tests := map[string]func(t *testing.T) *test{
"fail/getHostsFunc-fail": func(t *testing.T) *test {
return &test{
getHostsFunc: func(cert *x509.Certificate) ([]sshutil.Host, error) {
return nil, errors.New("force")
},
cert: &x509.Certificate{},
err: errors.New("getSSHHosts: force"),
code: http.StatusInternalServerError,
}
},
"ok/getHostsFunc-defined": func(t *testing.T) *test {
hosts := []sshutil.Host{
{HostID: "1", Hostname: "foo"},
{HostID: "2", Hostname: "bar"},
}
return &test{
getHostsFunc: func(cert *x509.Certificate) ([]sshutil.Host, error) {
return hosts, nil
},
cert: &x509.Certificate{},
cmp: func(got []sshutil.Host) {
assert.Equals(t, got, hosts)
},
}
},
"fail/db-get-fail": func(t *testing.T) *test {
return &test{
auth: testAuthority(t, WithDatabase(&db.MockAuthDB{
MGetSSHHostPrincipals: func() ([]string, error) {
return nil, errors.New("force")
},
})),
cert: &x509.Certificate{},
err: errors.New("getSSHHosts: force"),
code: http.StatusInternalServerError,
}
},
"ok": func(t *testing.T) *test {
return &test{
auth: testAuthority(t, WithDatabase(&db.MockAuthDB{
MGetSSHHostPrincipals: func() ([]string, error) {
return []string{"foo", "bar"}, nil
},
})),
cert: &x509.Certificate{},
cmp: func(got []sshutil.Host) {
assert.Equals(t, got, []sshutil.Host{
{Hostname: "foo"},
{Hostname: "bar"},
})
},
}
},
}
for name, genTestCase := range tests {
t.Run(name, func(t *testing.T) {
tc := genTestCase(t)
auth := tc.auth
if auth == nil {
auth = a
}
auth.sshGetHostsFunc = tc.getHostsFunc
hosts, err := auth.GetSSHHosts(tc.cert)
if err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
tc.cmp(hosts)
}
}
})
}
}
func TestAuthority_RekeySSH(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.FatalError(t, err)
pub, err := ssh.NewPublicKey(key.Public())
assert.FatalError(t, err)
signKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.FatalError(t, err)
signer, err := ssh.NewSignerFromKey(signKey)
assert.FatalError(t, err)
userOptions := sshTestModifier{
CertType: ssh.UserCert,
}
now := time.Now().UTC()
a := testAuthority(t)
type test struct {
auth *Authority
userSigner ssh.Signer
hostSigner ssh.Signer
cert *ssh.Certificate
key ssh.PublicKey
signOpts []provisioner.SignOption
cmpResult func(old, n *ssh.Certificate)
err error
code int
}
tests := map[string]func(t *testing.T) *test{
"fail/opts-type": func(t *testing.T) *test {
return &test{
userSigner: signer,
hostSigner: signer,
key: pub,
signOpts: []provisioner.SignOption{userOptions},
err: errors.New("rekeySSH; invalid extra option type"),
code: http.StatusInternalServerError,
}
},
"fail/old-cert-validAfter": func(t *testing.T) *test {
return &test{
userSigner: signer,
hostSigner: signer,
cert: &ssh.Certificate{},
key: pub,
signOpts: []provisioner.SignOption{},
err: errors.New("rekeySSH; cannot rekey certificate without validity period"),
code: http.StatusBadRequest,
}
},
"fail/old-cert-validBefore": func(t *testing.T) *test {
return &test{
userSigner: signer,
hostSigner: signer,
cert: &ssh.Certificate{ValidAfter: uint64(now.Unix())},
key: pub,
signOpts: []provisioner.SignOption{},
err: errors.New("rekeySSH; cannot rekey certificate without validity period"),
code: http.StatusBadRequest,
}
},
"fail/old-cert-no-user-key": func(t *testing.T) *test {
return &test{
userSigner: nil,
hostSigner: signer,
cert: &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: ssh.UserCert},
key: pub,
signOpts: []provisioner.SignOption{},
err: errors.New("rekeySSH; user certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
},
"fail/old-cert-no-host-key": func(t *testing.T) *test {
return &test{
userSigner: signer,
hostSigner: nil,
cert: &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: ssh.HostCert},
key: pub,
signOpts: []provisioner.SignOption{},
err: errors.New("rekeySSH; host certificate signing is not enabled"),
code: http.StatusNotImplemented,
}
},
"fail/unexpected-old-cert-type": func(t *testing.T) *test {
return &test{
userSigner: signer,
hostSigner: signer,
cert: &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: 0},
key: pub,
signOpts: []provisioner.SignOption{},
err: errors.New("rekeySSH; unexpected ssh certificate type: 0"),
code: http.StatusBadRequest,
}
},
"fail/db-store": func(t *testing.T) *test {
return &test{
auth: testAuthority(t, WithDatabase(&db.MockAuthDB{
MStoreSSHCertificate: func(cert *ssh.Certificate) error {
return errors.New("force")
},
})),
userSigner: signer,
hostSigner: nil,
cert: &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: ssh.UserCert},
key: pub,
signOpts: []provisioner.SignOption{},
err: errors.New("rekeySSH; error storing certificate in db: force"),
code: http.StatusInternalServerError,
}
},
"ok": func(t *testing.T) *test {
va1 := now.Add(-24 * time.Hour)
vb1 := now.Add(-23 * time.Hour)
return &test{
userSigner: signer,
hostSigner: nil,
cert: &ssh.Certificate{
ValidAfter: uint64(va1.Unix()),
ValidBefore: uint64(vb1.Unix()),
CertType: ssh.UserCert,
ValidPrincipals: []string{"foo", "bar"},
KeyId: "foo",
},
key: pub,
signOpts: []provisioner.SignOption{},
cmpResult: func(old, n *ssh.Certificate) {
assert.Equals(t, n.CertType, old.CertType)
assert.Equals(t, n.ValidPrincipals, old.ValidPrincipals)
assert.Equals(t, n.KeyId, old.KeyId)
assert.True(t, n.ValidAfter > uint64(now.Add(-5*time.Minute).Unix()))
assert.True(t, n.ValidAfter < uint64(now.Add(5*time.Minute).Unix()))
l8r := now.Add(1 * time.Hour)
assert.True(t, n.ValidBefore > uint64(l8r.Add(-5*time.Minute).Unix()))
assert.True(t, n.ValidBefore < uint64(l8r.Add(5*time.Minute).Unix()))
},
}
},
}
for name, genTestCase := range tests {
t.Run(name, func(t *testing.T) {
tc := genTestCase(t)
auth := tc.auth
if auth == nil {
auth = a
}
a.sshCAUserCertSignKey = tc.userSigner
a.sshCAHostCertSignKey = tc.hostSigner
cert, err := auth.RekeySSH(tc.cert, tc.key, tc.signOpts...)
if err != nil {
if assert.NotNil(t, tc.err) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
tc.cmpResult(tc.cert, cert)
}
}
})
}
}

View file

@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJj80EJXJR9vxefhdqOLSdzRzBw24t9YKPxb+eCYLf7BU50pJQnB/jK2ZM3qLFbieLaYjngZ86T4DzHxlPAnlAY=

View file

@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ8einS88ZaWpcTZG27D5N9JDKfGv0rzjDByLGsZzMsLYl3XcsN9IWKXB6b+5GJ3UaoZf/pFxzRzIdDIh7Ypw3Y=

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKZCgb5pTSSCbr/xcHCOkl9O6tQtZmNahr3Ap3/c2nBLoAoGCCqGSM49
AwEHoUQDQgAEmPzQQlclH2/F5+F2o4tJ3NHMHDbi31go/Fv54Jgt/sFTnSklCcH+
MrZkzeosVuJ4tpiOeBnzpPgPMfGU8CeUBg==
-----END EC PRIVATE KEY-----

View file

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDuzykyPM6rLnSoyF4jnOpPAlyKZERqtaB8PTh179DMgoAoGCCqGSM49
AwEHoUQDQgAEnx6KdLzxlpalxNkbbsPk30kMp8a/SvOMMHIsaxnMywtiXddyw30h
YpcHpv7kYndRqhl/+kXHNHMh0MiHtinDdg==
-----END EC PRIVATE KEY-----

View file

@ -14,6 +14,7 @@ import (
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util"
@ -60,7 +61,7 @@ func withDefaultASN1DN(def *x509util.ASN1DN) x509util.WithOption {
// Sign creates a signed certificate from a certificate signing request.
func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Options, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
var (
errContext = apiCtx{"csr": csr, "signOptions": signOpts}
opts = []errs.Option{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)}
mods = []x509util.WithOption{withDefaultASN1DN(a.config.AuthorityConfig.Template)}
certValidators = []provisioner.CertificateValidator{}
issIdentity = a.intermediateIdentity
@ -75,54 +76,52 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti
certValidators = append(certValidators, k)
case provisioner.CertificateRequestValidator:
if err := k.Valid(csr); err != nil {
return nil, &apiError{errors.Wrap(err, "sign"), http.StatusUnauthorized, errContext}
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
case provisioner.ProfileModifier:
mods = append(mods, k.Option(signOpts))
default:
return nil, &apiError{errors.Errorf("sign: invalid extra option type %T", k),
http.StatusInternalServerError, errContext}
return nil, errs.InternalServerError(errors.Errorf("authority.Sign; invalid extra option type %T", k), opts...)
}
}
if err := csr.CheckSignature(); err != nil {
return nil, &apiError{errors.Wrap(err, "sign: invalid certificate request"),
http.StatusBadRequest, errContext}
return nil, errs.Wrap(http.StatusBadRequest, err, "authority.Sign; invalid certificate request", opts...)
}
leaf, err := x509util.NewLeafProfileWithCSR(csr, issIdentity.Crt, issIdentity.Key, mods...)
if err != nil {
return nil, &apiError{errors.Wrapf(err, "sign"), http.StatusInternalServerError, errContext}
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...)
}
for _, v := range certValidators {
if err := v.Valid(leaf.Subject()); err != nil {
return nil, &apiError{errors.Wrap(err, "sign"), http.StatusUnauthorized, errContext}
if err := v.Valid(leaf.Subject(), signOpts); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
}
crtBytes, err := leaf.CreateCertificate()
if err != nil {
return nil, &apiError{errors.Wrap(err, "sign: error creating new leaf certificate"),
http.StatusInternalServerError, errContext}
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error creating new leaf certificate", opts...)
}
serverCert, err := x509.ParseCertificate(crtBytes)
if err != nil {
return nil, &apiError{errors.Wrap(err, "sign: error parsing new leaf certificate"),
http.StatusInternalServerError, errContext}
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error parsing new leaf certificate", opts...)
}
caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw)
if err != nil {
return nil, &apiError{errors.Wrap(err, "sign: error parsing intermediate certificate"),
http.StatusInternalServerError, errContext}
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error parsing intermediate certificate", opts...)
}
if err = a.db.StoreCertificate(serverCert); err != nil {
if err != db.ErrNotImplemented {
return nil, &apiError{errors.Wrap(err, "sign: error storing certificate in db"),
http.StatusInternalServerError, errContext}
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error storing certificate in db", opts...)
}
}
@ -132,9 +131,11 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Opti
// Renew creates a new Certificate identical to the old certificate, except
// with a validity window that begins 'now'.
func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {
opts := []errs.Option{errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String())}
// Check step provisioner extensions
if err := a.authorizeRenew(oldCert); err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew", opts...)
}
// Issuer
@ -161,16 +162,16 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error
MaxPathLenZero: oldCert.MaxPathLenZero,
OCSPServer: oldCert.OCSPServer,
IssuingCertificateURL: oldCert.IssuingCertificateURL,
PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical,
PermittedEmailAddresses: oldCert.PermittedEmailAddresses,
DNSNames: oldCert.DNSNames,
EmailAddresses: oldCert.EmailAddresses,
IPAddresses: oldCert.IPAddresses,
URIs: oldCert.URIs,
PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical,
PermittedDNSDomains: oldCert.PermittedDNSDomains,
ExcludedDNSDomains: oldCert.ExcludedDNSDomains,
PermittedIPRanges: oldCert.PermittedIPRanges,
ExcludedIPRanges: oldCert.ExcludedIPRanges,
PermittedEmailAddresses: oldCert.PermittedEmailAddresses,
ExcludedEmailAddresses: oldCert.ExcludedEmailAddresses,
PermittedURIDomains: oldCert.PermittedURIDomains,
ExcludedURIDomains: oldCert.ExcludedURIDomains,
@ -190,29 +191,28 @@ func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error
leaf, err := x509util.NewLeafProfileWithTemplate(newCert,
issIdentity.Crt, issIdentity.Key)
if err != nil {
return nil, &apiError{err, http.StatusInternalServerError, apiCtx{}}
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew", opts...)
}
crtBytes, err := leaf.CreateCertificate()
if err != nil {
return nil, &apiError{errors.Wrap(err, "error renewing certificate from existing server certificate"),
http.StatusInternalServerError, apiCtx{}}
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Renew; error renewing certificate from existing server certificate", opts...)
}
serverCert, err := x509.ParseCertificate(crtBytes)
if err != nil {
return nil, &apiError{errors.Wrap(err, "error parsing new server certificate"),
http.StatusInternalServerError, apiCtx{}}
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Renew; error parsing new server certificate", opts...)
}
caCert, err := x509.ParseCertificate(issIdentity.Crt.Raw)
if err != nil {
return nil, &apiError{errors.Wrap(err, "error parsing intermediate certificate"),
http.StatusInternalServerError, apiCtx{}}
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Renew; error parsing intermediate certificate", opts...)
}
if err = a.db.StoreCertificate(serverCert); err != nil {
if err != db.ErrNotImplemented {
return nil, &apiError{errors.Wrap(err, "error storing certificate in db"),
http.StatusInternalServerError, apiCtx{}}
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Renew; error storing certificate in db", opts...)
}
}
@ -236,26 +236,26 @@ type RevokeOptions struct {
// being renewed.
//
// TODO: Add OCSP and CRL support.
func (a *Authority) Revoke(ctx context.Context, opts *RevokeOptions) error {
errContext := apiCtx{
"serialNumber": opts.Serial,
"reasonCode": opts.ReasonCode,
"reason": opts.Reason,
"passiveOnly": opts.PassiveOnly,
"mTLS": opts.MTLS,
"context": string(provisioner.MethodFromContext(ctx)),
func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error {
opts := []errs.Option{
errs.WithKeyVal("serialNumber", revokeOpts.Serial),
errs.WithKeyVal("reasonCode", revokeOpts.ReasonCode),
errs.WithKeyVal("reason", revokeOpts.Reason),
errs.WithKeyVal("passiveOnly", revokeOpts.PassiveOnly),
errs.WithKeyVal("MTLS", revokeOpts.MTLS),
errs.WithKeyVal("context", string(provisioner.MethodFromContext(ctx))),
}
if opts.MTLS {
errContext["certificate"] = base64.StdEncoding.EncodeToString(opts.Crt.Raw)
if revokeOpts.MTLS {
opts = append(opts, errs.WithKeyVal("certificate", base64.StdEncoding.EncodeToString(revokeOpts.Crt.Raw)))
} else {
errContext["ott"] = opts.OTT
opts = append(opts, errs.WithKeyVal("token", revokeOpts.OTT))
}
rci := &db.RevokedCertificateInfo{
Serial: opts.Serial,
ReasonCode: opts.ReasonCode,
Reason: opts.Reason,
MTLS: opts.MTLS,
Serial: revokeOpts.Serial,
ReasonCode: revokeOpts.ReasonCode,
Reason: revokeOpts.Reason,
MTLS: revokeOpts.MTLS,
RevokedAt: time.Now().UTC(),
}
@ -264,48 +264,43 @@ func (a *Authority) Revoke(ctx context.Context, opts *RevokeOptions) error {
err error
)
// If not mTLS then get the TokenID of the token.
if !opts.MTLS {
// Validate payload
token, err := jose.ParseSigned(opts.OTT)
if !revokeOpts.MTLS {
token, err := jose.ParseSigned(revokeOpts.OTT)
if err != nil {
return &apiError{errors.Wrapf(err, "revoke: error parsing token"),
http.StatusUnauthorized, errContext}
return errs.Wrap(http.StatusUnauthorized, err,
"authority.Revoke; error parsing token", opts...)
}
// Get claims w/out verification. We should have already verified this token
// earlier with a call to authorizeSSHRevoke.
// Get claims w/out verification.
var claims Claims
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
return &apiError{errors.Wrap(err, "revoke"), http.StatusUnauthorized, errContext}
return errs.Wrap(http.StatusUnauthorized, err, "authority.Revoke", opts...)
}
// This method will also validate the audiences for JWK provisioners.
var ok bool
p, ok = a.provisioners.LoadByToken(token, &claims.Claims)
if !ok {
return &apiError{
errors.Errorf("revoke: provisioner not found"),
http.StatusInternalServerError, errContext}
return errs.InternalServerError(errors.Errorf("authority.Revoke; provisioner not found"), opts...)
}
rci.TokenID, err = p.GetTokenID(opts.OTT)
rci.TokenID, err = p.GetTokenID(revokeOpts.OTT)
if err != nil {
return &apiError{errors.Wrap(err, "revoke: could not get ID for token"),
http.StatusInternalServerError, errContext}
return errs.Wrap(http.StatusInternalServerError, err,
"authority.Revoke; could not get ID for token")
}
errContext["tokenID"] = rci.TokenID
opts = append(opts, errs.WithKeyVal("tokenID", rci.TokenID))
} else {
// Load the Certificate provisioner if one exists.
p, err = a.LoadProvisionerByCertificate(opts.Crt)
p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt)
if err != nil {
return &apiError{
errors.Wrap(err, "revoke: unable to load certificate provisioner"),
http.StatusUnauthorized, errContext}
return errs.Wrap(http.StatusUnauthorized, err,
"authority.Revoke: unable to load certificate provisioner", opts...)
}
}
rci.ProvisionerID = p.GetID()
errContext["provisionerID"] = rci.ProvisionerID
opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID))
if provisioner.MethodFromContext(ctx) == provisioner.RevokeSSHMethod {
if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {
err = a.db.RevokeSSH(rci)
} else { // default to revoke x509
err = a.db.Revoke(rci)
@ -314,13 +309,12 @@ func (a *Authority) Revoke(ctx context.Context, opts *RevokeOptions) error {
case nil:
return nil
case db.ErrNotImplemented:
return &apiError{errors.New("revoke: no persistence layer configured"),
http.StatusNotImplemented, errContext}
return errs.NotImplemented(errors.New("authority.Revoke; no persistence layer configured"), opts...)
case db.ErrAlreadyExists:
return &apiError{errors.Errorf("revoke: certificate with serial number %s has already been revoked", rci.Serial),
http.StatusBadRequest, errContext}
return errs.BadRequest(errors.Errorf("authority.Revoke; certificate with serial "+
"number %s has already been revoked", rci.Serial), opts...)
default:
return &apiError{err, http.StatusInternalServerError, errContext}
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
}
}
@ -330,17 +324,17 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
a.intermediateIdentity.Crt, a.intermediateIdentity.Key,
x509util.WithHosts(strings.Join(a.config.DNSNames, ",")))
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate")
}
crtBytes, err := profile.CreateCertificate()
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate")
}
keyPEM, err := pemutil.Serialize(profile.SubjectPrivateKey())
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate")
}
crtPEM := pem.EncodeToMemory(&pem.Block{
@ -352,19 +346,21 @@ func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
// to a tls.Certificate.
intermediatePEM, err := pemutil.Serialize(a.intermediateIdentity.Crt)
if err != nil {
return nil, err
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate")
}
tlsCrt, err := tls.X509KeyPair(append(crtPEM,
pem.EncodeToMemory(intermediatePEM)...),
pem.EncodeToMemory(keyPEM))
if err != nil {
return nil, errors.Wrap(err, "error creating tls certificate")
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.GetTLSCertificate; error creating tls certificate")
}
// Get the 'leaf' certificate and set the attribute accordingly.
leaf, err := x509.ParseCertificate(tlsCrt.Certificate[0])
if err != nil {
return nil, errors.Wrap(err, "error parsing tls certificate")
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.GetTLSCertificate; error parsing tls certificate")
}
tlsCrt.Leaf = leaf

View file

@ -7,7 +7,6 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
@ -19,6 +18,7 @@ import (
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/keys"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/tlsutil"
@ -77,7 +77,7 @@ func getCSR(t *testing.T, priv interface{}, opts ...func(*x509.CertificateReques
return csr
}
func TestSign(t *testing.T) {
func TestAuthority_Sign(t *testing.T) {
pub, priv, err := keys.GenerateDefaultKeyPair()
assert.FatalError(t, err)
@ -102,7 +102,7 @@ func TestSign(t *testing.T) {
p := a.config.AuthorityConfig.Provisioners[1].(*provisioner.JWK)
key, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
assert.FatalError(t, err)
token, err := generateToken("smallstep test", "step-cli", "https://test.ca.smallstep.com/sign", []string{"test.smallstep.com"}, time.Now(), key)
token, err := generateToken("smallstep test", "step-cli", testAudiences.Sign[0], []string{"test.smallstep.com"}, time.Now(), key)
assert.FatalError(t, err)
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod)
extraOpts, err := a.Authorize(ctx, token)
@ -113,7 +113,8 @@ func TestSign(t *testing.T) {
csr *x509.CertificateRequest
signOpts provisioner.Options
extraOpts []provisioner.SignOption
err *apiError
err error
code int
}
tests := map[string]func(*testing.T) *signTest{
"fail invalid signature": func(t *testing.T) *signTest {
@ -124,10 +125,8 @@ func TestSign(t *testing.T) {
csr: csr,
extraOpts: extraOpts,
signOpts: signOpts,
err: &apiError{errors.New("sign: invalid certificate request"),
http.StatusBadRequest,
apiCtx{"csr": csr, "signOptions": signOpts},
},
err: errors.New("authority.Sign; invalid certificate request"),
code: http.StatusBadRequest,
}
},
"fail invalid extra option": func(t *testing.T) *signTest {
@ -138,10 +137,8 @@ func TestSign(t *testing.T) {
csr: csr,
extraOpts: append(extraOpts, "42"),
signOpts: signOpts,
err: &apiError{errors.New("sign: invalid extra option type string"),
http.StatusInternalServerError,
apiCtx{"csr": csr, "signOptions": signOpts},
},
err: errors.New("authority.Sign; invalid extra option type string"),
code: http.StatusInternalServerError,
}
},
"fail merge default ASN1DN": func(t *testing.T) *signTest {
@ -153,10 +150,8 @@ func TestSign(t *testing.T) {
csr: csr,
extraOpts: extraOpts,
signOpts: signOpts,
err: &apiError{errors.New("sign: default ASN1DN template cannot be nil"),
http.StatusInternalServerError,
apiCtx{"csr": csr, "signOptions": signOpts},
},
err: errors.New("authority.Sign: default ASN1DN template cannot be nil"),
code: http.StatusInternalServerError,
}
},
"fail create cert": func(t *testing.T) *signTest {
@ -168,10 +163,8 @@ func TestSign(t *testing.T) {
csr: csr,
extraOpts: extraOpts,
signOpts: signOpts,
err: &apiError{errors.New("sign: error creating new leaf certificate"),
http.StatusInternalServerError,
apiCtx{"csr": csr, "signOptions": signOpts},
},
err: errors.New("authority.Sign; error creating new leaf certificate"),
code: http.StatusInternalServerError,
}
},
"fail provisioner duration claim": func(t *testing.T) *signTest {
@ -185,10 +178,8 @@ func TestSign(t *testing.T) {
csr: csr,
extraOpts: extraOpts,
signOpts: _signOpts,
err: &apiError{errors.New("sign: requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h0m0s"),
http.StatusUnauthorized,
apiCtx{"csr": csr, "signOptions": _signOpts},
},
err: errors.New("authority.Sign: requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h0m0s"),
code: http.StatusUnauthorized,
}
},
"fail validate sans when adding common name not in claims": func(t *testing.T) *signTest {
@ -200,10 +191,8 @@ func TestSign(t *testing.T) {
csr: csr,
extraOpts: extraOpts,
signOpts: signOpts,
err: &apiError{errors.New("sign: certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]"),
http.StatusUnauthorized,
apiCtx{"csr": csr, "signOptions": signOpts},
},
err: errors.New("authority.Sign: certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]"),
code: http.StatusUnauthorized,
}
},
"fail rsa key too short": func(t *testing.T) *signTest {
@ -228,20 +217,16 @@ ZYtQ9Ot36qc=
csr: csr,
extraOpts: extraOpts,
signOpts: signOpts,
err: &apiError{errors.New("sign: rsa key in CSR must be at least 2048 bits (256 bytes)"),
http.StatusUnauthorized,
apiCtx{"csr": csr, "signOptions": signOpts},
},
err: errors.New("authority.Sign: rsa key in CSR must be at least 2048 bits (256 bytes)"),
code: http.StatusUnauthorized,
}
},
"fail store cert in db": func(t *testing.T) *signTest {
csr := getCSR(t, priv)
_a := testAuthority(t)
_a.db = &MockAuthDB{
storeCertificate: func(crt *x509.Certificate) error {
return &apiError{errors.New("force"),
http.StatusInternalServerError,
apiCtx{"csr": csr, "signOptions": signOpts}}
_a.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
return errors.New("force")
},
}
return &signTest{
@ -249,17 +234,15 @@ ZYtQ9Ot36qc=
csr: csr,
extraOpts: extraOpts,
signOpts: signOpts,
err: &apiError{errors.New("sign: error storing certificate in db: force"),
http.StatusInternalServerError,
apiCtx{"csr": csr, "signOptions": signOpts},
},
err: errors.New("authority.Sign; error storing certificate in db: force"),
code: http.StatusInternalServerError,
}
},
"ok": func(t *testing.T) *signTest {
csr := getCSR(t, priv)
_a := testAuthority(t)
_a.db = &MockAuthDB{
storeCertificate: func(crt *x509.Certificate) error {
_a.db = &db.MockAuthDB{
MStoreCertificate: func(crt *x509.Certificate) error {
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
return nil
},
@ -279,15 +262,17 @@ ZYtQ9Ot36qc=
certChain, err := tc.auth.Sign(tc.csr, tc.signOpts, tc.extraOpts...)
if err != nil {
if assert.NotNil(t, tc.err) {
switch v := err.(type) {
case *apiError:
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
assert.Equals(t, v.code, tc.err.code)
assert.Equals(t, v.context, tc.err.context)
default:
t.Errorf("unexpected error type: %T", v)
}
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
assert.Nil(t, certChain)
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
ctxErr, ok := err.(*errs.Error)
assert.Fatal(t, ok, "error is not of type *errs.Error")
assert.Equals(t, ctxErr.Details["csr"], tc.csr)
assert.Equals(t, ctxErr.Details["signOptions"], tc.signOpts)
}
} else {
leaf := certChain[0]
@ -346,7 +331,7 @@ ZYtQ9Ot36qc=
}
}
func TestRenew(t *testing.T) {
func TestAuthority_Renew(t *testing.T) {
pub, _, err := keys.GenerateDefaultKeyPair()
assert.FatalError(t, err)
@ -375,9 +360,9 @@ func TestRenew(t *testing.T) {
x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"),
withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID))
assert.FatalError(t, err)
crtBytes, err := leaf.CreateCertificate()
certBytes, err := leaf.CreateCertificate()
assert.FatalError(t, err)
crt, err := x509.ParseCertificate(crtBytes)
cert, err := x509.ParseCertificate(certBytes)
assert.FatalError(t, err)
leafNoRenew, err := x509util.NewLeafProfile("norenew", a.intermediateIdentity.Crt,
@ -388,15 +373,16 @@ func TestRenew(t *testing.T) {
withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),
)
assert.FatalError(t, err)
crtBytesNoRenew, err := leafNoRenew.CreateCertificate()
certBytesNoRenew, err := leafNoRenew.CreateCertificate()
assert.FatalError(t, err)
crtNoRenew, err := x509.ParseCertificate(crtBytesNoRenew)
certNoRenew, err := x509.ParseCertificate(certBytesNoRenew)
assert.FatalError(t, err)
type renewTest struct {
auth *Authority
crt *x509.Certificate
err *apiError
cert *x509.Certificate
err error
code int
}
tests := map[string]func() (*renewTest, error){
"fail-create-cert": func() (*renewTest, error) {
@ -404,25 +390,22 @@ func TestRenew(t *testing.T) {
_a.intermediateIdentity.Key = nil
return &renewTest{
auth: _a,
crt: crt,
err: &apiError{errors.New("error renewing certificate from existing server certificate"),
http.StatusInternalServerError, apiCtx{}},
cert: cert,
err: errors.New("authority.Renew; error renewing certificate from existing server certificate"),
code: http.StatusInternalServerError,
}, nil
},
"fail-unauthorized": func() (*renewTest, error) {
ctx := map[string]interface{}{
"serialNumber": crtNoRenew.SerialNumber.String(),
}
return &renewTest{
crt: crtNoRenew,
err: &apiError{errors.New("renew: renew is disabled for provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"),
http.StatusUnauthorized, ctx},
cert: certNoRenew,
err: errors.New("authority.Renew: authority.authorizeRenew: jwk.AuthorizeRenew; renew is disabled for jwk provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk"),
code: http.StatusUnauthorized,
}, nil
},
"success": func() (*renewTest, error) {
return &renewTest{
auth: a,
crt: crt,
cert: cert,
}, nil
},
"success-new-intermediate": func() (*renewTest, error) {
@ -430,23 +413,23 @@ func TestRenew(t *testing.T) {
assert.FatalError(t, err)
newRootBytes, err := newRootProfile.CreateCertificate()
assert.FatalError(t, err)
newRootCrt, err := x509.ParseCertificate(newRootBytes)
newRootCert, err := x509.ParseCertificate(newRootBytes)
assert.FatalError(t, err)
newIntermediateProfile, err := x509util.NewIntermediateProfile("new-intermediate",
newRootCrt, newRootProfile.SubjectPrivateKey())
newRootCert, newRootProfile.SubjectPrivateKey())
assert.FatalError(t, err)
newIntermediateBytes, err := newIntermediateProfile.CreateCertificate()
assert.FatalError(t, err)
newIntermediateCrt, err := x509.ParseCertificate(newIntermediateBytes)
newIntermediateCert, err := x509.ParseCertificate(newIntermediateBytes)
assert.FatalError(t, err)
_a := testAuthority(t)
_a.intermediateIdentity.Key = newIntermediateProfile.SubjectPrivateKey()
_a.intermediateIdentity.Crt = newIntermediateCrt
_a.intermediateIdentity.Crt = newIntermediateCert
return &renewTest{
auth: _a,
crt: crt,
cert: cert,
}, nil
},
}
@ -458,32 +441,33 @@ func TestRenew(t *testing.T) {
var certChain []*x509.Certificate
if tc.auth != nil {
certChain, err = tc.auth.Renew(tc.crt)
certChain, err = tc.auth.Renew(tc.cert)
} else {
certChain, err = a.Renew(tc.crt)
certChain, err = a.Renew(tc.cert)
}
if err != nil {
if assert.NotNil(t, tc.err) {
switch v := err.(type) {
case *apiError:
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
assert.Equals(t, v.code, tc.err.code)
assert.Equals(t, v.context, tc.err.context)
default:
t.Errorf("unexpected error type: %T", v)
}
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
assert.Nil(t, certChain)
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
ctxErr, ok := err.(*errs.Error)
assert.Fatal(t, ok, "error is not of type *errs.Error")
assert.Equals(t, ctxErr.Details["serialNumber"], tc.cert.SerialNumber.String())
}
} else {
leaf := certChain[0]
intermediate := certChain[1]
if assert.Nil(t, tc.err) {
assert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.crt.NotAfter.Sub(crt.NotBefore))
assert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), tc.cert.NotAfter.Sub(cert.NotBefore))
assert.True(t, leaf.NotBefore.After(now.Add(-time.Minute)))
assert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute)))
assert.True(t, leaf.NotBefore.Before(now.Add(time.Minute)))
expiry := now.Add(time.Minute * 7)
assert.True(t, leaf.NotAfter.After(expiry.Add(-time.Minute)))
assert.True(t, leaf.NotAfter.After(expiry.Add(-2*time.Minute)))
assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Minute)))
tmplt := a.config.AuthorityConfig.Template
@ -513,7 +497,7 @@ func TestRenew(t *testing.T) {
if a.intermediateIdentity.Crt.SerialNumber == tc.auth.intermediateIdentity.Crt.SerialNumber {
assert.Equals(t, leaf.AuthorityKeyId, a.intermediateIdentity.Crt.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.crt.Extensions {
for _, ext1 := range tc.cert.Extensions {
found := false
for _, ext2 := range leaf.Extensions {
if reflect.DeepEqual(ext1, ext2) {
@ -529,7 +513,7 @@ func TestRenew(t *testing.T) {
// We did change the intermediate before renewing.
assert.Equals(t, leaf.AuthorityKeyId, tc.auth.intermediateIdentity.Crt.SubjectKeyId)
// Compare extensions: they can be in a different order
for _, ext1 := range tc.crt.Extensions {
for _, ext1 := range tc.cert.Extensions {
// The authority key id extension should be different b/c the intermediates are different.
if ext1.Id.Equal(oidAuthorityKeyIdentifier) {
for _, ext2 := range leaf.Extensions {
@ -560,7 +544,7 @@ func TestRenew(t *testing.T) {
}
}
func TestGetTLSOptions(t *testing.T) {
func TestAuthority_GetTLSOptions(t *testing.T) {
type renewTest struct {
auth *Authority
opts *tlsutil.TLSOptions
@ -596,21 +580,12 @@ func TestGetTLSOptions(t *testing.T) {
}
}
func TestRevoke(t *testing.T) {
func TestAuthority_Revoke(t *testing.T) {
reasonCode := 2
reason := "bob was let go"
validIssuer := "step-cli"
validAudience := []string{"https://test.ca.smallstep.com/revoke"}
validAudience := testAudiences.Revoke
now := time.Now().UTC()
getCtx := func() map[string]interface{} {
return apiCtx{
"serialNumber": "sn",
"reasonCode": reasonCode,
"reason": reason,
"mTLS": false,
"passiveOnly": false,
}
}
jwk, err := jose.ParseKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
assert.FatalError(t, err)
@ -619,30 +594,30 @@ func TestRevoke(t *testing.T) {
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
assert.FatalError(t, err)
a := testAuthority(t)
type test struct {
a *Authority
opts *RevokeOptions
err *apiError
auth *Authority
opts *RevokeOptions
err error
code int
checkErrDetails func(err *errs.Error)
}
tests := map[string]func() test{
"error/token/authorizeRevoke error": func() test {
a := testAuthority(t)
ctx := getCtx()
ctx["ott"] = "foo"
"fail/token/authorizeRevoke error": func() test {
return test{
a: a,
auth: a,
opts: &RevokeOptions{
OTT: "foo",
Serial: "sn",
ReasonCode: reasonCode,
Reason: reason,
},
err: &apiError{errors.New("revoke: authorizeRevoke: authorizeToken: error parsing token"),
http.StatusUnauthorized, ctx},
err: errors.New("authority.Revoke; error parsing token"),
code: http.StatusUnauthorized,
}
},
"error/nil-db": func() test {
a := testAuthority(t)
"fail/nil-db": func() test {
cl := jwt.Claims{
Subject: "sn",
Issuer: validIssuer,
@ -654,30 +629,30 @@ func TestRevoke(t *testing.T) {
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
ctx := getCtx()
ctx["ott"] = raw
ctx["tokenID"] = "44"
ctx["provisionerID"] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc"
return test{
a: a,
auth: a,
opts: &RevokeOptions{
Serial: "sn",
ReasonCode: reasonCode,
Reason: reason,
OTT: raw,
},
err: &apiError{errors.New("revoke: no persistence layer configured"),
http.StatusNotImplemented, ctx},
err: errors.New("authority.Revoke; no persistence layer configured"),
code: http.StatusNotImplemented,
checkErrDetails: func(err *errs.Error) {
assert.Equals(t, err.Details["token"], raw)
assert.Equals(t, err.Details["tokenID"], "44")
assert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
},
}
},
"error/db-revoke": func() test {
a := testAuthority(t)
a.db = &MockAuthDB{
useToken: func(id, tok string) (bool, error) {
"fail/db-revoke": func() test {
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{
MUseToken: func(id, tok string) (bool, error) {
return true, nil
},
err: errors.New("force"),
}
Err: errors.New("force"),
}))
cl := jwt.Claims{
Subject: "sn",
@ -690,30 +665,30 @@ func TestRevoke(t *testing.T) {
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
ctx := getCtx()
ctx["ott"] = raw
ctx["tokenID"] = "44"
ctx["provisionerID"] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc"
return test{
a: a,
auth: _a,
opts: &RevokeOptions{
Serial: "sn",
ReasonCode: reasonCode,
Reason: reason,
OTT: raw,
},
err: &apiError{errors.New("force"),
http.StatusInternalServerError, ctx},
err: errors.New("authority.Revoke: force"),
code: http.StatusInternalServerError,
checkErrDetails: func(err *errs.Error) {
assert.Equals(t, err.Details["token"], raw)
assert.Equals(t, err.Details["tokenID"], "44")
assert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
},
}
},
"error/already-revoked": func() test {
a := testAuthority(t)
a.db = &MockAuthDB{
useToken: func(id, tok string) (bool, error) {
"fail/already-revoked": func() test {
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{
MUseToken: func(id, tok string) (bool, error) {
return true, nil
},
err: db.ErrAlreadyExists,
}
Err: db.ErrAlreadyExists,
}))
cl := jwt.Claims{
Subject: "sn",
@ -726,29 +701,29 @@ func TestRevoke(t *testing.T) {
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
ctx := getCtx()
ctx["ott"] = raw
ctx["tokenID"] = "44"
ctx["provisionerID"] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc"
return test{
a: a,
auth: _a,
opts: &RevokeOptions{
Serial: "sn",
ReasonCode: reasonCode,
Reason: reason,
OTT: raw,
},
err: &apiError{errors.New("revoke: certificate with serial number sn has already been revoked"),
http.StatusBadRequest, ctx},
err: errors.New("authority.Revoke; certificate with serial number sn has already been revoked"),
code: http.StatusBadRequest,
checkErrDetails: func(err *errs.Error) {
assert.Equals(t, err.Details["token"], raw)
assert.Equals(t, err.Details["tokenID"], "44")
assert.Equals(t, err.Details["provisionerID"], "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
},
}
},
"ok/token": func() test {
a := testAuthority(t)
a.db = &MockAuthDB{
useToken: func(id, tok string) (bool, error) {
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{
MUseToken: func(id, tok string) (bool, error) {
return true, nil
},
}
}))
cl := jwt.Claims{
Subject: "sn",
@ -761,7 +736,7 @@ func TestRevoke(t *testing.T) {
raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
assert.FatalError(t, err)
return test{
a: a,
auth: _a,
opts: &RevokeOptions{
Serial: "sn",
ReasonCode: reasonCode,
@ -770,39 +745,14 @@ func TestRevoke(t *testing.T) {
},
}
},
"error/mTLS/authorizeRevoke": func() test {
a := testAuthority(t)
a.db = &MockAuthDB{}
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
assert.FatalError(t, err)
ctx := getCtx()
ctx["certificate"] = base64.StdEncoding.EncodeToString(crt.Raw)
ctx["mTLS"] = true
return test{
a: a,
opts: &RevokeOptions{
Crt: crt,
Serial: "sn",
ReasonCode: reasonCode,
Reason: reason,
MTLS: true,
},
err: &apiError{errors.New("revoke: authorizeRevoke: serial number in certificate different than body"),
http.StatusUnauthorized, ctx},
}
},
"ok/mTLS": func() test {
a := testAuthority(t)
a.db = &MockAuthDB{}
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
assert.FatalError(t, err)
return test{
a: a,
auth: _a,
opts: &RevokeOptions{
Crt: crt,
Serial: "102012593071130646873265215610956555026",
@ -816,15 +766,24 @@ func TestRevoke(t *testing.T) {
for name, f := range tests {
tc := f()
t.Run(name, func(t *testing.T) {
if err := tc.a.Revoke(context.TODO(), tc.opts); err != nil {
if assert.NotNil(t, tc.err) {
switch v := err.(type) {
case *apiError:
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
assert.Equals(t, v.code, tc.err.code)
assert.Equals(t, v.context, tc.err.context)
default:
t.Errorf("unexpected error type: %T", v)
ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)
if err := tc.auth.Revoke(ctx, tc.opts); err != nil {
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tc.code)
assert.HasPrefix(t, err.Error(), tc.err.Error())
ctxErr, ok := err.(*errs.Error)
assert.Fatal(t, ok, "error is not of type *errs.Error")
assert.Equals(t, ctxErr.Details["serialNumber"], tc.opts.Serial)
assert.Equals(t, ctxErr.Details["reasonCode"], tc.opts.ReasonCode)
assert.Equals(t, ctxErr.Details["reason"], tc.opts.Reason)
assert.Equals(t, ctxErr.Details["MTLS"], tc.opts.MTLS)
assert.Equals(t, ctxErr.Details["context"], string(provisioner.RevokeMethod))
if tc.checkErrDetails != nil {
tc.checkErrDetails(ctxErr)
}
}
} else {

View file

@ -22,6 +22,7 @@ import (
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/keys"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/randutil"
@ -102,7 +103,7 @@ func TestCASign(t *testing.T) {
ca: ca,
body: "invalid json",
status: http.StatusBadRequest,
errMsg: "Bad Request",
errMsg: errs.BadRequestDefaultMsg,
}
},
"fail invalid-csr-sig": func(t *testing.T) *signTest {
@ -140,7 +141,7 @@ ZEp7knvU2psWRw==
ca: ca,
body: string(body),
status: http.StatusBadRequest,
errMsg: "Bad Request",
errMsg: errs.BadRequestDefaultMsg,
}
},
"fail unauthorized-ott": func(t *testing.T) *signTest {
@ -155,7 +156,7 @@ ZEp7knvU2psWRw==
ca: ca,
body: string(body),
status: http.StatusUnauthorized,
errMsg: "Unauthorized",
errMsg: errs.UnauthorizedDefaultMsg,
}
},
"fail commonname-claim": func(t *testing.T) *signTest {
@ -188,7 +189,7 @@ ZEp7knvU2psWRw==
ca: ca,
body: string(body),
status: http.StatusUnauthorized,
errMsg: "Unauthorized",
errMsg: errs.UnauthorizedDefaultMsg,
}
},
"ok": func(t *testing.T) *signTest {
@ -392,7 +393,7 @@ func TestCAProvisionerEncryptedKey(t *testing.T) {
ca: ca,
kid: "foo",
status: http.StatusNotFound,
errMsg: "Not Found",
errMsg: errs.NotFoundDefaultMsg,
}
},
"ok": func(t *testing.T) *ekt {
@ -455,7 +456,7 @@ func TestCARoot(t *testing.T) {
ca: ca,
sha: "foo",
status: http.StatusNotFound,
errMsg: "Not Found",
errMsg: errs.NotFoundDefaultMsg,
}
},
"success": func(t *testing.T) *rootTest {
@ -575,7 +576,7 @@ func TestCARenew(t *testing.T) {
ca: ca,
tlsConnState: nil,
status: http.StatusBadRequest,
errMsg: "Bad Request",
errMsg: errs.BadRequestDefaultMsg,
}
},
"request-missing-peer-certificate": func(t *testing.T) *renewTest {
@ -583,7 +584,7 @@ func TestCARenew(t *testing.T) {
ca: ca,
tlsConnState: &tls.ConnectionState{PeerCertificates: []*x509.Certificate{}},
status: http.StatusBadRequest,
errMsg: "Bad Request",
errMsg: errs.BadRequestDefaultMsg,
}
},
"success": func(t *testing.T) *renewTest {

View file

@ -486,7 +486,7 @@ func (c *Client) Version() (*api.VersionResponse, error) {
retry:
resp, err := c.client.Get(u.String())
if err != nil {
return nil, errors.Wrapf(err, "client GET %s failed", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Version; client GET %s failed", u)
}
if resp.StatusCode >= 400 {
if !retried && c.retryOnError(resp) {
@ -497,7 +497,7 @@ retry:
}
var version api.VersionResponse
if err := readJSON(resp.Body, &version); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Version; error reading %s", u)
}
return &version, nil
}
@ -510,7 +510,7 @@ func (c *Client) Health() (*api.HealthResponse, error) {
retry:
resp, err := c.client.Get(u.String())
if err != nil {
return nil, errors.Wrapf(err, "client GET %s failed", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Health; client GET %s failed", u)
}
if resp.StatusCode >= 400 {
if !retried && c.retryOnError(resp) {
@ -521,7 +521,7 @@ retry:
}
var health api.HealthResponse
if err := readJSON(resp.Body, &health); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Health; error reading %s", u)
}
return &health, nil
}
@ -537,7 +537,7 @@ func (c *Client) Root(sha256Sum string) (*api.RootResponse, error) {
retry:
resp, err := newInsecureClient().Get(u.String())
if err != nil {
return nil, errors.Wrapf(err, "client GET %s failed", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Root; client GET %s failed", u)
}
if resp.StatusCode >= 400 {
if !retried && c.retryOnError(resp) {
@ -548,12 +548,12 @@ retry:
}
var root api.RootResponse
if err := readJSON(resp.Body, &root); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Root; error reading %s", u)
}
// verify the sha256
sum := sha256.Sum256(root.RootPEM.Raw)
if sha256Sum != strings.ToLower(hex.EncodeToString(sum[:])) {
return nil, errors.New("root certificate SHA256 fingerprint do not match")
return nil, errs.BadRequest(errors.New("client.Root; root certificate SHA256 fingerprint do not match"))
}
return &root, nil
}
@ -564,13 +564,13 @@ func (c *Client) Sign(req *api.SignRequest) (*api.SignResponse, error) {
var retried bool
body, err := json.Marshal(req)
if err != nil {
return nil, errors.Wrap(err, "error marshaling request")
return nil, errs.Wrap(http.StatusInternalServerError, err, "client.Sign; error marshaling request")
}
u := c.endpoint.ResolveReference(&url.URL{Path: "/sign"})
retry:
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
if err != nil {
return nil, errors.Wrapf(err, "client POST %s failed", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Sign; client POST %s failed", u)
}
if resp.StatusCode >= 400 {
if !retried && c.retryOnError(resp) {
@ -581,7 +581,7 @@ retry:
}
var sign api.SignResponse
if err := readJSON(resp.Body, &sign); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Sign; error reading %s", u)
}
// Add tls.ConnectionState:
// We'll extract the root certificate from the verified chains
@ -598,7 +598,7 @@ func (c *Client) Renew(tr http.RoundTripper) (*api.SignResponse, error) {
retry:
resp, err := client.Post(u.String(), "application/json", http.NoBody)
if err != nil {
return nil, errors.Wrapf(err, "client POST %s failed", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Renew; client POST %s failed", u)
}
if resp.StatusCode >= 400 {
if !retried && c.retryOnError(resp) {
@ -609,7 +609,7 @@ retry:
}
var sign api.SignResponse
if err := readJSON(resp.Body, &sign); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
return nil, errs.Wrapf(http.StatusInternalServerError, err, "client.Renew; error reading %s", u)
}
return &sign, nil
}
@ -1008,13 +1008,13 @@ func (c *Client) SSHBastion(req *api.SSHBastionRequest) (*api.SSHBastionResponse
var retried bool
body, err := json.Marshal(req)
if err != nil {
return nil, errors.Wrap(err, "error marshaling request")
return nil, errors.Wrap(err, "client.SSHBastion; error marshaling request")
}
u := c.endpoint.ResolveReference(&url.URL{Path: "/ssh/bastion"})
retry:
resp, err := c.client.Post(u.String(), "application/json", bytes.NewReader(body))
if err != nil {
return nil, errors.Wrapf(err, "client POST %s failed", u)
return nil, errors.Wrapf(err, "client.SSHBastion; client POST %s failed", u)
}
if resp.StatusCode >= 400 {
if !retried && c.retryOnError(resp) {
@ -1025,7 +1025,7 @@ retry:
}
var bastion api.SSHBastionResponse
if err := readJSON(resp.Body, &bastion); err != nil {
return nil, errors.Wrapf(err, "error reading %s", u)
return nil, errors.Wrapf(err, "client.SSHBastion; error reading %s", u)
}
return &bastion, nil
}

View file

@ -16,12 +16,12 @@ import (
"testing"
"time"
"github.com/smallstep/certificates/errs"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/errs"
"github.com/smallstep/cli/crypto/x509util"
"golang.org/x/crypto/ssh"
)
@ -154,18 +154,17 @@ func equalJSON(t *testing.T, a interface{}, b interface{}) bool {
func TestClient_Version(t *testing.T) {
ok := &api.VersionResponse{Version: "test"}
internal := errs.InternalServerError(fmt.Errorf("Internal Server Error"))
notFound := errs.NotFound(fmt.Errorf("Not Found"))
tests := []struct {
name string
response interface{}
responseCode int
wantErr bool
expectedErr error
}{
{"ok", ok, 200, false},
{"500", internal, 500, true},
{"404", notFound, 404, true},
{"ok", ok, 200, false, nil},
{"500", errs.InternalServerError(errors.New("force")), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)},
{"404", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -185,7 +184,6 @@ func TestClient_Version(t *testing.T) {
got, err := c.Version()
if (err != nil) != tt.wantErr {
fmt.Printf("%+v", err)
t.Errorf("Client.Version() error = %v, wantErr %v", err, tt.wantErr)
return
}
@ -195,9 +193,7 @@ func TestClient_Version(t *testing.T) {
if got != nil {
t.Errorf("Client.Version() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.Version() error = %v, want %v", err, tt.response)
}
assert.HasPrefix(t, tt.expectedErr.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Version() = %v, want %v", got, tt.response)
@ -209,16 +205,16 @@ func TestClient_Version(t *testing.T) {
func TestClient_Health(t *testing.T) {
ok := &api.HealthResponse{Status: "ok"}
nok := errs.InternalServerError(fmt.Errorf("Internal Server Error"))
tests := []struct {
name string
response interface{}
responseCode int
wantErr bool
expectedErr error
}{
{"ok", ok, 200, false},
{"not ok", nok, 500, true},
{"ok", ok, 200, false, nil},
{"not ok", errs.InternalServerError(errors.New("force")), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -248,9 +244,7 @@ func TestClient_Health(t *testing.T) {
if got != nil {
t.Errorf("Client.Health() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.Health() error = %v, want %v", err, tt.response)
}
assert.HasPrefix(t, tt.expectedErr.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Health() = %v, want %v", got, tt.response)
@ -264,7 +258,6 @@ func TestClient_Root(t *testing.T) {
ok := &api.RootResponse{
RootPEM: api.Certificate{Certificate: parseCertificate(rootPEM)},
}
notFound := errs.NotFound(fmt.Errorf("Not Found"))
tests := []struct {
name string
@ -272,9 +265,10 @@ func TestClient_Root(t *testing.T) {
response interface{}
responseCode int
wantErr bool
expectedErr error
}{
{"ok", "a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d", ok, 200, false},
{"not found", "invalid", notFound, 404, true},
{"ok", "a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d", ok, 200, false, nil},
{"not found", "invalid", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -307,9 +301,7 @@ func TestClient_Root(t *testing.T) {
if got != nil {
t.Errorf("Client.Root() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.Root() error = %v, want %v", err, tt.response)
}
assert.HasPrefix(t, tt.expectedErr.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Root() = %v, want %v", got, tt.response)
@ -334,8 +326,6 @@ func TestClient_Sign(t *testing.T) {
NotBefore: api.NewTimeDuration(time.Now()),
NotAfter: api.NewTimeDuration(time.Now().AddDate(0, 1, 0)),
}
unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized"))
badRequest := errs.BadRequest(fmt.Errorf("Bad Request"))
tests := []struct {
name string
@ -343,11 +333,12 @@ func TestClient_Sign(t *testing.T) {
response interface{}
responseCode int
wantErr bool
expectedErr error
}{
{"ok", request, ok, 200, false},
{"unauthorized", request, unauthorized, 401, true},
{"empty request", &api.SignRequest{}, badRequest, 403, true},
{"nil request", nil, badRequest, 403, true},
{"ok", request, ok, 200, false, nil},
{"unauthorized", request, errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},
{"empty request", &api.SignRequest{}, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)},
{"nil request", nil, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -364,7 +355,9 @@ func TestClient_Sign(t *testing.T) {
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
body := new(api.SignRequest)
if err := api.ReadJSON(req.Body, body); err != nil {
api.WriteError(w, badRequest)
e, ok := tt.response.(error)
assert.Fatal(t, ok, "response expected to be error type")
api.WriteError(w, e)
return
} else if !equalJSON(t, body, tt.request) {
if tt.request == nil {
@ -390,9 +383,7 @@ func TestClient_Sign(t *testing.T) {
if got != nil {
t.Errorf("Client.Sign() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.Sign() error = %v, want %v", err, tt.response)
}
assert.HasPrefix(t, tt.expectedErr.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Sign() = %v, want %v", got, tt.response)
@ -409,19 +400,17 @@ func TestClient_Revoke(t *testing.T) {
OTT: "the-ott",
ReasonCode: 4,
}
unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized"))
badRequest := errs.BadRequest(fmt.Errorf("Bad Request"))
tests := []struct {
name string
request *api.RevokeRequest
response interface{}
responseCode int
wantErr bool
expectedErr error
}{
{"ok", request, ok, 200, false},
{"unauthorized", request, unauthorized, 401, true},
{"nil request", nil, badRequest, 403, true},
{"ok", request, ok, 200, false, nil},
{"unauthorized", request, errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},
{"nil request", nil, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -438,7 +427,9 @@ func TestClient_Revoke(t *testing.T) {
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
body := new(api.RevokeRequest)
if err := api.ReadJSON(req.Body, body); err != nil {
api.WriteError(w, badRequest)
e, ok := tt.response.(error)
assert.Fatal(t, ok, "response expected to be error type")
api.WriteError(w, e)
return
} else if !equalJSON(t, body, tt.request) {
if tt.request == nil {
@ -464,9 +455,7 @@ func TestClient_Revoke(t *testing.T) {
if got != nil {
t.Errorf("Client.Revoke() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.Revoke() error = %v, want %v", err, tt.response)
}
assert.HasPrefix(t, tt.expectedErr.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Revoke() = %v, want %v", got, tt.response)
@ -485,19 +474,18 @@ func TestClient_Renew(t *testing.T) {
{Certificate: parseCertificate(rootPEM)},
},
}
unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized"))
badRequest := errs.BadRequest(fmt.Errorf("Bad Request"))
tests := []struct {
name string
response interface{}
responseCode int
wantErr bool
err error
}{
{"ok", ok, 200, false},
{"unauthorized", unauthorized, 401, true},
{"empty request", badRequest, 403, true},
{"nil request", badRequest, 403, true},
{"ok", ok, 200, false, nil},
{"unauthorized", errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},
{"empty request", errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)},
{"nil request", errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -527,9 +515,11 @@ func TestClient_Renew(t *testing.T) {
if got != nil {
t.Errorf("Client.Renew() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.Renew() error = %v, want %v", err, tt.response)
}
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.responseCode)
assert.HasPrefix(t, tt.err.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Renew() = %v, want %v", got, tt.response)
@ -589,9 +579,7 @@ func TestClient_Provisioners(t *testing.T) {
if got != nil {
t.Errorf("Client.Provisioners() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.Provisioners() error = %v, want %v", err, tt.response)
}
assert.HasPrefix(t, errs.InternalServerErrorDefaultMsg, err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Provisioners() = %v, want %v", got, tt.response)
@ -605,7 +593,6 @@ func TestClient_ProvisionerKey(t *testing.T) {
ok := &api.ProvisionerKeyResponse{
Key: "an encrypted key",
}
notFound := errs.NotFound(fmt.Errorf("Not Found"))
tests := []struct {
name string
@ -613,9 +600,10 @@ func TestClient_ProvisionerKey(t *testing.T) {
response interface{}
responseCode int
wantErr bool
err error
}{
{"ok", "kid", ok, 200, false},
{"fail", "invalid", notFound, 500, true},
{"ok", "kid", ok, 200, false, nil},
{"fail", "invalid", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -648,9 +636,11 @@ func TestClient_ProvisionerKey(t *testing.T) {
if got != nil {
t.Errorf("Client.ProvisionerKey() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.ProvisionerKey() error = %v, want %v", err, tt.response)
}
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.responseCode)
assert.HasPrefix(t, tt.err.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.ProvisionerKey() = %v, want %v", got, tt.response)
@ -666,19 +656,17 @@ func TestClient_Roots(t *testing.T) {
{Certificate: parseCertificate(rootPEM)},
},
}
unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized"))
badRequest := errs.BadRequest(fmt.Errorf("Bad Request"))
tests := []struct {
name string
response interface{}
responseCode int
wantErr bool
err error
}{
{"ok", ok, 200, false},
{"unauthorized", unauthorized, 401, true},
{"empty request", badRequest, 403, true},
{"nil request", badRequest, 403, true},
{"ok", ok, 200, false, nil},
{"unauthorized", errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},
{"bad-request", errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -708,9 +696,10 @@ func TestClient_Roots(t *testing.T) {
if got != nil {
t.Errorf("Client.Roots() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.Roots() error = %v, want %v", err, tt.response)
}
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.responseCode)
assert.HasPrefix(t, tt.err.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Roots() = %v, want %v", got, tt.response)
@ -726,19 +715,16 @@ func TestClient_Federation(t *testing.T) {
{Certificate: parseCertificate(rootPEM)},
},
}
unauthorized := errs.Unauthorized(fmt.Errorf("Unauthorized"))
badRequest := errs.BadRequest(fmt.Errorf("Bad Request"))
tests := []struct {
name string
response interface{}
responseCode int
wantErr bool
err error
}{
{"ok", ok, 200, false},
{"unauthorized", unauthorized, 401, true},
{"empty request", badRequest, 403, true},
{"nil request", badRequest, 403, true},
{"ok", ok, 200, false, nil},
{"unauthorized", errs.Unauthorized(errors.New("force")), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -768,9 +754,10 @@ func TestClient_Federation(t *testing.T) {
if got != nil {
t.Errorf("Client.Federation() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.Federation() error = %v, want %v", err, tt.response)
}
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.responseCode)
assert.HasPrefix(t, tt.err.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.Federation() = %v, want %v", got, tt.response)
@ -790,16 +777,16 @@ func TestClient_SSHRoots(t *testing.T) {
HostKeys: []api.SSHPublicKey{{PublicKey: key}},
UserKeys: []api.SSHPublicKey{{PublicKey: key}},
}
notFound := errs.NotFound(fmt.Errorf("Not Found"))
tests := []struct {
name string
response interface{}
responseCode int
wantErr bool
err error
}{
{"ok", ok, 200, false},
{"not found", notFound, 404, true},
{"ok", ok, 200, false, nil},
{"not found", errs.NotFound(errors.New("force")), 404, true, errors.New(errs.NotFoundDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -829,9 +816,10 @@ func TestClient_SSHRoots(t *testing.T) {
if got != nil {
t.Errorf("Client.SSHKeys() = %v, want nil", got)
}
if !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.SSHKeys() error = %v, want %v", err, tt.response)
}
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.responseCode)
assert.HasPrefix(t, tt.err.Error(), err.Error())
default:
if !reflect.DeepEqual(got, tt.response) {
t.Errorf("Client.SSHKeys() = %v, want %v", got, tt.response)
@ -948,7 +936,6 @@ func TestClient_SSHBastion(t *testing.T) {
Hostname: "bastion.local",
},
}
badRequest := errs.BadRequest(fmt.Errorf("Bad Request"))
tests := []struct {
name string
@ -956,11 +943,11 @@ func TestClient_SSHBastion(t *testing.T) {
response interface{}
responseCode int
wantErr bool
err error
}{
{"ok", &api.SSHBastionRequest{Hostname: "host.local"}, ok, 200, false},
{"bad response", &api.SSHBastionRequest{Hostname: "host.local"}, "bad json", 200, true},
{"empty request", &api.SSHBastionRequest{}, badRequest, 403, true},
{"nil request", nil, badRequest, 403, true},
{"ok", &api.SSHBastionRequest{Hostname: "host.local"}, ok, 200, false, nil},
{"bad-response", &api.SSHBastionRequest{Hostname: "host.local"}, "bad json", 200, true, nil},
{"bad-request", &api.SSHBastionRequest{}, errs.BadRequest(errors.New("force")), 400, true, errors.New(errs.BadRequestDefaultMsg)},
}
srv := httptest.NewServer(nil)
@ -990,8 +977,11 @@ func TestClient_SSHBastion(t *testing.T) {
if got != nil {
t.Errorf("Client.SSHBastion() = %v, want nil", got)
}
if tt.responseCode != 200 && !reflect.DeepEqual(err, tt.response) {
t.Errorf("Client.SSHBastion() error = %v, want %v", err, tt.response)
if tt.responseCode != 200 {
sc, ok := err.(errs.StatusCoder)
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
assert.Equals(t, sc.StatusCode(), tt.responseCode)
assert.HasPrefix(t, tt.err.Error(), err.Error())
}
default:
if !reflect.DeepEqual(got, tt.response) {

View file

@ -276,6 +276,7 @@ func TestIdentity_Renew(t *testing.T) {
}
oldIdentityDir := identityDir
identityDir = "testdata/identity"
defer func() {
identityDir = oldIdentityDir
os.RemoveAll(tmpDir)

View file

@ -270,6 +270,105 @@ func (db *DB) Shutdown() error {
return nil
}
// MockAuthDB mocks the AuthDB interface. //
type MockAuthDB struct {
Err error
Ret1 interface{}
MIsRevoked func(string) (bool, error)
MIsSSHRevoked func(string) (bool, error)
MRevoke func(rci *RevokedCertificateInfo) error
MRevokeSSH func(rci *RevokedCertificateInfo) error
MStoreCertificate func(crt *x509.Certificate) error
MUseToken func(id, tok string) (bool, error)
MIsSSHHost func(principal string) (bool, error)
MStoreSSHCertificate func(crt *ssh.Certificate) error
MGetSSHHostPrincipals func() ([]string, error)
MShutdown func() error
}
// IsRevoked mock.
func (m *MockAuthDB) IsRevoked(sn string) (bool, error) {
if m.MIsRevoked != nil {
return m.MIsRevoked(sn)
}
return m.Ret1.(bool), m.Err
}
// IsSSHRevoked mock.
func (m *MockAuthDB) IsSSHRevoked(sn string) (bool, error) {
if m.MIsSSHRevoked != nil {
return m.MIsSSHRevoked(sn)
}
return m.Ret1.(bool), m.Err
}
// UseToken mock.
func (m *MockAuthDB) UseToken(id, tok string) (bool, error) {
if m.MUseToken != nil {
return m.MUseToken(id, tok)
}
if m.Ret1 == nil {
return false, m.Err
}
return m.Ret1.(bool), m.Err
}
// Revoke mock.
func (m *MockAuthDB) Revoke(rci *RevokedCertificateInfo) error {
if m.MRevoke != nil {
return m.MRevoke(rci)
}
return m.Err
}
// RevokeSSH mock.
func (m *MockAuthDB) RevokeSSH(rci *RevokedCertificateInfo) error {
if m.MRevokeSSH != nil {
return m.MRevokeSSH(rci)
}
return m.Err
}
// StoreCertificate mock.
func (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error {
if m.MStoreCertificate != nil {
return m.MStoreCertificate(crt)
}
return m.Err
}
// IsSSHHost mock.
func (m *MockAuthDB) IsSSHHost(principal string) (bool, error) {
if m.MIsSSHHost != nil {
return m.MIsSSHHost(principal)
}
return m.Ret1.(bool), m.Err
}
// StoreSSHCertificate mock.
func (m *MockAuthDB) StoreSSHCertificate(crt *ssh.Certificate) error {
if m.MStoreSSHCertificate != nil {
return m.MStoreSSHCertificate(crt)
}
return m.Err
}
// GetSSHHostPrincipals mock.
func (m *MockAuthDB) GetSSHHostPrincipals() ([]string, error) {
if m.MGetSSHHostPrincipals != nil {
return m.MGetSSHHostPrincipals()
}
return m.Ret1.([]string), m.Err
}
// Shutdown mock.
func (m *MockAuthDB) Shutdown() error {
if m.MShutdown != nil {
return m.MShutdown()
}
return m.Err
}
// MockNoSQLDB //
type MockNoSQLDB struct {
Err error

View file

@ -21,9 +21,9 @@ type StackTracer interface {
// Option modifies the Error type.
type Option func(e *Error) error
// WithMessage returns an Option that modifies the error by overwriting the
// withDefaultMessage returns an Option that modifies the error by overwriting the
// message only if it is empty.
func WithMessage(format string, args ...interface{}) Option {
func withDefaultMessage(format string, args ...interface{}) Option {
return func(e *Error) error {
if len(e.Msg) > 0 {
return e
@ -33,25 +33,52 @@ func WithMessage(format string, args ...interface{}) Option {
}
}
// WithMessage returns an Option that modifies the error by overwriting the
// message only if it is empty.
func WithMessage(format string, args ...interface{}) Option {
return func(e *Error) error {
e.Msg = fmt.Sprintf(format, args...)
return e
}
}
// WithKeyVal returns an Option that adds the given key-value pair to the
// Error details. This is helpful for debugging errors.
func WithKeyVal(key string, val interface{}) Option {
return func(e *Error) error {
if e.Details == nil {
e.Details = make(map[string]interface{})
}
e.Details[key] = val
return e
}
}
// Error represents the CA API errors.
type Error struct {
Status int
Err error
Msg string
Status int
Err error
Msg string
Details map[string]interface{}
}
// New returns a new Error. If the given error implements the StatusCoder
// interface we will ignore the given status.
func New(status int, err error, opts ...Option) error {
var e *Error
if sc, ok := err.(StatusCoder); ok {
e = &Error{Status: sc.StatusCode(), Err: err}
} else {
cause := errors.Cause(err)
if sc, ok := cause.(StatusCoder); ok {
var (
e *Error
ok bool
)
if e, ok = err.(*Error); !ok {
if sc, ok := err.(StatusCoder); ok {
e = &Error{Status: sc.StatusCode(), Err: err}
} else {
e = &Error{Status: status, Err: err}
cause := errors.Cause(err)
if sc, ok := cause.(StatusCoder); ok {
e = &Error{Status: sc.StatusCode(), Err: err}
} else {
e = &Error{Status: status, Err: err}
}
}
}
for _, o := range opts {
@ -188,63 +215,62 @@ func StatusCodeError(code int, e error, opts ...Option) error {
}
}
var seeLogs = "Please see the certificate authority logs for more info."
var (
seeLogs = "Please see the certificate authority logs for more info."
// BadRequestDefaultMsg 400 default msg
BadRequestDefaultMsg = "The request could not be completed due to being poorly formatted or missing critical data. " + seeLogs
// UnauthorizedDefaultMsg 401 default msg
UnauthorizedDefaultMsg = "The request lacked necessary authorization to be completed. " + seeLogs
// ForbiddenDefaultMsg 403 default msg
ForbiddenDefaultMsg = "The request was forbidden by the certificate authority. " + seeLogs
// NotFoundDefaultMsg 404 default msg
NotFoundDefaultMsg = "The requested resource could not be found. " + seeLogs
// InternalServerErrorDefaultMsg 500 default msg
InternalServerErrorDefaultMsg = "The certificate authority encountered an Internal Server Error. " + seeLogs
// NotImplementedDefaultMsg 501 default msg
NotImplementedDefaultMsg = "The requested method is not implemented by the certificate authority. " + seeLogs
)
// InternalServerError returns a 500 error with the given error.
func InternalServerError(err error, opts ...Option) error {
if len(opts) == 0 {
opts = append(opts, WithMessage("The certificate authority encountered an Internal Server Error. "+seeLogs))
}
opts = append(opts, withDefaultMessage(InternalServerErrorDefaultMsg))
return New(http.StatusInternalServerError, err, opts...)
}
// NotImplemented returns a 501 error with the given error.
func NotImplemented(err error, opts ...Option) error {
if len(opts) == 0 {
opts = append(opts, WithMessage("The requested method is not implemented by the certificate authority. "+seeLogs))
}
opts = append(opts, withDefaultMessage(NotImplementedDefaultMsg))
return New(http.StatusNotImplemented, err, opts...)
}
// BadRequest returns an 400 error with the given error.
func BadRequest(err error, opts ...Option) error {
if len(opts) == 0 {
opts = append(opts, WithMessage("The request could not be completed due to being poorly formatted or "+
"missing critical data. "+seeLogs))
}
opts = append(opts, withDefaultMessage(BadRequestDefaultMsg))
return New(http.StatusBadRequest, err, opts...)
}
// Unauthorized returns an 401 error with the given error.
func Unauthorized(err error, opts ...Option) error {
if len(opts) == 0 {
opts = append(opts, WithMessage("The request lacked necessary authorization to be completed. "+seeLogs))
}
opts = append(opts, withDefaultMessage(UnauthorizedDefaultMsg))
return New(http.StatusUnauthorized, err, opts...)
}
// Forbidden returns an 403 error with the given error.
func Forbidden(err error, opts ...Option) error {
if len(opts) == 0 {
opts = append(opts, WithMessage("The request was Forbidden by the certificate authority. "+seeLogs))
}
opts = append(opts, withDefaultMessage(ForbiddenDefaultMsg))
return New(http.StatusForbidden, err, opts...)
}
// NotFound returns an 404 error with the given error.
func NotFound(err error, opts ...Option) error {
if len(opts) == 0 {
opts = append(opts, WithMessage("The requested resource could not be found. "+seeLogs))
}
opts = append(opts, withDefaultMessage(NotFoundDefaultMsg))
return New(http.StatusNotFound, err, opts...)
}
// UnexpectedError will be used when the certificate authority makes an outgoing
// request and receives an unhandled status code.
func UnexpectedError(code int, err error, opts ...Option) error {
if len(opts) == 0 {
opts = append(opts, WithMessage("The certificate authority received an "+
"unexpected HTTP status code - '%d'. "+seeLogs, code))
}
opts = append(opts, withDefaultMessage("The certificate authority received an "+
"unexpected HTTP status code - '%d'. "+seeLogs, code))
return New(code, err, opts...)
}

View file

@ -1,11 +1,9 @@
package api
package errs
import (
"fmt"
"reflect"
"testing"
"github.com/smallstep/certificates/errs"
)
func TestError_MarshalJSON(t *testing.T) {
@ -24,7 +22,7 @@ func TestError_MarshalJSON(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := &errs.Error{
e := &Error{
Status: tt.fields.Status,
Err: tt.fields.Err,
}
@ -47,15 +45,15 @@ func TestError_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
args args
expected *errs.Error
expected *Error
wantErr bool
}{
{"ok", args{[]byte(`{"status":400,"message":"bad request"}`)}, &errs.Error{Status: 400, Err: fmt.Errorf("bad request")}, false},
{"fail", args{[]byte(`{"status":"400","message":"bad request"}`)}, &errs.Error{}, true},
{"ok", args{[]byte(`{"status":400,"message":"bad request"}`)}, &Error{Status: 400, Err: fmt.Errorf("bad request")}, false},
{"fail", args{[]byte(`{"status":"400","message":"bad request"}`)}, &Error{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := new(errs.Error)
e := new(Error)
if err := e.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
t.Errorf("Error.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
}