c7f226bcec
It supports renewing X.509 certificates when an RA is configured with stepcas. This will only work when the renewal uses a token, and it won't work with mTLS. The audience cannot be properly verified when an RA is used, to avoid this we will get from the database if an RA was used to issue the initial certificate and we will accept the renew token. Fixes #1021 for stepcas
1956 lines
61 KiB
Go
1956 lines
61 KiB
Go
package authority
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha1" //nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.step.sm/crypto/jose"
|
|
"go.step.sm/crypto/keyutil"
|
|
"go.step.sm/crypto/minica"
|
|
"go.step.sm/crypto/pemutil"
|
|
"go.step.sm/crypto/x509util"
|
|
|
|
"github.com/smallstep/assert"
|
|
"github.com/smallstep/certificates/api/render"
|
|
"github.com/smallstep/certificates/authority/config"
|
|
"github.com/smallstep/certificates/authority/policy"
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
|
"github.com/smallstep/certificates/cas/softcas"
|
|
"github.com/smallstep/certificates/db"
|
|
"github.com/smallstep/certificates/errs"
|
|
"github.com/smallstep/nosql/database"
|
|
)
|
|
|
|
var (
|
|
stepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}
|
|
stepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 1)...)
|
|
)
|
|
|
|
const provisionerTypeJWK = 1
|
|
|
|
type stepProvisionerASN1 struct {
|
|
Type int
|
|
Name []byte
|
|
CredentialID []byte
|
|
}
|
|
|
|
type certificateDurationEnforcer struct {
|
|
NotBefore time.Time
|
|
NotAfter time.Time
|
|
}
|
|
|
|
func (m *certificateDurationEnforcer) Enforce(cert *x509.Certificate) error {
|
|
cert.NotBefore = m.NotBefore
|
|
cert.NotAfter = m.NotAfter
|
|
return nil
|
|
}
|
|
|
|
type certificateChainDB struct {
|
|
db.MockAuthDB
|
|
MStoreCertificateChain func(provisioner.Interface, ...*x509.Certificate) error
|
|
}
|
|
|
|
func (d *certificateChainDB) StoreCertificateChain(p provisioner.Interface, certs ...*x509.Certificate) error {
|
|
return d.MStoreCertificateChain(p, certs...)
|
|
}
|
|
|
|
func getDefaultIssuer(a *Authority) *x509.Certificate {
|
|
return a.x509CAService.(*softcas.SoftCAS).CertificateChain[len(a.x509CAService.(*softcas.SoftCAS).CertificateChain)-1]
|
|
}
|
|
|
|
func getDefaultSigner(a *Authority) crypto.Signer {
|
|
return a.x509CAService.(*softcas.SoftCAS).Signer
|
|
}
|
|
|
|
func generateCertificate(t *testing.T, commonName string, sans []string, opts ...interface{}) *x509.Certificate {
|
|
t.Helper()
|
|
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
assert.FatalError(t, err)
|
|
|
|
cr, err := x509util.CreateCertificateRequest(commonName, sans, priv)
|
|
assert.FatalError(t, err)
|
|
|
|
template, err := x509util.NewCertificate(cr)
|
|
assert.FatalError(t, err)
|
|
|
|
cert := template.GetCertificate()
|
|
for _, m := range opts {
|
|
switch m := m.(type) {
|
|
case provisioner.CertificateModifierFunc:
|
|
err = m.Modify(cert, provisioner.SignOptions{})
|
|
assert.FatalError(t, err)
|
|
case signerFunc:
|
|
cert, err = m(cert, priv.Public())
|
|
assert.FatalError(t, err)
|
|
default:
|
|
t.Fatalf("unknown type %T", m)
|
|
}
|
|
|
|
}
|
|
return cert
|
|
}
|
|
|
|
func generateRootCertificate(t *testing.T) (*x509.Certificate, crypto.Signer) {
|
|
t.Helper()
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
assert.FatalError(t, err)
|
|
|
|
cr, err := x509util.CreateCertificateRequest("TestRootCA", nil, priv)
|
|
assert.FatalError(t, err)
|
|
|
|
data := x509util.CreateTemplateData("TestRootCA", nil)
|
|
template, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data))
|
|
assert.FatalError(t, err)
|
|
|
|
cert := template.GetCertificate()
|
|
cert, err = x509util.CreateCertificate(cert, cert, priv.Public(), priv)
|
|
assert.FatalError(t, err)
|
|
return cert, priv
|
|
}
|
|
|
|
func generateIntermidiateCertificate(t *testing.T, issuer *x509.Certificate, signer crypto.Signer) (*x509.Certificate, crypto.Signer) {
|
|
t.Helper()
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
assert.FatalError(t, err)
|
|
|
|
cr, err := x509util.CreateCertificateRequest("TestIntermediateCA", nil, priv)
|
|
assert.FatalError(t, err)
|
|
|
|
data := x509util.CreateTemplateData("TestIntermediateCA", nil)
|
|
template, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data))
|
|
assert.FatalError(t, err)
|
|
|
|
cert := template.GetCertificate()
|
|
cert, err = x509util.CreateCertificate(cert, issuer, priv.Public(), signer)
|
|
assert.FatalError(t, err)
|
|
return cert, priv
|
|
}
|
|
|
|
func withSubject(sub pkix.Name) provisioner.CertificateModifierFunc {
|
|
return func(crt *x509.Certificate, _ provisioner.SignOptions) error {
|
|
crt.Subject = sub
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func withProvisionerOID(name, kid string) provisioner.CertificateModifierFunc {
|
|
return func(crt *x509.Certificate, _ provisioner.SignOptions) error {
|
|
b, err := asn1.Marshal(stepProvisionerASN1{
|
|
Type: provisionerTypeJWK,
|
|
Name: []byte(name),
|
|
CredentialID: []byte(kid),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
crt.ExtraExtensions = append(crt.ExtraExtensions, pkix.Extension{
|
|
Id: stepOIDProvisioner,
|
|
Critical: false,
|
|
Value: b,
|
|
})
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func withNotBeforeNotAfter(notBefore, notAfter time.Time) provisioner.CertificateModifierFunc {
|
|
return func(crt *x509.Certificate, _ provisioner.SignOptions) error {
|
|
crt.NotBefore = notBefore
|
|
crt.NotAfter = notAfter
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type signerFunc func(crt *x509.Certificate, pub crypto.PublicKey) (*x509.Certificate, error)
|
|
|
|
func withSigner(issuer *x509.Certificate, signer crypto.Signer) signerFunc {
|
|
return func(crt *x509.Certificate, pub crypto.PublicKey) (*x509.Certificate, error) {
|
|
return x509util.CreateCertificate(crt, issuer, pub, signer)
|
|
}
|
|
}
|
|
|
|
func getCSR(t *testing.T, priv interface{}, opts ...func(*x509.CertificateRequest)) *x509.CertificateRequest {
|
|
_csr := &x509.CertificateRequest{
|
|
Subject: pkix.Name{CommonName: "smallstep test"},
|
|
DNSNames: []string{"test.smallstep.com"},
|
|
}
|
|
for _, opt := range opts {
|
|
opt(_csr)
|
|
}
|
|
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv)
|
|
assert.FatalError(t, err)
|
|
csr, err := x509.ParseCertificateRequest(csrBytes)
|
|
assert.FatalError(t, err)
|
|
return csr
|
|
}
|
|
|
|
func setExtraExtsCSR(exts []pkix.Extension) func(*x509.CertificateRequest) {
|
|
return func(csr *x509.CertificateRequest) {
|
|
csr.ExtraExtensions = exts
|
|
}
|
|
}
|
|
|
|
func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
|
|
b, err := x509.MarshalPKIXPublicKey(pub)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error marshaling public key: %w", err)
|
|
}
|
|
info := struct {
|
|
Algorithm pkix.AlgorithmIdentifier
|
|
SubjectPublicKey asn1.BitString
|
|
}{}
|
|
if _, err = asn1.Unmarshal(b, &info); err != nil {
|
|
return nil, fmt.Errorf("error unmarshaling public key: %w", err)
|
|
}
|
|
//nolint:gosec // used to create the Subject Key Identifier by RFC 5280
|
|
hash := sha1.Sum(info.SubjectPublicKey.Bytes)
|
|
return hash[:], nil
|
|
}
|
|
|
|
type basicConstraints struct {
|
|
IsCA bool `asn1:"optional"`
|
|
MaxPathLen int `asn1:"optional,default:-1"`
|
|
}
|
|
|
|
type testEnforcer struct {
|
|
enforcer func(*x509.Certificate) error
|
|
}
|
|
|
|
func (e *testEnforcer) Enforce(cert *x509.Certificate) error {
|
|
if e.enforcer != nil {
|
|
return e.enforcer(cert)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestAuthority_Sign(t *testing.T) {
|
|
pub, priv, err := keyutil.GenerateDefaultKeyPair()
|
|
assert.FatalError(t, err)
|
|
|
|
a := testAuthority(t)
|
|
assert.FatalError(t, err)
|
|
a.config.AuthorityConfig.Template = &ASN1DN{
|
|
Country: "Tazmania",
|
|
Organization: "Acme Co",
|
|
Locality: "Landscapes",
|
|
Province: "Sudden Cliffs",
|
|
StreetAddress: "TNT",
|
|
CommonName: "test.smallstep.com",
|
|
}
|
|
|
|
nb := time.Now()
|
|
signOpts := provisioner.SignOptions{
|
|
NotBefore: provisioner.NewTimeDuration(nb),
|
|
NotAfter: provisioner.NewTimeDuration(nb.Add(time.Minute * 5)),
|
|
Backdate: 1 * time.Minute,
|
|
}
|
|
|
|
// Create a token to get test extra opts.
|
|
p := a.config.AuthorityConfig.Provisioners[1].(*provisioner.JWK)
|
|
key, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
|
assert.FatalError(t, err)
|
|
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)
|
|
assert.FatalError(t, err)
|
|
|
|
type signTest struct {
|
|
auth *Authority
|
|
csr *x509.CertificateRequest
|
|
signOpts provisioner.SignOptions
|
|
extraOpts []provisioner.SignOption
|
|
notBefore time.Time
|
|
notAfter time.Time
|
|
extensionsCount int
|
|
err error
|
|
code int
|
|
}
|
|
tests := map[string]func(*testing.T) *signTest{
|
|
"fail invalid signature": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
csr.Signature = []byte("foo")
|
|
return &signTest{
|
|
auth: a,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("invalid certificate request"),
|
|
code: http.StatusBadRequest,
|
|
}
|
|
},
|
|
"fail invalid extra option": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
csr.Raw = []byte("foo")
|
|
return &signTest{
|
|
auth: a,
|
|
csr: csr,
|
|
extraOpts: append(extraOpts, "42"),
|
|
signOpts: signOpts,
|
|
err: errors.New("authority.Sign; invalid extra option type string"),
|
|
code: http.StatusInternalServerError,
|
|
}
|
|
},
|
|
"fail merge default ASN1DN": func(t *testing.T) *signTest {
|
|
_a := testAuthority(t)
|
|
_a.config.AuthorityConfig.Template = nil
|
|
csr := getCSR(t, priv)
|
|
return &signTest{
|
|
auth: _a,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("default ASN1DN template cannot be nil"),
|
|
code: http.StatusForbidden,
|
|
}
|
|
},
|
|
"fail create cert": func(t *testing.T) *signTest {
|
|
_a := testAuthority(t)
|
|
_a.x509CAService.(*softcas.SoftCAS).Signer = nil
|
|
csr := getCSR(t, priv)
|
|
return &signTest{
|
|
auth: _a,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("authority.Sign; error creating certificate"),
|
|
code: http.StatusInternalServerError,
|
|
}
|
|
},
|
|
"fail provisioner duration claim": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
_signOpts := provisioner.SignOptions{
|
|
NotBefore: provisioner.NewTimeDuration(nb),
|
|
NotAfter: provisioner.NewTimeDuration(nb.Add(time.Hour * 25)),
|
|
}
|
|
return &signTest{
|
|
auth: a,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: _signOpts,
|
|
err: errors.New("requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h1m0s"),
|
|
code: http.StatusForbidden,
|
|
}
|
|
},
|
|
"fail validate sans when adding common name not in claims": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv, func(csr *x509.CertificateRequest) {
|
|
csr.DNSNames = append(csr.DNSNames, csr.Subject.CommonName)
|
|
})
|
|
return &signTest{
|
|
auth: a,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]"),
|
|
code: http.StatusForbidden,
|
|
}
|
|
},
|
|
"fail rsa key too short": func(t *testing.T) *signTest {
|
|
shortRSAKeyPEM := `-----BEGIN CERTIFICATE REQUEST-----
|
|
MIIBhDCB7gIBADAZMRcwFQYDVQQDEw5zbWFsbHN0ZXAgdGVzdDCBnzANBgkqhkiG
|
|
9w0BAQEFAAOBjQAwgYkCgYEA5JlgH99HvHHsCD6XTqqYj3bXU2oIlnYGoLVs7IJ4
|
|
k205rv5/YWky2gjdpIv0Tnaf3o57IJ891lB7GiyO5iHIEUv5N9dVzrdUboyzk2uZ
|
|
7JMMNB43CSLB2oNuwJjLeAM/yBzlhRnvpKjrNSfSV+cH54FXdnbFbcTFMStnjqKG
|
|
MeECAwEAAaAsMCoGCSqGSIb3DQEJDjEdMBswGQYDVR0RBBIwEIIOc21hbGxzdGVw
|
|
IHRlc3QwDQYJKoZIhvcNAQELBQADgYEAKwsbr8Zfcq05DgOoJ//cXMFK1SP8ktRU
|
|
N2++E8Ww0Tet9oyNRArqxxS/UyVio63D3wynzRAB25PFGpYG1cN4b81Gv/foFUT6
|
|
W5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5/3i16d5zhuxUoaPTYr
|
|
ZYtQ9Ot36qc=
|
|
-----END CERTIFICATE REQUEST-----`
|
|
block, _ := pem.Decode([]byte(shortRSAKeyPEM))
|
|
assert.FatalError(t, err)
|
|
csr, err := x509.ParseCertificateRequest(block.Bytes)
|
|
assert.FatalError(t, err)
|
|
|
|
return &signTest{
|
|
auth: a,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("certificate request RSA key must be at least 2048 bits (256 bytes)"),
|
|
code: http.StatusForbidden,
|
|
}
|
|
},
|
|
"fail store cert in db": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
_a := testAuthority(t)
|
|
_a.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
return errors.New("force")
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: _a,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("authority.Sign; error storing certificate in db: force"),
|
|
code: http.StatusInternalServerError,
|
|
}
|
|
},
|
|
"fail custom template": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
testAuthority := testAuthority(t)
|
|
p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
|
|
if !ok {
|
|
t.Fatal("provisioner not found")
|
|
}
|
|
p.(*provisioner.JWK).Options = &provisioner.Options{
|
|
X509: &provisioner.X509Options{Template: `{{ fail "fail message" }}`},
|
|
}
|
|
testExtraOpts, err := testAuthority.Authorize(ctx, token)
|
|
assert.FatalError(t, err)
|
|
testAuthority.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: testAuthority,
|
|
csr: csr,
|
|
extraOpts: testExtraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("fail message"),
|
|
code: http.StatusBadRequest,
|
|
}
|
|
},
|
|
"fail bad JSON syntax template file": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
testAuthority := testAuthority(t)
|
|
p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
|
|
if !ok {
|
|
t.Fatal("provisioner not found")
|
|
}
|
|
p.(*provisioner.JWK).Options = &provisioner.Options{
|
|
X509: &provisioner.X509Options{
|
|
TemplateFile: "./testdata/templates/badjsonsyntax.tpl",
|
|
},
|
|
}
|
|
testExtraOpts, err := testAuthority.Authorize(ctx, token)
|
|
assert.FatalError(t, err)
|
|
testAuthority.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: testAuthority,
|
|
csr: csr,
|
|
extraOpts: testExtraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("error applying certificate template: invalid character"),
|
|
code: http.StatusInternalServerError,
|
|
}
|
|
},
|
|
"fail bad JSON value template file": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
testAuthority := testAuthority(t)
|
|
p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
|
|
if !ok {
|
|
t.Fatal("provisioner not found")
|
|
}
|
|
p.(*provisioner.JWK).Options = &provisioner.Options{
|
|
X509: &provisioner.X509Options{
|
|
TemplateFile: "./testdata/templates/badjsonvalue.tpl",
|
|
},
|
|
}
|
|
testExtraOpts, err := testAuthority.Authorize(ctx, token)
|
|
assert.FatalError(t, err)
|
|
testAuthority.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: testAuthority,
|
|
csr: csr,
|
|
extraOpts: testExtraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("error applying certificate template: cannot unmarshal"),
|
|
code: http.StatusInternalServerError,
|
|
}
|
|
},
|
|
"fail with provisioner enforcer": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
aa := testAuthority(t)
|
|
aa.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
return &signTest{
|
|
auth: aa,
|
|
csr: csr,
|
|
extraOpts: append(extraOpts, &testEnforcer{
|
|
enforcer: func(crt *x509.Certificate) error { return fmt.Errorf("an error") },
|
|
}),
|
|
signOpts: signOpts,
|
|
err: errors.New("error creating certificate"),
|
|
code: http.StatusForbidden,
|
|
}
|
|
},
|
|
"fail with custom enforcer": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
aa := testAuthority(t, WithX509Enforcers(&testEnforcer{
|
|
enforcer: func(cert *x509.Certificate) error {
|
|
return fmt.Errorf("an error")
|
|
},
|
|
}))
|
|
aa.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: aa,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
err: errors.New("error creating certificate"),
|
|
code: http.StatusForbidden,
|
|
}
|
|
},
|
|
"fail with policy": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
aa := testAuthority(t)
|
|
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
|
aa.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
fmt.Println(crt.Subject)
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
options := &policy.Options{
|
|
X509: &policy.X509PolicyOptions{
|
|
DeniedNames: &policy.X509NameOptions{
|
|
DNSDomains: []string{"test.smallstep.com"},
|
|
},
|
|
},
|
|
}
|
|
engine, err := policy.New(options)
|
|
assert.FatalError(t, err)
|
|
aa.policyEngine = engine
|
|
return &signTest{
|
|
auth: aa,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
|
extensionsCount: 6,
|
|
err: errors.New("dns name \"test.smallstep.com\" not allowed"),
|
|
code: http.StatusForbidden,
|
|
}
|
|
},
|
|
"fail enriching webhooks": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
csr.Raw = []byte("foo")
|
|
return &signTest{
|
|
auth: a,
|
|
csr: csr,
|
|
extensionsCount: 7,
|
|
extraOpts: append(extraOpts, &mockWebhookController{
|
|
enrichErr: provisioner.ErrWebhookDenied,
|
|
}),
|
|
signOpts: signOpts,
|
|
err: provisioner.ErrWebhookDenied,
|
|
code: http.StatusForbidden,
|
|
}
|
|
},
|
|
"fail authorizing webhooks": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
csr.Raw = []byte("foo")
|
|
return &signTest{
|
|
auth: a,
|
|
csr: csr,
|
|
extensionsCount: 7,
|
|
extraOpts: append(extraOpts, &mockWebhookController{
|
|
authorizeErr: provisioner.ErrWebhookDenied,
|
|
}),
|
|
signOpts: signOpts,
|
|
err: provisioner.ErrWebhookDenied,
|
|
code: http.StatusForbidden,
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
_a := testAuthority(t)
|
|
_a.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: a,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
|
extensionsCount: 6,
|
|
}
|
|
},
|
|
"ok with enforced modifier": func(t *testing.T) *signTest {
|
|
bcExt := pkix.Extension{}
|
|
bcExt.Id = asn1.ObjectIdentifier{2, 5, 29, 19}
|
|
bcExt.Critical = false
|
|
bcExt.Value, err = asn1.Marshal(basicConstraints{IsCA: true, MaxPathLen: 4})
|
|
assert.FatalError(t, err)
|
|
|
|
csr := getCSR(t, priv, setExtraExtsCSR([]pkix.Extension{
|
|
bcExt,
|
|
{Id: stepOIDProvisioner, Value: []byte("foo")},
|
|
{Id: []int{1, 1, 1}, Value: []byte("bar")}}))
|
|
now := time.Now().UTC()
|
|
//nolint:gocritic
|
|
enforcedExtraOptions := append(extraOpts, &certificateDurationEnforcer{
|
|
NotBefore: now,
|
|
NotAfter: now.Add(365 * 24 * time.Hour),
|
|
})
|
|
_a := testAuthority(t)
|
|
_a.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: a,
|
|
csr: csr,
|
|
extraOpts: enforcedExtraOptions,
|
|
signOpts: signOpts,
|
|
notBefore: now.Truncate(time.Second),
|
|
notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second),
|
|
extensionsCount: 6,
|
|
}
|
|
},
|
|
"ok with custom template": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
testAuthority := testAuthority(t)
|
|
testAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
|
p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
|
|
if !ok {
|
|
t.Fatal("provisioner not found")
|
|
}
|
|
p.(*provisioner.JWK).Options = &provisioner.Options{
|
|
X509: &provisioner.X509Options{Template: `{
|
|
"subject": {{toJson .Subject}},
|
|
"dnsNames": {{ toJson .Insecure.CR.DNSNames }},
|
|
"keyUsage": ["digitalSignature"],
|
|
"extKeyUsage": ["serverAuth","clientAuth"]
|
|
}`},
|
|
}
|
|
testExtraOpts, err := testAuthority.Authorize(ctx, token)
|
|
assert.FatalError(t, err)
|
|
testAuthority.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: testAuthority,
|
|
csr: csr,
|
|
extraOpts: testExtraOpts,
|
|
signOpts: signOpts,
|
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
|
extensionsCount: 6,
|
|
}
|
|
},
|
|
"ok with enriching webhook": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
testAuthority := testAuthority(t)
|
|
testAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
|
p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
|
|
if !ok {
|
|
t.Fatal("provisioner not found")
|
|
}
|
|
p.(*provisioner.JWK).Options = &provisioner.Options{
|
|
X509: &provisioner.X509Options{Template: `{
|
|
"subject": {"commonName": {{ toJson .Webhooks.people.role }} },
|
|
"dnsNames": {{ toJson .Insecure.CR.DNSNames }},
|
|
"keyUsage": ["digitalSignature"],
|
|
"extKeyUsage": ["serverAuth","clientAuth"]
|
|
}`},
|
|
}
|
|
testExtraOpts, err := testAuthority.Authorize(ctx, token)
|
|
assert.FatalError(t, err)
|
|
testAuthority.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
for i, o := range testExtraOpts {
|
|
if wc, ok := o.(*provisioner.WebhookController); ok {
|
|
testExtraOpts[i] = &mockWebhookController{
|
|
templateData: wc.TemplateData,
|
|
respData: map[string]any{"people": map[string]any{"role": "smallstep test"}},
|
|
}
|
|
}
|
|
}
|
|
return &signTest{
|
|
auth: testAuthority,
|
|
csr: csr,
|
|
extraOpts: testExtraOpts,
|
|
signOpts: signOpts,
|
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
|
extensionsCount: 6,
|
|
}
|
|
},
|
|
"ok/csr with no template critical SAN extension": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv, func(csr *x509.CertificateRequest) {
|
|
csr.Subject = pkix.Name{}
|
|
}, func(csr *x509.CertificateRequest) {
|
|
csr.DNSNames = []string{"foo", "bar"}
|
|
})
|
|
now := time.Now().UTC()
|
|
enforcedExtraOptions := []provisioner.SignOption{&certificateDurationEnforcer{
|
|
NotBefore: now,
|
|
NotAfter: now.Add(365 * 24 * time.Hour),
|
|
}}
|
|
_a := testAuthority(t)
|
|
_a.config.AuthorityConfig.Template = &ASN1DN{}
|
|
_a.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject, pkix.Name{})
|
|
return nil
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: _a,
|
|
csr: csr,
|
|
extraOpts: enforcedExtraOptions,
|
|
signOpts: provisioner.SignOptions{},
|
|
notBefore: now.Truncate(time.Second),
|
|
notAfter: now.Add(365 * 24 * time.Hour).Truncate(time.Second),
|
|
extensionsCount: 5,
|
|
}
|
|
},
|
|
"ok with custom enforcer": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
aa := testAuthority(t, WithX509Enforcers(&testEnforcer{
|
|
enforcer: func(cert *x509.Certificate) error {
|
|
cert.CRLDistributionPoints = []string{"http://ca.example.org/leaf.crl"}
|
|
return nil
|
|
},
|
|
}))
|
|
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
|
aa.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
assert.Equals(t, crt.CRLDistributionPoints, []string{"http://ca.example.org/leaf.crl"})
|
|
return nil
|
|
},
|
|
}
|
|
return &signTest{
|
|
auth: aa,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
|
extensionsCount: 7,
|
|
}
|
|
},
|
|
"ok with policy": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
aa := testAuthority(t)
|
|
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
|
aa.db = &db.MockAuthDB{
|
|
MStoreCertificate: func(crt *x509.Certificate) error {
|
|
assert.Equals(t, crt.Subject.CommonName, "smallstep test")
|
|
return nil
|
|
},
|
|
}
|
|
options := &policy.Options{
|
|
X509: &policy.X509PolicyOptions{
|
|
AllowedNames: &policy.X509NameOptions{
|
|
CommonNames: []string{"smallstep test"},
|
|
DNSDomains: []string{"*.smallstep.com"},
|
|
},
|
|
},
|
|
}
|
|
engine, err := policy.New(options)
|
|
assert.FatalError(t, err)
|
|
aa.policyEngine = engine
|
|
return &signTest{
|
|
auth: aa,
|
|
csr: csr,
|
|
extraOpts: extraOpts,
|
|
signOpts: signOpts,
|
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
|
extensionsCount: 6,
|
|
}
|
|
},
|
|
"ok with attestation data": func(t *testing.T) *signTest {
|
|
csr := getCSR(t, priv)
|
|
aa := testAuthority(t)
|
|
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
|
aa.db = &certificateChainDB{
|
|
MStoreCertificateChain: func(prov provisioner.Interface, certs ...*x509.Certificate) error {
|
|
p, ok := prov.(attProvisioner)
|
|
if assert.True(t, ok) {
|
|
assert.Equals(t, &provisioner.AttestationData{
|
|
PermanentIdentifier: "1234567890",
|
|
}, p.AttestationData())
|
|
}
|
|
if assert.Len(t, 2, certs) {
|
|
assert.Equals(t, certs[0].Subject.CommonName, "smallstep test")
|
|
assert.Equals(t, certs[1].Subject.CommonName, "smallstep Intermediate CA")
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
return &signTest{
|
|
auth: aa,
|
|
csr: csr,
|
|
extraOpts: append(extraOpts, provisioner.AttestationData{
|
|
PermanentIdentifier: "1234567890",
|
|
}),
|
|
signOpts: signOpts,
|
|
notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
|
|
notAfter: signOpts.NotAfter.Time().Truncate(time.Second),
|
|
extensionsCount: 6,
|
|
}
|
|
},
|
|
}
|
|
|
|
for name, genTestCase := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := genTestCase(t)
|
|
|
|
certChain, err := tc.auth.Sign(tc.csr, tc.signOpts, tc.extraOpts...)
|
|
if err != nil {
|
|
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
|
|
assert.Nil(t, certChain)
|
|
var sc render.StatusCodedError
|
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
|
|
|
var ctxErr *errs.Error
|
|
assert.Fatal(t, errors.As(err, &ctxErr), "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]
|
|
intermediate := certChain[1]
|
|
if assert.Nil(t, tc.err) {
|
|
assert.Equals(t, leaf.NotBefore, tc.notBefore)
|
|
assert.Equals(t, leaf.NotAfter, tc.notAfter)
|
|
tmplt := a.config.AuthorityConfig.Template
|
|
if tc.csr.Subject.CommonName == "" {
|
|
assert.Equals(t, leaf.Subject, pkix.Name{})
|
|
} else {
|
|
assert.Equals(t, leaf.Subject.String(),
|
|
pkix.Name{
|
|
Country: []string{tmplt.Country},
|
|
Organization: []string{tmplt.Organization},
|
|
Locality: []string{tmplt.Locality},
|
|
StreetAddress: []string{tmplt.StreetAddress},
|
|
Province: []string{tmplt.Province},
|
|
CommonName: "smallstep test",
|
|
}.String())
|
|
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com"})
|
|
}
|
|
assert.Equals(t, leaf.Issuer, intermediate.Subject)
|
|
assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
|
|
assert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
|
|
assert.Equals(t, leaf.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
|
|
|
|
issuer := getDefaultIssuer(a)
|
|
subjectKeyID, err := generateSubjectKeyID(pub)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
|
|
assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
|
|
|
|
// Verify Provisioner OID
|
|
found := 0
|
|
for _, ext := range leaf.Extensions {
|
|
switch {
|
|
case ext.Id.Equal(stepOIDProvisioner):
|
|
found++
|
|
val := stepProvisionerASN1{}
|
|
_, err := asn1.Unmarshal(ext.Value, &val)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, val.Type, provisionerTypeJWK)
|
|
assert.Equals(t, val.Name, []byte(p.Name))
|
|
assert.Equals(t, val.CredentialID, []byte(p.Key.KeyID))
|
|
|
|
// Basic Constraints
|
|
case ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 19})):
|
|
val := basicConstraints{}
|
|
_, err := asn1.Unmarshal(ext.Value, &val)
|
|
assert.FatalError(t, err)
|
|
assert.False(t, val.IsCA, false)
|
|
assert.Equals(t, val.MaxPathLen, 0)
|
|
|
|
// SAN extension
|
|
case ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 17})):
|
|
if tc.csr.Subject.CommonName == "" {
|
|
// Empty CSR subject test does not use any provisioner extensions.
|
|
// So provisioner ID ext will be missing.
|
|
found = 1
|
|
}
|
|
}
|
|
}
|
|
assert.Equals(t, found, 1)
|
|
realIntermediate, err := x509.ParseCertificate(issuer.Raw)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, intermediate, realIntermediate)
|
|
assert.Len(t, tc.extensionsCount, leaf.Extensions)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthority_Renew(t *testing.T) {
|
|
a := testAuthority(t)
|
|
a.config.AuthorityConfig.Template = &ASN1DN{
|
|
Country: "Tazmania",
|
|
Organization: "Acme Co",
|
|
Locality: "Landscapes",
|
|
Province: "Sudden Cliffs",
|
|
StreetAddress: "TNT",
|
|
CommonName: "renew",
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
nb1 := now.Add(-time.Minute * 7)
|
|
na1 := now.Add(time.Hour)
|
|
so := &provisioner.SignOptions{
|
|
NotBefore: provisioner.NewTimeDuration(nb1),
|
|
NotAfter: provisioner.NewTimeDuration(na1),
|
|
}
|
|
|
|
issuer := getDefaultIssuer(a)
|
|
signer := getDefaultSigner(a)
|
|
|
|
cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
|
|
withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
|
|
withDefaultASN1DN(a.config.AuthorityConfig.Template),
|
|
withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),
|
|
withSigner(issuer, signer))
|
|
|
|
certExtraNames := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
|
|
withSubject(pkix.Name{
|
|
CommonName: "renew",
|
|
ExtraNames: []pkix.AttributeTypeAndValue{
|
|
{Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}, Value: "dc"},
|
|
},
|
|
}),
|
|
withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
|
|
withDefaultASN1DN(a.config.AuthorityConfig.Template),
|
|
withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),
|
|
withSigner(issuer, signer))
|
|
|
|
certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
|
|
withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
|
|
withDefaultASN1DN(a.config.AuthorityConfig.Template),
|
|
withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),
|
|
withSigner(issuer, signer))
|
|
|
|
type renewTest struct {
|
|
auth *Authority
|
|
cert *x509.Certificate
|
|
err error
|
|
code int
|
|
}
|
|
tests := map[string]func() (*renewTest, error){
|
|
"fail/create-cert": func() (*renewTest, error) {
|
|
_a := testAuthority(t)
|
|
_a.x509CAService.(*softcas.SoftCAS).Signer = nil
|
|
return &renewTest{
|
|
auth: _a,
|
|
cert: cert,
|
|
err: errors.New("error creating certificate"),
|
|
code: http.StatusInternalServerError,
|
|
}, nil
|
|
},
|
|
"fail/unauthorized": func() (*renewTest, error) {
|
|
return &renewTest{
|
|
cert: certNoRenew,
|
|
err: errors.New("authority.authorizeRenew: renew is disabled for provisioner 'dev'"),
|
|
code: http.StatusUnauthorized,
|
|
}, nil
|
|
},
|
|
"fail/WithAuthorizeRenewFunc": func() (*renewTest, error) {
|
|
aa := testAuthority(t, WithAuthorizeRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *x509.Certificate) error {
|
|
return errs.Unauthorized("not authorized")
|
|
}))
|
|
aa.x509CAService = a.x509CAService
|
|
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
|
return &renewTest{
|
|
auth: aa,
|
|
cert: cert,
|
|
err: errors.New("authority.authorizeRenew: not authorized"),
|
|
code: http.StatusUnauthorized,
|
|
}, nil
|
|
},
|
|
"ok": func() (*renewTest, error) {
|
|
return &renewTest{
|
|
auth: a,
|
|
cert: cert,
|
|
}, nil
|
|
},
|
|
"ok/WithExtraNames": func() (*renewTest, error) {
|
|
return &renewTest{
|
|
auth: a,
|
|
cert: certExtraNames,
|
|
}, nil
|
|
},
|
|
"ok/success-new-intermediate": func() (*renewTest, error) {
|
|
rootCert, rootSigner := generateRootCertificate(t)
|
|
intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner)
|
|
|
|
_a := testAuthority(t)
|
|
_a.x509CAService.(*softcas.SoftCAS).CertificateChain = []*x509.Certificate{intCert}
|
|
_a.x509CAService.(*softcas.SoftCAS).Signer = intSigner
|
|
return &renewTest{
|
|
auth: _a,
|
|
cert: cert,
|
|
}, nil
|
|
},
|
|
"ok/WithAuthorizeRenewFunc": func() (*renewTest, error) {
|
|
aa := testAuthority(t, WithAuthorizeRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *x509.Certificate) error {
|
|
return nil
|
|
}))
|
|
aa.x509CAService = a.x509CAService
|
|
aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
|
|
return &renewTest{
|
|
auth: aa,
|
|
cert: cert,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
for name, genTestCase := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc, err := genTestCase()
|
|
assert.FatalError(t, err)
|
|
|
|
var certChain []*x509.Certificate
|
|
if tc.auth != nil {
|
|
certChain, err = tc.auth.Renew(tc.cert)
|
|
} else {
|
|
certChain, err = a.Renew(tc.cert)
|
|
}
|
|
if err != nil {
|
|
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
|
|
assert.Nil(t, certChain)
|
|
var sc render.StatusCodedError
|
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
|
|
|
var ctxErr *errs.Error
|
|
assert.Fatal(t, errors.As(err, &ctxErr), "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.cert.NotAfter.Sub(cert.NotBefore))
|
|
|
|
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(-2*time.Minute)))
|
|
assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour)))
|
|
|
|
tmplt := a.config.AuthorityConfig.Template
|
|
assert.Equals(t, leaf.RawSubject, tc.cert.RawSubject)
|
|
assert.Equals(t, leaf.Subject.Country, []string{tmplt.Country})
|
|
assert.Equals(t, leaf.Subject.Organization, []string{tmplt.Organization})
|
|
assert.Equals(t, leaf.Subject.Locality, []string{tmplt.Locality})
|
|
assert.Equals(t, leaf.Subject.StreetAddress, []string{tmplt.StreetAddress})
|
|
assert.Equals(t, leaf.Subject.Province, []string{tmplt.Province})
|
|
assert.Equals(t, leaf.Subject.CommonName, tmplt.CommonName)
|
|
|
|
assert.Equals(t, leaf.Issuer, intermediate.Subject)
|
|
|
|
assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
|
|
assert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
|
|
assert.Equals(t, leaf.ExtKeyUsage,
|
|
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
|
|
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"})
|
|
|
|
subjectKeyID, err := generateSubjectKeyID(leaf.PublicKey)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
|
|
|
|
// We did not change the intermediate before renewing.
|
|
authIssuer := getDefaultIssuer(tc.auth)
|
|
if issuer.SerialNumber == authIssuer.SerialNumber {
|
|
assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
|
|
// Compare extensions: they can be in a different order
|
|
for _, ext1 := range tc.cert.Extensions {
|
|
//skip SubjectKeyIdentifier
|
|
if ext1.Id.Equal(oidSubjectKeyIdentifier) {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, ext2 := range leaf.Extensions {
|
|
if reflect.DeepEqual(ext1, ext2) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String())
|
|
}
|
|
}
|
|
} else {
|
|
// We did change the intermediate before renewing.
|
|
assert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId)
|
|
// Compare extensions: they can be in a different order
|
|
for _, ext1 := range tc.cert.Extensions {
|
|
//skip SubjectKeyIdentifier
|
|
if ext1.Id.Equal(oidSubjectKeyIdentifier) {
|
|
continue
|
|
}
|
|
// The authority key id extension should be different b/c the intermediates are different.
|
|
if ext1.Id.Equal(oidAuthorityKeyIdentifier) {
|
|
for _, ext2 := range leaf.Extensions {
|
|
assert.False(t, reflect.DeepEqual(ext1, ext2))
|
|
}
|
|
continue
|
|
} else {
|
|
found := false
|
|
for _, ext2 := range leaf.Extensions {
|
|
if reflect.DeepEqual(ext1, ext2) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
realIntermediate, err := x509.ParseCertificate(authIssuer.Raw)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, intermediate, realIntermediate)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthority_Rekey(t *testing.T) {
|
|
pub, _, err := keyutil.GenerateDefaultKeyPair()
|
|
assert.FatalError(t, err)
|
|
|
|
a := testAuthority(t)
|
|
a.config.AuthorityConfig.Template = &ASN1DN{
|
|
Country: "Tazmania",
|
|
Organization: "Acme Co",
|
|
Locality: "Landscapes",
|
|
Province: "Sudden Cliffs",
|
|
StreetAddress: "TNT",
|
|
CommonName: "renew",
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
nb1 := now.Add(-time.Minute * 7)
|
|
na1 := now.Add(time.Hour)
|
|
so := &provisioner.SignOptions{
|
|
NotBefore: provisioner.NewTimeDuration(nb1),
|
|
NotAfter: provisioner.NewTimeDuration(na1),
|
|
}
|
|
|
|
issuer := getDefaultIssuer(a)
|
|
signer := getDefaultSigner(a)
|
|
|
|
cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
|
|
withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
|
|
withDefaultASN1DN(a.config.AuthorityConfig.Template),
|
|
withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),
|
|
withSigner(issuer, signer))
|
|
|
|
certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
|
|
withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
|
|
withDefaultASN1DN(a.config.AuthorityConfig.Template),
|
|
withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),
|
|
withSigner(issuer, signer))
|
|
|
|
type renewTest struct {
|
|
auth *Authority
|
|
cert *x509.Certificate
|
|
pk crypto.PublicKey
|
|
err error
|
|
code int
|
|
}
|
|
tests := map[string]func() (*renewTest, error){
|
|
"fail/create-cert": func() (*renewTest, error) {
|
|
_a := testAuthority(t)
|
|
_a.x509CAService.(*softcas.SoftCAS).Signer = nil
|
|
return &renewTest{
|
|
auth: _a,
|
|
cert: cert,
|
|
err: errors.New("error creating certificate"),
|
|
code: http.StatusInternalServerError,
|
|
}, nil
|
|
},
|
|
"fail/unauthorized": func() (*renewTest, error) {
|
|
return &renewTest{
|
|
cert: certNoRenew,
|
|
err: errors.New("authority.authorizeRenew: renew is disabled for provisioner 'dev'"),
|
|
code: http.StatusUnauthorized,
|
|
}, nil
|
|
},
|
|
"ok/renew": func() (*renewTest, error) {
|
|
return &renewTest{
|
|
auth: a,
|
|
cert: cert,
|
|
}, nil
|
|
},
|
|
"ok/rekey": func() (*renewTest, error) {
|
|
return &renewTest{
|
|
auth: a,
|
|
cert: cert,
|
|
pk: pub,
|
|
}, nil
|
|
},
|
|
"ok/renew/success-new-intermediate": func() (*renewTest, error) {
|
|
rootCert, rootSigner := generateRootCertificate(t)
|
|
intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner)
|
|
|
|
_a := testAuthority(t)
|
|
_a.x509CAService.(*softcas.SoftCAS).CertificateChain = []*x509.Certificate{intCert}
|
|
_a.x509CAService.(*softcas.SoftCAS).Signer = intSigner
|
|
return &renewTest{
|
|
auth: _a,
|
|
cert: cert,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
for name, genTestCase := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc, err := genTestCase()
|
|
assert.FatalError(t, err)
|
|
|
|
var certChain []*x509.Certificate
|
|
if tc.auth != nil {
|
|
certChain, err = tc.auth.Rekey(tc.cert, tc.pk)
|
|
} else {
|
|
certChain, err = a.Rekey(tc.cert, tc.pk)
|
|
}
|
|
if err != nil {
|
|
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
|
|
assert.Nil(t, certChain)
|
|
var sc render.StatusCodedError
|
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
|
|
|
var ctxErr *errs.Error
|
|
assert.Fatal(t, errors.As(err, &ctxErr), "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.cert.NotAfter.Sub(cert.NotBefore))
|
|
|
|
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(-2*time.Minute)))
|
|
assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour)))
|
|
|
|
tmplt := a.config.AuthorityConfig.Template
|
|
assert.Equals(t, leaf.Subject.String(),
|
|
pkix.Name{
|
|
Country: []string{tmplt.Country},
|
|
Organization: []string{tmplt.Organization},
|
|
Locality: []string{tmplt.Locality},
|
|
StreetAddress: []string{tmplt.StreetAddress},
|
|
Province: []string{tmplt.Province},
|
|
CommonName: tmplt.CommonName,
|
|
}.String())
|
|
assert.Equals(t, leaf.Issuer, intermediate.Subject)
|
|
|
|
assert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)
|
|
assert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)
|
|
assert.Equals(t, leaf.ExtKeyUsage,
|
|
[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
|
|
assert.Equals(t, leaf.DNSNames, []string{"test.smallstep.com", "test"})
|
|
|
|
// Test Public Key and SubjectKeyId
|
|
expectedPK := tc.pk
|
|
if tc.pk == nil {
|
|
expectedPK = cert.PublicKey
|
|
}
|
|
assert.Equals(t, leaf.PublicKey, expectedPK)
|
|
|
|
subjectKeyID, err := generateSubjectKeyID(expectedPK)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, leaf.SubjectKeyId, subjectKeyID)
|
|
if tc.pk == nil {
|
|
assert.Equals(t, leaf.SubjectKeyId, cert.SubjectKeyId)
|
|
}
|
|
|
|
// We did not change the intermediate before renewing.
|
|
authIssuer := getDefaultIssuer(tc.auth)
|
|
if issuer.SerialNumber == authIssuer.SerialNumber {
|
|
assert.Equals(t, leaf.AuthorityKeyId, issuer.SubjectKeyId)
|
|
// Compare extensions: they can be in a different order
|
|
for _, ext1 := range tc.cert.Extensions {
|
|
//skip SubjectKeyIdentifier
|
|
if ext1.Id.Equal(oidSubjectKeyIdentifier) {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, ext2 := range leaf.Extensions {
|
|
if reflect.DeepEqual(ext1, ext2) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String())
|
|
}
|
|
}
|
|
} else {
|
|
// We did change the intermediate before renewing.
|
|
assert.Equals(t, leaf.AuthorityKeyId, authIssuer.SubjectKeyId)
|
|
// Compare extensions: they can be in a different order
|
|
for _, ext1 := range tc.cert.Extensions {
|
|
//skip SubjectKeyIdentifier
|
|
if ext1.Id.Equal(oidSubjectKeyIdentifier) {
|
|
continue
|
|
}
|
|
// The authority key id extension should be different b/c the intermediates are different.
|
|
if ext1.Id.Equal(oidAuthorityKeyIdentifier) {
|
|
for _, ext2 := range leaf.Extensions {
|
|
assert.False(t, reflect.DeepEqual(ext1, ext2))
|
|
}
|
|
continue
|
|
} else {
|
|
found := false
|
|
for _, ext2 := range leaf.Extensions {
|
|
if reflect.DeepEqual(ext1, ext2) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
realIntermediate, err := x509.ParseCertificate(authIssuer.Raw)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, intermediate, realIntermediate)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthority_GetTLSOptions(t *testing.T) {
|
|
type renewTest struct {
|
|
auth *Authority
|
|
opts *TLSOptions
|
|
}
|
|
tests := map[string]func() (*renewTest, error){
|
|
"default": func() (*renewTest, error) {
|
|
a := testAuthority(t)
|
|
return &renewTest{auth: a, opts: &DefaultTLSOptions}, nil
|
|
},
|
|
"non-default": func() (*renewTest, error) {
|
|
a := testAuthority(t)
|
|
a.config.TLS = &TLSOptions{
|
|
CipherSuites: CipherSuites{
|
|
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
|
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
|
},
|
|
MinVersion: 1.0,
|
|
MaxVersion: 1.1,
|
|
Renegotiation: true,
|
|
}
|
|
return &renewTest{auth: a, opts: a.config.TLS}, nil
|
|
},
|
|
}
|
|
|
|
for name, genTestCase := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc, err := genTestCase()
|
|
assert.FatalError(t, err)
|
|
|
|
opts := tc.auth.GetTLSOptions()
|
|
assert.Equals(t, opts, tc.opts)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthority_Revoke(t *testing.T) {
|
|
reasonCode := 2
|
|
reason := "bob was let go"
|
|
validIssuer := "step-cli"
|
|
validAudience := testAudiences.Revoke
|
|
now := time.Now().UTC()
|
|
|
|
jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
|
assert.FatalError(t, err)
|
|
|
|
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
|
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
|
assert.FatalError(t, err)
|
|
|
|
a := testAuthority(t)
|
|
|
|
tlsRevokeCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)
|
|
|
|
type test struct {
|
|
auth *Authority
|
|
ctx context.Context
|
|
opts *RevokeOptions
|
|
err error
|
|
code int
|
|
checkErrDetails func(err *errs.Error)
|
|
}
|
|
tests := map[string]func() test{
|
|
"fail/token/authorizeRevoke error": func() test {
|
|
return test{
|
|
auth: a,
|
|
ctx: tlsRevokeCtx,
|
|
opts: &RevokeOptions{
|
|
OTT: "foo",
|
|
Serial: "sn",
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
},
|
|
err: errors.New("authority.Revoke; error parsing token"),
|
|
code: http.StatusUnauthorized,
|
|
}
|
|
},
|
|
"fail/nil-db": func() test {
|
|
cl := jose.Claims{
|
|
Subject: "sn",
|
|
Issuer: validIssuer,
|
|
NotBefore: jose.NewNumericDate(now),
|
|
Expiry: jose.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "44",
|
|
}
|
|
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
|
auth: a,
|
|
ctx: tlsRevokeCtx,
|
|
opts: &RevokeOptions{
|
|
Serial: "sn",
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
OTT: raw,
|
|
},
|
|
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")
|
|
},
|
|
}
|
|
},
|
|
"fail/db-revoke": func() test {
|
|
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{
|
|
MUseToken: func(id, tok string) (bool, error) {
|
|
return true, nil
|
|
},
|
|
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
|
return nil, errors.New("not found")
|
|
},
|
|
Err: errors.New("force"),
|
|
}))
|
|
|
|
cl := jose.Claims{
|
|
Subject: "sn",
|
|
Issuer: validIssuer,
|
|
NotBefore: jose.NewNumericDate(now),
|
|
Expiry: jose.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "44",
|
|
}
|
|
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
|
auth: _a,
|
|
ctx: tlsRevokeCtx,
|
|
opts: &RevokeOptions{
|
|
Serial: "sn",
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
OTT: raw,
|
|
},
|
|
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")
|
|
},
|
|
}
|
|
},
|
|
"fail/already-revoked": func() test {
|
|
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{
|
|
MUseToken: func(id, tok string) (bool, error) {
|
|
return true, nil
|
|
},
|
|
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
|
return nil, errors.New("not found")
|
|
},
|
|
Err: db.ErrAlreadyExists,
|
|
}))
|
|
|
|
cl := jose.Claims{
|
|
Subject: "sn",
|
|
Issuer: validIssuer,
|
|
NotBefore: jose.NewNumericDate(now),
|
|
Expiry: jose.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "44",
|
|
}
|
|
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
|
auth: _a,
|
|
ctx: tlsRevokeCtx,
|
|
opts: &RevokeOptions{
|
|
Serial: "sn",
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
OTT: raw,
|
|
},
|
|
err: errors.New("certificate with serial number 'sn' is already 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, WithDatabase(&db.MockAuthDB{
|
|
MUseToken: func(id, tok string) (bool, error) {
|
|
return true, nil
|
|
},
|
|
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
|
return nil, errors.New("not found")
|
|
},
|
|
}))
|
|
|
|
cl := jose.Claims{
|
|
Subject: "sn",
|
|
Issuer: validIssuer,
|
|
NotBefore: jose.NewNumericDate(now),
|
|
Expiry: jose.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "44",
|
|
}
|
|
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
auth: _a,
|
|
ctx: tlsRevokeCtx,
|
|
opts: &RevokeOptions{
|
|
Serial: "sn",
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
OTT: raw,
|
|
},
|
|
}
|
|
},
|
|
"ok/mTLS": func() test {
|
|
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))
|
|
|
|
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
|
auth: _a,
|
|
ctx: tlsRevokeCtx,
|
|
opts: &RevokeOptions{
|
|
Crt: crt,
|
|
Serial: "102012593071130646873265215610956555026",
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
MTLS: true,
|
|
},
|
|
}
|
|
},
|
|
"ok/mTLS-no-provisioner": func() test {
|
|
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))
|
|
|
|
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
|
assert.FatalError(t, err)
|
|
// Filter out provisioner extension.
|
|
for i, ext := range crt.Extensions {
|
|
if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}) {
|
|
crt.Extensions = append(crt.Extensions[:i], crt.Extensions[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
return test{
|
|
auth: _a,
|
|
ctx: tlsRevokeCtx,
|
|
opts: &RevokeOptions{
|
|
Crt: crt,
|
|
Serial: "102012593071130646873265215610956555026",
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
MTLS: true,
|
|
},
|
|
}
|
|
},
|
|
"ok/ACME": func() test {
|
|
_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))
|
|
|
|
crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
|
|
assert.FatalError(t, err)
|
|
|
|
return test{
|
|
auth: _a,
|
|
ctx: tlsRevokeCtx,
|
|
opts: &RevokeOptions{
|
|
Crt: crt,
|
|
Serial: "102012593071130646873265215610956555026",
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
ACME: true,
|
|
},
|
|
}
|
|
},
|
|
"ok/ssh": func() test {
|
|
a := testAuthority(t, WithDatabase(&db.MockAuthDB{
|
|
MRevoke: func(rci *db.RevokedCertificateInfo) error {
|
|
return errors.New("Revoke was called")
|
|
},
|
|
MRevokeSSH: func(rci *db.RevokedCertificateInfo) error {
|
|
return nil
|
|
},
|
|
}))
|
|
|
|
cl := jose.Claims{
|
|
Subject: "sn",
|
|
Issuer: validIssuer,
|
|
NotBefore: jose.NewNumericDate(now),
|
|
Expiry: jose.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: "44",
|
|
}
|
|
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
auth: a,
|
|
ctx: provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod),
|
|
opts: &RevokeOptions{
|
|
Serial: "sn",
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
OTT: raw,
|
|
},
|
|
}
|
|
},
|
|
}
|
|
for name, f := range tests {
|
|
tc := f()
|
|
t.Run(name, func(t *testing.T) {
|
|
if err := tc.auth.Revoke(tc.ctx, tc.opts); err != nil {
|
|
if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
|
|
var sc render.StatusCodedError
|
|
assert.Fatal(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
|
|
|
var ctxErr *errs.Error
|
|
assert.Fatal(t, errors.As(err, &ctxErr), "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"], provisioner.RevokeMethod.String())
|
|
|
|
if tc.checkErrDetails != nil {
|
|
tc.checkErrDetails(ctxErr)
|
|
}
|
|
}
|
|
} else {
|
|
assert.Nil(t, tc.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthority_constraints(t *testing.T) {
|
|
ca, err := minica.New(
|
|
minica.WithIntermediateTemplate(`{
|
|
"subject": {{ toJson .Subject }},
|
|
"keyUsage": ["certSign", "crlSign"],
|
|
"basicConstraints": {
|
|
"isCA": true,
|
|
"maxPathLen": 0
|
|
},
|
|
"nameConstraints": {
|
|
"critical": true,
|
|
"permittedDNSDomains": ["internal.example.org"],
|
|
"excludedDNSDomains": ["internal.example.com"],
|
|
"permittedIPRanges": ["192.168.1.0/24", "192.168.2.1/32"],
|
|
"excludedIPRanges": ["192.168.3.0/24", "192.168.4.0/28"],
|
|
"permittedEmailAddresses": ["root@example.org", "example.org", ".acme.org"],
|
|
"excludedEmailAddresses": ["root@example.com", "example.com", ".acme.com"],
|
|
"permittedURIDomains": ["uuid.example.org", ".acme.org"],
|
|
"excludedURIDomains": ["uuid.example.com", ".acme.com"]
|
|
}
|
|
}`),
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
auth, err := NewEmbedded(WithX509RootCerts(ca.Root), WithX509Signer(ca.Intermediate, ca.Signer))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
signer, err := keyutil.GenerateDefaultSigner()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
sans []string
|
|
wantErr bool
|
|
}{
|
|
{"ok dns", []string{"internal.example.org", "host.internal.example.org"}, false},
|
|
{"ok ip", []string{"192.168.1.10", "192.168.2.1"}, false},
|
|
{"ok email", []string{"root@example.org", "info@example.org", "info@www.acme.org"}, false},
|
|
{"ok uri", []string{"https://uuid.example.org/b908d973-5167-4a62-abe3-6beda358d82a", "https://uuid.acme.org/1724aae1-1bb3-44fb-83c3-9a1a18df67c8"}, false},
|
|
{"fail permitted dns", []string{"internal.acme.org"}, true},
|
|
{"fail excluded dns", []string{"internal.example.com"}, true},
|
|
{"fail permitted ips", []string{"192.168.2.10"}, true},
|
|
{"fail excluded ips", []string{"192.168.3.1"}, true},
|
|
{"fail permitted emails", []string{"root@acme.org"}, true},
|
|
{"fail excluded emails", []string{"root@example.com"}, true},
|
|
{"fail permitted uris", []string{"https://acme.org/uuid/7848819c-9d0b-4e12-bbff-cd66079a3444"}, true},
|
|
{"fail excluded uris", []string{"https://uuid.example.com/d325eda7-6356-4d60-b8f6-3d64724afeb3"}, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
csr, err := x509util.CreateCertificateRequest(tt.sans[0], tt.sans, signer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cert, err := ca.SignCSR(csr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data := x509util.CreateTemplateData(tt.sans[0], tt.sans)
|
|
templateOption, err := provisioner.TemplateOptions(nil, data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = auth.Sign(csr, provisioner.SignOptions{}, templateOption)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Authority.Sign() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
|
|
_, err = auth.Renew(cert)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Authority.Renew() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthority_CRL(t *testing.T) {
|
|
reasonCode := 2
|
|
reason := "bob was let go"
|
|
validIssuer := "step-cli"
|
|
validAudience := testAudiences.Revoke
|
|
now := time.Now().UTC()
|
|
//
|
|
jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
|
|
assert.FatalError(t, err)
|
|
//
|
|
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
|
(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
|
|
assert.FatalError(t, err)
|
|
|
|
crlCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)
|
|
|
|
var crlStore db.CertificateRevocationListInfo
|
|
var revokedList []db.RevokedCertificateInfo
|
|
|
|
type test struct {
|
|
auth *Authority
|
|
ctx context.Context
|
|
expected []string
|
|
err error
|
|
}
|
|
tests := map[string]func() test{
|
|
"fail/empty-crl": func() test {
|
|
a := testAuthority(t, WithDatabase(&db.MockAuthDB{
|
|
MUseToken: func(id, tok string) (bool, error) {
|
|
return true, nil
|
|
},
|
|
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
|
return nil, errors.New("not found")
|
|
},
|
|
MStoreCRL: func(i *db.CertificateRevocationListInfo) error {
|
|
crlStore = *i
|
|
return nil
|
|
},
|
|
MGetCRL: func() (*db.CertificateRevocationListInfo, error) {
|
|
return nil, database.ErrNotFound
|
|
},
|
|
MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {
|
|
return &revokedList, nil
|
|
},
|
|
MRevoke: func(rci *db.RevokedCertificateInfo) error {
|
|
revokedList = append(revokedList, *rci)
|
|
return nil
|
|
},
|
|
}))
|
|
a.config.CRL = &config.CRLConfig{
|
|
Enabled: true,
|
|
}
|
|
|
|
return test{
|
|
auth: a,
|
|
ctx: crlCtx,
|
|
expected: nil,
|
|
err: database.ErrNotFound,
|
|
}
|
|
},
|
|
"ok/crl-full": func() test {
|
|
a := testAuthority(t, WithDatabase(&db.MockAuthDB{
|
|
MUseToken: func(id, tok string) (bool, error) {
|
|
return true, nil
|
|
},
|
|
MGetCertificate: func(sn string) (*x509.Certificate, error) {
|
|
return nil, errors.New("not found")
|
|
},
|
|
MStoreCRL: func(i *db.CertificateRevocationListInfo) error {
|
|
crlStore = *i
|
|
return nil
|
|
},
|
|
MGetCRL: func() (*db.CertificateRevocationListInfo, error) {
|
|
return &crlStore, nil
|
|
},
|
|
MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {
|
|
return &revokedList, nil
|
|
},
|
|
MRevoke: func(rci *db.RevokedCertificateInfo) error {
|
|
revokedList = append(revokedList, *rci)
|
|
return nil
|
|
},
|
|
}))
|
|
a.config.CRL = &config.CRLConfig{
|
|
Enabled: true,
|
|
GenerateOnRevoke: true,
|
|
}
|
|
|
|
var ex []string
|
|
|
|
for i := 0; i < 100; i++ {
|
|
sn := fmt.Sprintf("%v", i)
|
|
|
|
cl := jose.Claims{
|
|
Subject: fmt.Sprintf("sn-%v", i),
|
|
Issuer: validIssuer,
|
|
NotBefore: jose.NewNumericDate(now),
|
|
Expiry: jose.NewNumericDate(now.Add(time.Minute)),
|
|
Audience: validAudience,
|
|
ID: sn,
|
|
}
|
|
raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
|
|
assert.FatalError(t, err)
|
|
err = a.Revoke(crlCtx, &RevokeOptions{
|
|
Serial: sn,
|
|
ReasonCode: reasonCode,
|
|
Reason: reason,
|
|
OTT: raw,
|
|
})
|
|
|
|
assert.FatalError(t, err)
|
|
|
|
ex = append(ex, sn)
|
|
}
|
|
|
|
return test{
|
|
auth: a,
|
|
ctx: crlCtx,
|
|
expected: ex,
|
|
}
|
|
},
|
|
}
|
|
for name, f := range tests {
|
|
tc := f()
|
|
t.Run(name, func(t *testing.T) {
|
|
if crlBytes, err := tc.auth.GetCertificateRevocationList(); err == nil {
|
|
crl, parseErr := x509.ParseCRL(crlBytes)
|
|
if parseErr != nil {
|
|
t.Errorf("x509.ParseCertificateRequest() error = %v, wantErr %v", parseErr, nil)
|
|
return
|
|
}
|
|
|
|
var cmpList []string
|
|
for _, c := range crl.TBSCertList.RevokedCertificates {
|
|
cmpList = append(cmpList, c.SerialNumber.String())
|
|
}
|
|
|
|
assert.Equals(t, cmpList, tc.expected)
|
|
} else {
|
|
assert.NotNil(t, tc.err, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|