forked from TrueCloudLab/certificates
Create a method to initialize the authority without a config file.
When the CA is embedded in a third party product like Caddy, the config needed to use placeholders to be valid. This change adds a new method `NewEmbeddedAuthority` that allows to create an authority with the given options, the minimum options are a root and intermediate certificate, and the intermediate key. Fixes #218
This commit is contained in:
parent
ca0861bf17
commit
824374bde0
4 changed files with 192 additions and 4 deletions
|
@ -85,6 +85,45 @@ func New(config *Config, opts ...Option) (*Authority, error) {
|
|||
return a, nil
|
||||
}
|
||||
|
||||
// NewEmbeddedAuthority initializes an authority that can be embedded in a
|
||||
// different project without the limitations of the config.
|
||||
func NewEmbeddedAuthority(opts ...Option) (*Authority, error) {
|
||||
config := &Config{
|
||||
DNSNames: []string{"localhost", "127.0.0.1", "::1"},
|
||||
AuthorityConfig: defaultAuthConfig,
|
||||
TLS: &DefaultTLSOptions,
|
||||
}
|
||||
a := &Authority{
|
||||
config: config,
|
||||
certificates: new(sync.Map),
|
||||
provisioners: provisioner.NewCollection(config.getAudiences()),
|
||||
}
|
||||
|
||||
// Apply options.
|
||||
for _, fn := range opts {
|
||||
if err := fn(a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required options
|
||||
switch {
|
||||
case len(a.rootX509Certs) == 0 && a.config.Root.HasEmpties():
|
||||
return nil, errors.New("cannot create an authority without a root certificate")
|
||||
case a.x509Issuer == nil && a.config.IntermediateCert == "":
|
||||
return nil, errors.New("cannot create an authority without an issuer certificate")
|
||||
case a.x509Signer == nil && a.config.IntermediateKey == "":
|
||||
return nil, errors.New("cannot create an authority without an issuer signer")
|
||||
}
|
||||
|
||||
// Initialize authority from options or configuration.
|
||||
if err := a.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// init performs validation and initializes the fields of an Authority struct.
|
||||
func (a *Authority) init() error {
|
||||
// Check if handler has already been validated/initialized.
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
@ -10,6 +15,7 @@ import (
|
|||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/cli/crypto/pemutil"
|
||||
stepJOSE "github.com/smallstep/cli/jose"
|
||||
)
|
||||
|
||||
|
@ -182,3 +188,121 @@ func TestAuthority_GetDatabase(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEmbeddedAuthority(t *testing.T) {
|
||||
caPEM, err := ioutil.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 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 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 := NewEmbeddedAuthority(tt.args.opts...)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewEmbeddedAuthority() 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.x509Signer)
|
||||
assert.NotNil(t, got.x509Issuer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEmbeddedAuthority_Sign(t *testing.T) {
|
||||
caPEM, err := ioutil.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 := NewEmbeddedAuthority(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.Options{})
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, []string{"foo.bar.zar"}, cert[0].DNSNames)
|
||||
assert.Equals(t, crt, cert[1])
|
||||
}
|
||||
|
||||
func TestNewEmbeddedAuthority_GetTLSCertificate(t *testing.T) {
|
||||
caPEM, err := ioutil.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 := NewEmbeddedAuthority(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")))
|
||||
}
|
||||
|
|
|
@ -75,6 +75,15 @@ type AuthConfig struct {
|
|||
Backdate *provisioner.Duration `json:"backdate,omitempty"`
|
||||
}
|
||||
|
||||
// defaultAuthConfig used when skipping validation.
|
||||
var defaultAuthConfig = &AuthConfig{
|
||||
Provisioners: provisioner.List{},
|
||||
Template: &x509util.ASN1DN{},
|
||||
Backdate: &provisioner.Duration{
|
||||
Duration: defaultBackdate,
|
||||
},
|
||||
}
|
||||
|
||||
// Validate validates the authority configuration.
|
||||
func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
|
||||
if c == nil {
|
||||
|
@ -93,7 +102,7 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
|
|||
}
|
||||
|
||||
if c.Template == nil {
|
||||
c.Template = &x509util.ASN1DN{}
|
||||
c.Template = defaultAuthConfig.Template
|
||||
}
|
||||
|
||||
if c.Backdate != nil {
|
||||
|
@ -101,9 +110,7 @@ func (c *AuthConfig) Validate(audiences provisioner.Audiences) error {
|
|||
return errors.New("authority.backdate cannot be less than 0")
|
||||
}
|
||||
} else {
|
||||
c.Backdate = &provisioner.Duration{
|
||||
Duration: defaultBackdate,
|
||||
}
|
||||
c.Backdate = defaultAuthConfig.Backdate
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -17,6 +17,24 @@ import (
|
|||
// Option sets options to the Authority.
|
||||
type Option func(*Authority) error
|
||||
|
||||
// WithConfig replaces the current config with the given one. No validation is
|
||||
// performed in the given value.
|
||||
func WithConfig(config *Config) Option {
|
||||
return func(a *Authority) error {
|
||||
a.config = config
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfigFile reads the given filename as a configuration file and replaces
|
||||
// the current one. No validation is performed in the given configuration.
|
||||
func WithConfigFile(filename string) Option {
|
||||
return func(a *Authority) (err error) {
|
||||
a.config, err = LoadConfiguration(filename)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// WithDatabase sets an already initialized authority database to a new
|
||||
// authority. This option is intended to be use on graceful reloads.
|
||||
func WithDatabase(db db.AuthDB) Option {
|
||||
|
|
Loading…
Reference in a new issue