certificates/authority/authority_test.go
Mariano Cano 8200d19894
Improve CRL implementation
This commit adds some changes to PR #731, some of them are:
- Add distribution point to the CRL
- Properly stop the goroutine that generates the CRLs
- CRL config validation
- Remove expired certificates from the CRL
- Require enable set to true to generate a CRL

This last point is the principal change in behaviour from the previous
implementation. The CRL will not be generated if it's not enabled, and
if it is enabled it will always be regenerated at some point, not only
if there is a revocation.
2022-10-26 18:55:24 -07:00

452 lines
13 KiB
Go

package authority
import (
"crypto"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"net"
"os"
"reflect"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
)
func testAuthority(t *testing.T, opts ...Option) *Authority {
maxjwk, err := jose.ReadKey("testdata/secrets/max_pub.jwk")
assert.FatalError(t, err)
clijwk, err := jose.ReadKey("testdata/secrets/step_cli_key_pub.jwk")
assert.FatalError(t, err)
disableRenewal := true
enableSSHCA := true
p := provisioner.List{
&provisioner.JWK{
Name: "Max",
Type: "JWK",
Key: maxjwk,
},
&provisioner.JWK{
Name: "step-cli",
Type: "JWK",
Key: clijwk,
Claims: &provisioner.Claims{
EnableSSHCA: &enableSSHCA,
},
},
&provisioner.JWK{
Name: "dev",
Type: "JWK",
Key: maxjwk,
Claims: &provisioner.Claims{
DisableRenewal: &disableRenewal,
},
},
&provisioner.JWK{
Name: "renew_disabled",
Type: "JWK",
Key: maxjwk,
Claims: &provisioner.Claims{
DisableRenewal: &disableRenewal,
},
},
&provisioner.SSHPOP{
Name: "sshpop",
Type: "SSHPOP",
Claims: &provisioner.Claims{
EnableSSHCA: &enableSSHCA,
},
},
}
c := &Config{
Address: "127.0.0.1:443",
Root: []string{"testdata/certs/root_ca.crt"},
IntermediateCert: "testdata/certs/intermediate_ca.crt",
IntermediateKey: "testdata/secrets/intermediate_ca_key",
SSH: &SSHConfig{
HostKey: "testdata/secrets/ssh_host_ca_key",
UserKey: "testdata/secrets/ssh_user_ca_key",
},
DNSNames: []string{"example.com"},
Password: "pass",
AuthorityConfig: &AuthConfig{
Provisioners: p,
},
}
a, err := New(c, opts...)
assert.FatalError(t, err)
// Avoid errors when test tokens are created before the test authority. This
// happens in some tests where we re-create the same authority to test
// special cases without re-creating the token.
a.startTime = a.startTime.Add(-1 * time.Minute)
return a
}
func TestAuthorityNew(t *testing.T) {
type newTest struct {
config *Config
err error
}
tests := map[string]func(t *testing.T) *newTest{
"ok": func(t *testing.T) *newTest {
c, err := LoadConfiguration("../ca/testdata/ca.json")
assert.FatalError(t, err)
return &newTest{
config: c,
}
},
"fail bad root": func(t *testing.T) *newTest {
c, err := LoadConfiguration("../ca/testdata/ca.json")
assert.FatalError(t, err)
c.Root = []string{"foo"}
return &newTest{
config: c,
err: errors.New("error reading foo: no such file or directory"),
}
},
"fail bad password": func(t *testing.T) *newTest {
c, err := LoadConfiguration("../ca/testdata/ca.json")
assert.FatalError(t, err)
c.Password = "wrong"
return &newTest{
config: c,
err: errors.New("error decrypting ../ca/testdata/secrets/intermediate_ca_key: x509: decryption password incorrect"),
}
},
"fail loading CA cert": func(t *testing.T) *newTest {
c, err := LoadConfiguration("../ca/testdata/ca.json")
assert.FatalError(t, err)
c.IntermediateCert = "wrong"
return &newTest{
config: c,
err: errors.New("error reading wrong: no such file or directory"),
}
},
}
for name, genTestCase := range tests {
t.Run(name, func(t *testing.T) {
tc := genTestCase(t)
auth, err := New(tc.config)
if err != nil {
if assert.NotNil(t, tc.err) {
assert.HasPrefix(t, err.Error(), tc.err.Error())
}
} else {
if assert.Nil(t, tc.err) {
sum := sha256.Sum256(auth.rootX509Certs[0].Raw)
root, ok := auth.certificates.Load(hex.EncodeToString(sum[:]))
assert.Fatal(t, ok)
assert.Equals(t, auth.rootX509Certs[0], root)
assert.True(t, auth.initOnce)
assert.NotNil(t, auth.x509CAService)
for _, p := range tc.config.AuthorityConfig.Provisioners {
var _p provisioner.Interface
_p, ok = auth.provisioners.Load(p.GetID())
assert.True(t, ok)
assert.Equals(t, p, _p)
var kid, encryptedKey string
if kid, encryptedKey, ok = p.GetEncryptedKey(); ok {
var key string
key, ok = auth.provisioners.LoadEncryptedKey(kid)
assert.True(t, ok)
assert.Equals(t, encryptedKey, key)
}
}
// sanity check
_, ok = auth.provisioners.Load("fooo")
assert.False(t, ok)
}
}
})
}
}
func TestAuthority_GetDatabase(t *testing.T) {
auth := testAuthority(t)
authWithDatabase, err := New(auth.config, WithDatabase(auth.db))
assert.FatalError(t, err)
tests := []struct {
name string
auth *Authority
want db.AuthDB
}{
{"ok", auth, auth.db},
{"ok WithDatabase", authWithDatabase, auth.db},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.auth.GetDatabase(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Authority.GetDatabase() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewEmbedded(t *testing.T) {
caPEM, err := os.ReadFile("testdata/certs/root_ca.crt")
assert.FatalError(t, err)
crt, err := pemutil.ReadCertificate("testdata/certs/intermediate_ca.crt")
assert.FatalError(t, err)
key, err := pemutil.Read("testdata/secrets/intermediate_ca_key", pemutil.WithPassword([]byte("pass")))
assert.FatalError(t, err)
type args struct {
opts []Option
}
tests := []struct {
name string
args args
wantErr bool
}{
{"ok", args{[]Option{WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer))}}, false},
{"ok empty config", args{[]Option{WithConfig(&Config{}), WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer))}}, false},
{"ok config file", args{[]Option{WithConfigFile("../ca/testdata/ca.json")}}, false},
{"ok config", args{[]Option{WithConfig(&Config{
Root: []string{"testdata/certs/root_ca.crt"},
IntermediateCert: "testdata/certs/intermediate_ca.crt",
IntermediateKey: "testdata/secrets/intermediate_ca_key",
Password: "pass",
AuthorityConfig: &AuthConfig{},
})}}, false},
{"fail options", args{[]Option{WithX509RootBundle([]byte("bad data"))}}, true},
{"fail missing config", args{[]Option{WithConfig(nil), WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer))}}, true},
{"fail missing root", args{[]Option{WithX509Signer(crt, key.(crypto.Signer))}}, true},
{"fail missing signer", args{[]Option{WithX509RootBundle(caPEM)}}, true},
{"fail missing root file", args{[]Option{WithConfig(&Config{
IntermediateCert: "testdata/certs/intermediate_ca.crt",
IntermediateKey: "testdata/secrets/intermediate_ca_key",
Password: "pass",
AuthorityConfig: &AuthConfig{},
})}}, true},
{"fail missing issuer", args{[]Option{WithConfig(&Config{
Root: []string{"testdata/certs/root_ca.crt"},
IntermediateKey: "testdata/secrets/intermediate_ca_key",
Password: "pass",
AuthorityConfig: &AuthConfig{},
})}}, true},
{"fail missing signer", args{[]Option{WithConfig(&Config{
Root: []string{"testdata/certs/root_ca.crt"},
IntermediateCert: "testdata/certs/intermediate_ca.crt",
Password: "pass",
AuthorityConfig: &AuthConfig{},
})}}, true},
{"fail bad password", args{[]Option{WithConfig(&Config{
Root: []string{"testdata/certs/root_ca.crt"},
IntermediateCert: "testdata/certs/intermediate_ca.crt",
IntermediateKey: "testdata/secrets/intermediate_ca_key",
Password: "bad",
AuthorityConfig: &AuthConfig{},
})}}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewEmbedded(tt.args.opts...)
if (err != nil) != tt.wantErr {
t.Errorf("NewEmbedded() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil {
assert.True(t, got.initOnce)
assert.NotNil(t, got.rootX509Certs)
assert.NotNil(t, got.x509CAService)
}
})
}
}
func TestNewEmbedded_Sign(t *testing.T) {
caPEM, err := os.ReadFile("testdata/certs/root_ca.crt")
assert.FatalError(t, err)
crt, err := pemutil.ReadCertificate("testdata/certs/intermediate_ca.crt")
assert.FatalError(t, err)
key, err := pemutil.Read("testdata/secrets/intermediate_ca_key", pemutil.WithPassword([]byte("pass")))
assert.FatalError(t, err)
a, err := NewEmbedded(WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer)))
assert.FatalError(t, err)
// Sign
cr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
DNSNames: []string{"foo.bar.zar"},
}, key)
assert.FatalError(t, err)
csr, err := x509.ParseCertificateRequest(cr)
assert.FatalError(t, err)
cert, err := a.Sign(csr, provisioner.SignOptions{})
assert.FatalError(t, err)
assert.Equals(t, []string{"foo.bar.zar"}, cert[0].DNSNames)
assert.Equals(t, crt, cert[1])
}
func TestNewEmbedded_GetTLSCertificate(t *testing.T) {
caPEM, err := os.ReadFile("testdata/certs/root_ca.crt")
assert.FatalError(t, err)
crt, err := pemutil.ReadCertificate("testdata/certs/intermediate_ca.crt")
assert.FatalError(t, err)
key, err := pemutil.Read("testdata/secrets/intermediate_ca_key", pemutil.WithPassword([]byte("pass")))
assert.FatalError(t, err)
a, err := NewEmbedded(WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer)))
assert.FatalError(t, err)
// GetTLSCertificate
cert, err := a.GetTLSCertificate()
assert.FatalError(t, err)
assert.Equals(t, []string{"localhost"}, cert.Leaf.DNSNames)
assert.True(t, cert.Leaf.IPAddresses[0].Equal(net.ParseIP("127.0.0.1")))
assert.True(t, cert.Leaf.IPAddresses[1].Equal(net.ParseIP("::1")))
}
func TestAuthority_CloseForReload(t *testing.T) {
tests := []struct {
name string
auth *Authority
}{
{"ok", testAuthority(t)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.auth.CloseForReload()
})
}
}
func testScepAuthority(t *testing.T, opts ...Option) *Authority {
p := provisioner.List{
&provisioner.SCEP{
Name: "scep1",
Type: "SCEP",
},
}
c := &Config{
Address: "127.0.0.1:8443",
InsecureAddress: "127.0.0.1:8080",
Root: []string{"testdata/scep/root.crt"},
IntermediateCert: "testdata/scep/intermediate.crt",
IntermediateKey: "testdata/scep/intermediate.key",
DNSNames: []string{"example.com"},
Password: "pass",
AuthorityConfig: &AuthConfig{
Provisioners: p,
},
}
a, err := New(c, opts...)
assert.FatalError(t, err)
return a
}
func TestAuthority_GetSCEPService(t *testing.T) {
_ = testScepAuthority(t)
p := provisioner.List{
&provisioner.SCEP{
Name: "scep1",
Type: "SCEP",
},
}
type fields struct {
config *Config
}
tests := []struct {
name string
fields fields
wantService bool
wantErr bool
}{
{
name: "ok",
fields: fields{
config: &Config{
Address: "127.0.0.1:8443",
InsecureAddress: "127.0.0.1:8080",
Root: []string{"testdata/scep/root.crt"},
IntermediateCert: "testdata/scep/intermediate.crt",
IntermediateKey: "testdata/scep/intermediate.key",
DNSNames: []string{"example.com"},
Password: "pass",
AuthorityConfig: &AuthConfig{
Provisioners: p,
},
},
},
wantService: true,
wantErr: false,
},
{
name: "wrong password",
fields: fields{
config: &Config{
Address: "127.0.0.1:8443",
InsecureAddress: "127.0.0.1:8080",
Root: []string{"testdata/scep/root.crt"},
IntermediateCert: "testdata/scep/intermediate.crt",
IntermediateKey: "testdata/scep/intermediate.key",
DNSNames: []string{"example.com"},
Password: "wrongpass",
AuthorityConfig: &AuthConfig{
Provisioners: p,
},
},
},
wantService: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a, err := New(tt.fields.config)
if (err != nil) != tt.wantErr {
t.Errorf("Authority.New(), error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantService {
if got := a.GetSCEPService(); (got != nil) != tt.wantService {
t.Errorf("Authority.GetSCEPService() = %v, wantService %v", got, tt.wantService)
}
}
})
}
}
func TestAuthority_GetID(t *testing.T) {
type fields struct {
authorityID string
}
tests := []struct {
name string
fields fields
want string
}{
{"ok", fields{""}, "00000000-0000-0000-0000-000000000000"},
{"ok with id", fields{"10b9a431-ed3b-4a5f-abee-ec35119b65e7"}, "10b9a431-ed3b-4a5f-abee-ec35119b65e7"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Authority{
config: &config.Config{
AuthorityConfig: &config.AuthConfig{
AuthorityID: tt.fields.authorityID,
},
},
}
if got := a.GetID(); got != tt.want {
t.Errorf("Authority.GetID() = %v, want %v", got, tt.want)
}
})
}
}