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.pull/166/head^2
parent
549291c2ca
commit
dccbdf3a90
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJj80EJXJR9vxefhdqOLSdzRzBw24t9YKPxb+eCYLf7BU50pJQnB/jK2ZM3qLFbieLaYjngZ86T4DzHxlPAnlAY=
|
@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ8einS88ZaWpcTZG27D5N9JDKfGv0rzjDByLGsZzMsLYl3XcsN9IWKXB6b+5GJ3UaoZf/pFxzRzIdDIh7Ypw3Y=
|
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIHzAUYu3h8e1gL5ONGZo+lghJJa9rl1TvP2UlqDXazxvoAoGCCqGSM49
|
||||
AwEHoUQDQgAEOLScS+1Yzmqdyots9lSC0tzTSXUXEgyOD9wYrQ0BqnVZtBXlQw1p
|
||||
m3fnF/7Ehl6bD1YZWjrF1t+IBZQMq1uBBw==
|
||||
-----END EC PRIVATE KEY-----
|
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEINWGD2xneE43YeytQzORItISxv6d/oH+9TXvDKHo6TyXoAoGCCqGSM49
|
||||
AwEHoUQDQgAEVK/EtXgVV7+7ppnQSjCtI5qb/gIGnQUF4i//F/JKKho7kRNyMDSn
|
||||
BP3kndiv8Yfxg4PsyIRY5ZofbEo5eJE6bg==
|
||||
-----END EC PRIVATE KEY-----
|
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIKZCgb5pTSSCbr/xcHCOkl9O6tQtZmNahr3Ap3/c2nBLoAoGCCqGSM49
|
||||
AwEHoUQDQgAEmPzQQlclH2/F5+F2o4tJ3NHMHDbi31go/Fv54Jgt/sFTnSklCcH+
|
||||
MrZkzeosVuJ4tpiOeBnzpPgPMfGU8CeUBg==
|
||||
-----END EC PRIVATE KEY-----
|
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIDuzykyPM6rLnSoyF4jnOpPAlyKZERqtaB8PTh179DMgoAoGCCqGSM49
|
||||
AwEHoUQDQgAEnx6KdLzxlpalxNkbbsPk30kMp8a/SvOMMHIsaxnMywtiXddyw30h
|
||||
YpcHpv7kYndRqhl/+kXHNHMh0MiHtinDdg==
|
||||
-----END EC PRIVATE KEY-----
|
@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJj80EJXJR9vxefhdqOLSdzRzBw24t9YKPxb+eCYLf7BU50pJQnB/jK2ZM3qLFbieLaYjngZ86T4DzHxlPAnlAY=
|
@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ8einS88ZaWpcTZG27D5N9JDKfGv0rzjDByLGsZzMsLYl3XcsN9IWKXB6b+5GJ3UaoZf/pFxzRzIdDIh7Ypw3Y=
|
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIKZCgb5pTSSCbr/xcHCOkl9O6tQtZmNahr3Ap3/c2nBLoAoGCCqGSM49
|
||||
AwEHoUQDQgAEmPzQQlclH2/F5+F2o4tJ3NHMHDbi31go/Fv54Jgt/sFTnSklCcH+
|
||||
MrZkzeosVuJ4tpiOeBnzpPgPMfGU8CeUBg==
|
||||
-----END EC PRIVATE KEY-----
|
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIDuzykyPM6rLnSoyF4jnOpPAlyKZERqtaB8PTh179DMgoAoGCCqGSM49
|
||||
AwEHoUQDQgAEnx6KdLzxlpalxNkbbsPk30kMp8a/SvOMMHIsaxnMywtiXddyw30h
|
||||
YpcHpv7kYndRqhl/+kXHNHMh0MiHtinDdg==
|
||||
-----END EC PRIVATE KEY-----
|
Loading…
Reference in New Issue