Validate constraints on Sign and Renew/Rekey

Fixes #1060
This commit is contained in:
Mariano Cano 2022-09-20 18:52:47 -07:00
parent 89b6aa924a
commit debe565e42
4 changed files with 154 additions and 13 deletions

View file

@ -1,6 +1,7 @@
package authority package authority
import ( import (
"bytes"
"context" "context"
"crypto" "crypto"
"crypto/sha256" "crypto/sha256"
@ -24,6 +25,7 @@ import (
adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql" adminDBNosql "github.com/smallstep/certificates/authority/admin/db/nosql"
"github.com/smallstep/certificates/authority/administrator" "github.com/smallstep/certificates/authority/administrator"
"github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/internal/constraints"
"github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/policy"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/cas" "github.com/smallstep/certificates/cas"
@ -52,6 +54,7 @@ type Authority struct {
rootX509Certs []*x509.Certificate rootX509Certs []*x509.Certificate
rootX509CertPool *x509.CertPool rootX509CertPool *x509.CertPool
federatedX509Certs []*x509.Certificate federatedX509Certs []*x509.Certificate
intermediateX509Certs []*x509.Certificate
certificates *sync.Map certificates *sync.Map
x509Enforcers []provisioner.CertificateEnforcer x509Enforcers []provisioner.CertificateEnforcer
@ -80,9 +83,8 @@ type Authority struct {
authorizeRenewFunc provisioner.AuthorizeRenewFunc authorizeRenewFunc provisioner.AuthorizeRenewFunc
authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc authorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc
// Constraint engine // Constraints and Policy engines
constraintsEngine *constraints.Engine
// Policy engines
policyEngine *policy.Engine policyEngine *policy.Engine
adminMutex sync.RWMutex adminMutex sync.RWMutex
@ -375,6 +377,11 @@ func (a *Authority) init() error {
if err != nil { if err != nil {
return err 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) a.x509CAService, err = cas.New(ctx, options)
if err != nil { if err != nil {
@ -612,6 +619,21 @@ func (a *Authority) init() error {
return err 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 // Load x509 and SSH Policy Engines
if err := a.reloadPolicyEngines(ctx); err != nil { if err := a.reloadPolicyEngines(ctx); err != nil {
return err return err

View file

@ -151,16 +151,23 @@ func WithKeyManager(k kms.KeyManager) Option {
// WithX509Signer defines the signer used to sign X509 certificates. // WithX509Signer defines the signer used to sign X509 certificates.
func WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option { 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 { return func(a *Authority) error {
srv, err := cas.New(context.Background(), casapi.Options{ srv, err := cas.New(context.Background(), casapi.Options{
Type: casapi.SoftCAS, Type: casapi.SoftCAS,
Signer: s, Signer: s,
CertificateChain: []*x509.Certificate{crt}, CertificateChain: issuerChain,
}) })
if err != nil { if err != nil {
return err return err
} }
a.x509CAService = srv a.x509CAService = srv
a.intermediateX509Certs = append(a.intermediateX509Certs, issuerChain...)
return nil 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 // WithX509RootBundle is an option that allows to define the list of root
// certificates. This option will replace any root certificate defined before. // certificates. This option will replace any root certificate defined before.
func WithX509RootBundle(pemCerts []byte) Option { func WithX509RootBundle(pemCerts []byte) Option {

View file

@ -256,6 +256,9 @@ func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.Sign
// isAllowedToSignX509Certificate checks if the Authority is allowed // isAllowedToSignX509Certificate checks if the Authority is allowed
// to sign the X.509 certificate. // to sign the X.509 certificate.
func (a *Authority) isAllowedToSignX509Certificate(cert *x509.Certificate) error { func (a *Authority) isAllowedToSignX509Certificate(cert *x509.Certificate) error {
if err := a.constraintsEngine.ValidateCertificate(cert); err != nil {
return err
}
return a.policyEngine.IsX509CertificateAllowed(cert) 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) 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{ resp, err := a.x509CAService.RenewCertificate(&casapi.RenewCertificateRequest{
Template: newCert, Template: newCert,
Lifetime: lifetime, Lifetime: lifetime,

View file

@ -22,6 +22,7 @@ import (
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil" "go.step.sm/crypto/keyutil"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/pemutil" "go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util" "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)
}
})
}
}