forked from TrueCloudLab/certificates
parent
89b6aa924a
commit
debe565e42
4 changed files with 154 additions and 13 deletions
|
@ -1,6 +1,7 @@
|
|||
package authority
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
|
@ -24,6 +25,7 @@ import (
|
|||
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
|
||||
"github.com/smallstep/certificates/authority/administrator"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/internal/constraints"
|
||||
"github.com/smallstep/certificates/authority/policy"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/cas"
|
||||
|
@ -52,6 +54,7 @@ type Authority struct {
|
|||
rootX509Certs []*x509.Certificate
|
||||
rootX509CertPool *x509.CertPool
|
||||
federatedX509Certs []*x509.Certificate
|
||||
intermediateX509Certs []*x509.Certificate
|
||||
certificates *sync.Map
|
||||
x509Enforcers []provisioner.CertificateEnforcer
|
||||
|
||||
|
@ -80,9 +83,8 @@ type Authority struct {
|
|||
authorizeRenewFunc provisioner.AuthorizeRenewFunc
|
||||
authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc
|
||||
|
||||
// Constraint engine
|
||||
|
||||
// Policy engines
|
||||
// Constraints and Policy engines
|
||||
constraintsEngine *constraints.Engine
|
||||
policyEngine *policy.Engine
|
||||
|
||||
adminMutex sync.RWMutex
|
||||
|
@ -375,6 +377,11 @@ func (a *Authority) init() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If not defined with an option, add intermediates to the the list
|
||||
// of used for constraints purposes.
|
||||
if len(a.intermediateX509Certs) == 0 {
|
||||
a.intermediateX509Certs = append(a.intermediateX509Certs, options.CertificateChain...)
|
||||
}
|
||||
}
|
||||
a.x509CAService, err = cas.New(ctx, options)
|
||||
if err != nil {
|
||||
|
@ -612,6 +619,21 @@ func (a *Authority) init() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Load X509 constraints engine.
|
||||
//
|
||||
// This is currently only available in CA mode.
|
||||
if size := len(a.intermediateX509Certs); size > 0 {
|
||||
last := a.intermediateX509Certs[size-1]
|
||||
constraintCerts := make([]*x509.Certificate, 0, size+1)
|
||||
constraintCerts = append(constraintCerts, a.intermediateX509Certs...)
|
||||
for _, root := range a.rootX509Certs {
|
||||
if bytes.Equal(last.RawIssuer, root.RawSubject) && bytes.Equal(last.AuthorityKeyId, root.SubjectKeyId) {
|
||||
constraintCerts = append(constraintCerts, root)
|
||||
}
|
||||
}
|
||||
a.constraintsEngine = constraints.New(constraintCerts...)
|
||||
}
|
||||
|
||||
// Load x509 and SSH Policy Engines
|
||||
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||
return err
|
||||
|
|
|
@ -151,16 +151,23 @@ func WithKeyManager(k kms.KeyManager) Option {
|
|||
|
||||
// WithX509Signer defines the signer used to sign X509 certificates.
|
||||
func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option {
|
||||
return WithX509SignerChain([]*x509.Certificate{crt}, s)
|
||||
}
|
||||
|
||||
// WithX509SignerChain defines the signer used to sign X509 certificates. This
|
||||
// option is similar to WithX509Signer but it supports a chain of intermediates.
|
||||
func WithX509SignerChain(issuerChain []*x509.Certificate, s crypto.Signer) Option {
|
||||
return func(a *Authority) error {
|
||||
srv, err := cas.New(context.Background(), casapi.Options{
|
||||
Type: casapi.SoftCAS,
|
||||
Signer: s,
|
||||
CertificateChain: []*x509.Certificate{crt},
|
||||
CertificateChain: issuerChain,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.x509CAService = srv
|
||||
a.intermediateX509Certs = append(a.intermediateX509Certs, issuerChain...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -233,6 +240,25 @@ func WithX509FederatedCerts(certs ...*x509.Certificate) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithX509RootCerts is an option that allows to define the list of intermediate
|
||||
// certificates that the CA will be using. This option will replace any
|
||||
// intermediate certificate defined before.
|
||||
//
|
||||
// Note that these certificates will not be bundled with the certificates signed
|
||||
// by the CA, the CAS service will take care of that, although they should
|
||||
// match, this is not guaranteed. These certificates will be mainly used for
|
||||
// constraint purposes.
|
||||
//
|
||||
// This option should only used on specific configurations, for example when
|
||||
// WithX509SignerFunc is used, as we don't know the list of intermediates on
|
||||
// advance.
|
||||
func WithX509IntermediateCerts(intermediateCerts ...*x509.Certificate) Option {
|
||||
return func(a *Authority) error {
|
||||
a.intermediateX509Certs = intermediateCerts
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithX509RootBundle is an option that allows to define the list of root
|
||||
// certificates. This option will replace any root certificate defined before.
|
||||
func WithX509RootBundle(pemCerts []byte) Option {
|
||||
|
|
|
@ -256,6 +256,9 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
|
|||
// isAllowedToSignX509Certificate checks if the Authority is allowed
|
||||
// to sign the X.509 certificate.
|
||||
func (a *Authority) isAllowedToSignX509Certificate(cert *x509.Certificate) error {
|
||||
if err := a.constraintsEngine.ValidateCertificate(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.policyEngine.IsX509CertificateAllowed(cert)
|
||||
}
|
||||
|
||||
|
@ -351,6 +354,12 @@ func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x5
|
|||
newCert.ExtraExtensions = append(newCert.ExtraExtensions, ext)
|
||||
}
|
||||
|
||||
// Check if the certificate is allowed to be renewed, policies or
|
||||
// constraints might change over time.
|
||||
if err := a.isAllowedToSignX509Certificate(newCert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := a.x509CAService.RenewCertificate(&casapi.RenewCertificateRequest{
|
||||
Template: newCert,
|
||||
Lifetime: lifetime,
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
|
||||
"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"
|
||||
|
||||
|
@ -1589,3 +1590,86 @@ func TestAuthority_Revoke(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue