mirror of
https://github.com/smallstep/certificates.git
synced 2024-11-17 15:29:21 +00:00
408 lines
12 KiB
Go
408 lines
12 KiB
Go
|
package authority
|
||
|
|
||
|
import (
|
||
|
"crypto/rand"
|
||
|
"crypto/sha1"
|
||
|
"crypto/x509"
|
||
|
"crypto/x509/pkix"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
"github.com/smallstep/assert"
|
||
|
"github.com/smallstep/ca-component/api"
|
||
|
"github.com/smallstep/cli/crypto/keys"
|
||
|
"github.com/smallstep/cli/crypto/tlsutil"
|
||
|
"github.com/smallstep/cli/crypto/x509util"
|
||
|
)
|
||
|
|
||
|
func getCSR(t *testing.T, priv interface{}) *x509.CertificateRequest {
|
||
|
_csr := &x509.CertificateRequest{
|
||
|
Subject: pkix.Name{CommonName: "test"},
|
||
|
DNSNames: []string{"test.smallstep.com"},
|
||
|
}
|
||
|
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv)
|
||
|
assert.FatalError(t, err)
|
||
|
csr, err := x509.ParseCertificateRequest(csrBytes)
|
||
|
assert.FatalError(t, err)
|
||
|
return csr
|
||
|
}
|
||
|
|
||
|
func TestSign(t *testing.T) {
|
||
|
pub, priv, err := keys.GenerateDefaultKeyPair()
|
||
|
assert.FatalError(t, err)
|
||
|
|
||
|
a := testAuthority(t)
|
||
|
assert.FatalError(t, err)
|
||
|
a.config.AuthorityConfig.Template = &x509util.ASN1DN{
|
||
|
Country: "Tazmania",
|
||
|
Organization: "Acme Co",
|
||
|
Locality: "Landscapes",
|
||
|
Province: "Sudden Cliffs",
|
||
|
StreetAddress: "TNT",
|
||
|
CommonName: "test",
|
||
|
}
|
||
|
|
||
|
now := time.Now()
|
||
|
|
||
|
type signTest struct {
|
||
|
auth *Authority
|
||
|
csr *x509.CertificateRequest
|
||
|
opts api.SignOptions
|
||
|
claims []api.Claim
|
||
|
err *apiError
|
||
|
}
|
||
|
tests := map[string]func(*testing.T) *signTest{
|
||
|
"fail-validate-claims": func(t *testing.T) *signTest {
|
||
|
csr := getCSR(t, priv)
|
||
|
return &signTest{
|
||
|
auth: a,
|
||
|
csr: csr,
|
||
|
opts: api.SignOptions{
|
||
|
NotBefore: now,
|
||
|
NotAfter: now.Add(time.Minute * 5),
|
||
|
},
|
||
|
claims: []api.Claim{&commonNameClaim{"foo"}},
|
||
|
err: &apiError{errors.New("common name claim failed - got test, want foo"),
|
||
|
http.StatusUnauthorized, context{}},
|
||
|
}
|
||
|
},
|
||
|
"fail-convert-stepCSR": func(t *testing.T) *signTest {
|
||
|
csr := getCSR(t, priv)
|
||
|
csr.Raw = []byte("foo")
|
||
|
return &signTest{
|
||
|
auth: a,
|
||
|
csr: csr,
|
||
|
opts: api.SignOptions{
|
||
|
NotBefore: now,
|
||
|
NotAfter: now.Add(time.Minute * 5),
|
||
|
},
|
||
|
claims: []api.Claim{&commonNameClaim{"test"}},
|
||
|
err: &apiError{errors.New("error converting x509 csr to stepx509 csr"),
|
||
|
http.StatusInternalServerError, context{}},
|
||
|
}
|
||
|
},
|
||
|
"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,
|
||
|
opts: api.SignOptions{
|
||
|
NotBefore: now,
|
||
|
NotAfter: now.Add(time.Minute * 5),
|
||
|
},
|
||
|
claims: []api.Claim{&commonNameClaim{"test"}},
|
||
|
err: &apiError{errors.New("default ASN1DN template cannot be nil"),
|
||
|
http.StatusInternalServerError, context{}},
|
||
|
}
|
||
|
},
|
||
|
"fail-create-cert": func(t *testing.T) *signTest {
|
||
|
_a := testAuthority(t)
|
||
|
_a.intermediateIdentity.Key = nil
|
||
|
csr := getCSR(t, priv)
|
||
|
return &signTest{
|
||
|
auth: _a,
|
||
|
csr: csr,
|
||
|
opts: api.SignOptions{
|
||
|
NotBefore: now,
|
||
|
NotAfter: now.Add(time.Minute * 5),
|
||
|
},
|
||
|
claims: []api.Claim{&commonNameClaim{"test"}},
|
||
|
err: &apiError{errors.New("error creating new leaf certificate from input csr"),
|
||
|
http.StatusInternalServerError, context{}},
|
||
|
}
|
||
|
},
|
||
|
"success": func(t *testing.T) *signTest {
|
||
|
csr := getCSR(t, priv)
|
||
|
return &signTest{
|
||
|
auth: a,
|
||
|
csr: csr,
|
||
|
opts: api.SignOptions{
|
||
|
NotBefore: now,
|
||
|
NotAfter: now.Add(time.Minute * 5),
|
||
|
},
|
||
|
claims: []api.Claim{&commonNameClaim{"test"}},
|
||
|
}
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for name, genTestCase := range tests {
|
||
|
t.Run(name, func(t *testing.T) {
|
||
|
tc := genTestCase(t)
|
||
|
|
||
|
leaf, intermediate, err := tc.auth.Sign(tc.csr, tc.opts, tc.claims...)
|
||
|
if err != nil {
|
||
|
if assert.NotNil(t, tc.err) {
|
||
|
switch v := err.(type) {
|
||
|
case *apiError:
|
||
|
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
|
||
|
assert.Equals(t, v.code, tc.err.code)
|
||
|
assert.Equals(t, v.context, tc.err.context)
|
||
|
default:
|
||
|
t.Errorf("unexpected error type: %T", v)
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
if assert.Nil(t, tc.err) {
|
||
|
assert.Equals(t, leaf.NotBefore, tc.opts.NotBefore.UTC().Truncate(time.Second))
|
||
|
assert.Equals(t, leaf.NotAfter, tc.opts.NotAfter.UTC().Truncate(time.Second))
|
||
|
tmplt := a.config.AuthorityConfig.Template
|
||
|
assert.Equals(t, fmt.Sprintf("%v", leaf.Subject),
|
||
|
fmt.Sprintf("%v", &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,
|
||
|
}))
|
||
|
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"})
|
||
|
|
||
|
pubBytes, err := x509.MarshalPKIXPublicKey(pub)
|
||
|
assert.FatalError(t, err)
|
||
|
hash := sha1.Sum(pubBytes)
|
||
|
assert.Equals(t, leaf.SubjectKeyId, hash[:])
|
||
|
|
||
|
assert.Equals(t, leaf.AuthorityKeyId, a.intermediateIdentity.Crt.SubjectKeyId)
|
||
|
|
||
|
realIntermediate, err := x509.ParseCertificate(a.intermediateIdentity.Crt.Raw)
|
||
|
assert.FatalError(t, err)
|
||
|
assert.Equals(t, intermediate, realIntermediate)
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestRenew(t *testing.T) {
|
||
|
pub, _, err := keys.GenerateDefaultKeyPair()
|
||
|
assert.FatalError(t, err)
|
||
|
|
||
|
a := testAuthority(t)
|
||
|
a.config.AuthorityConfig.Template = &x509util.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
|
||
|
so := &api.SignOptions{
|
||
|
NotBefore: nb1,
|
||
|
NotAfter: na1,
|
||
|
}
|
||
|
|
||
|
leaf, err := x509util.NewLeafProfile("renew", a.intermediateIdentity.Crt,
|
||
|
a.intermediateIdentity.Key,
|
||
|
x509util.WithNotBeforeAfter(so.NotBefore, so.NotAfter),
|
||
|
withDefaultASN1DN(a.config.AuthorityConfig.Template),
|
||
|
x509util.WithPublicKey(pub), x509util.WithHosts("test.smallstep.com,test"))
|
||
|
assert.FatalError(t, err)
|
||
|
crtBytes, err := leaf.CreateCertificate()
|
||
|
assert.FatalError(t, err)
|
||
|
crt, err := x509.ParseCertificate(crtBytes)
|
||
|
assert.FatalError(t, err)
|
||
|
|
||
|
type renewTest struct {
|
||
|
auth *Authority
|
||
|
crt *x509.Certificate
|
||
|
err *apiError
|
||
|
}
|
||
|
tests := map[string]func() (*renewTest, error){
|
||
|
"fail-conversion-stepx509": func() (*renewTest, error) {
|
||
|
return &renewTest{
|
||
|
crt: &x509.Certificate{Raw: []byte("foo")},
|
||
|
err: &apiError{errors.New("error converting x509.Certificate to stepx509.Certificate"),
|
||
|
http.StatusInternalServerError, context{}},
|
||
|
}, nil
|
||
|
},
|
||
|
"fail-create-cert": func() (*renewTest, error) {
|
||
|
_a := testAuthority(t)
|
||
|
_a.intermediateIdentity.Key = nil
|
||
|
return &renewTest{
|
||
|
auth: _a,
|
||
|
crt: crt,
|
||
|
err: &apiError{errors.New("error renewing certificate from existing server certificate"),
|
||
|
http.StatusInternalServerError, context{}},
|
||
|
}, nil
|
||
|
},
|
||
|
"success": func() (*renewTest, error) {
|
||
|
return &renewTest{
|
||
|
crt: crt,
|
||
|
}, nil
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for name, genTestCase := range tests {
|
||
|
t.Run(name, func(t *testing.T) {
|
||
|
tc, err := genTestCase()
|
||
|
assert.FatalError(t, err)
|
||
|
|
||
|
var leaf, intermediate *x509.Certificate
|
||
|
if tc.auth != nil {
|
||
|
leaf, intermediate, err = tc.auth.Renew(tc.crt)
|
||
|
} else {
|
||
|
leaf, intermediate, err = a.Renew(tc.crt)
|
||
|
}
|
||
|
if err != nil {
|
||
|
if assert.NotNil(t, tc.err) {
|
||
|
switch v := err.(type) {
|
||
|
case *apiError:
|
||
|
assert.HasPrefix(t, v.err.Error(), tc.err.Error())
|
||
|
assert.Equals(t, v.code, tc.err.code)
|
||
|
assert.Equals(t, v.context, tc.err.context)
|
||
|
default:
|
||
|
t.Errorf("unexpected error type: %T", v)
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
if assert.Nil(t, tc.err) {
|
||
|
assert.Equals(t, leaf.NotAfter.Sub(leaf.NotBefore), crt.NotAfter.Sub(crt.NotBefore))
|
||
|
|
||
|
assert.True(t, leaf.NotBefore.After(now.Add(-time.Minute)))
|
||
|
assert.True(t, leaf.NotBefore.Before(now.Add(time.Minute)))
|
||
|
|
||
|
expiry := now.Add(time.Minute * 7)
|
||
|
assert.True(t, leaf.NotAfter.After(expiry.Add(-time.Minute)))
|
||
|
assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Minute)))
|
||
|
|
||
|
tmplt := a.config.AuthorityConfig.Template
|
||
|
assert.Equals(t, fmt.Sprintf("%v", leaf.Subject),
|
||
|
fmt.Sprintf("%v", &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,
|
||
|
}))
|
||
|
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"})
|
||
|
|
||
|
pubBytes, err := x509.MarshalPKIXPublicKey(pub)
|
||
|
assert.FatalError(t, err)
|
||
|
hash := sha1.Sum(pubBytes)
|
||
|
assert.Equals(t, leaf.SubjectKeyId, hash[:])
|
||
|
|
||
|
assert.Equals(t, leaf.AuthorityKeyId, a.intermediateIdentity.Crt.SubjectKeyId)
|
||
|
|
||
|
realIntermediate, err := x509.ParseCertificate(a.intermediateIdentity.Crt.Raw)
|
||
|
assert.FatalError(t, err)
|
||
|
assert.Equals(t, intermediate, realIntermediate)
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestGetMinDuration(t *testing.T) {
|
||
|
type renewTest struct {
|
||
|
auth *Authority
|
||
|
d time.Duration
|
||
|
}
|
||
|
tests := map[string]func() (*renewTest, error){
|
||
|
"default": func() (*renewTest, error) {
|
||
|
a := testAuthority(t)
|
||
|
return &renewTest{auth: a, d: time.Minute * 5}, nil
|
||
|
},
|
||
|
"non-default": func() (*renewTest, error) {
|
||
|
a := testAuthority(t)
|
||
|
a.config.AuthorityConfig.MinCertDuration = &duration{time.Minute * 7}
|
||
|
return &renewTest{auth: a, d: time.Minute * 7}, nil
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for name, genTestCase := range tests {
|
||
|
t.Run(name, func(t *testing.T) {
|
||
|
tc, err := genTestCase()
|
||
|
assert.FatalError(t, err)
|
||
|
|
||
|
d := tc.auth.GetMinDuration()
|
||
|
assert.Equals(t, d, tc.d)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestGetMaxDuration(t *testing.T) {
|
||
|
type renewTest struct {
|
||
|
auth *Authority
|
||
|
d time.Duration
|
||
|
}
|
||
|
tests := map[string]func() (*renewTest, error){
|
||
|
"default": func() (*renewTest, error) {
|
||
|
a := testAuthority(t)
|
||
|
return &renewTest{auth: a, d: time.Hour * 24}, nil
|
||
|
},
|
||
|
"non-default": func() (*renewTest, error) {
|
||
|
a := testAuthority(t)
|
||
|
a.config.AuthorityConfig.MaxCertDuration = &duration{time.Minute * 7}
|
||
|
return &renewTest{auth: a, d: time.Minute * 7}, nil
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for name, genTestCase := range tests {
|
||
|
t.Run(name, func(t *testing.T) {
|
||
|
tc, err := genTestCase()
|
||
|
assert.FatalError(t, err)
|
||
|
|
||
|
d := tc.auth.GetMaxDuration()
|
||
|
assert.Equals(t, d, tc.d)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestGetTLSOptions(t *testing.T) {
|
||
|
type renewTest struct {
|
||
|
auth *Authority
|
||
|
opts *tlsutil.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 = &tlsutil.TLSOptions{
|
||
|
CipherSuites: x509util.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)
|
||
|
})
|
||
|
}
|
||
|
}
|