Skip to content

Commit

Permalink
feat: support for RFC 5280 4.2.1.10 CA Name Constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
rpoisel committed Sep 3, 2022
1 parent bba3a20 commit 85559a5
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 4 deletions.
17 changes: 14 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"net"
"regexp"
"strconv"
"strings"
Expand All @@ -19,6 +20,7 @@ import (
"github.com/cloudflare/cfssl/helpers"
"github.com/cloudflare/cfssl/log"
ocspConfig "github.com/cloudflare/cfssl/ocsp/config"

// empty import of zlint/v3 required to have lints registered.
_ "github.com/zmap/zlint/v3"
"github.com/zmap/zlint/v3/lint"
Expand Down Expand Up @@ -67,9 +69,18 @@ type AuthRemote struct {
// CAConstraint would verify against (and override) the CA
// extensions in the given CSR.
type CAConstraint struct {
IsCA bool `json:"is_ca"`
MaxPathLen int `json:"max_path_len"`
MaxPathLenZero bool `json:"max_path_len_zero"`
IsCA bool `json:"is_ca"`
MaxPathLen int `json:"max_path_len"`
MaxPathLenZero bool `json:"max_path_len_zero"`
PermittedDNSDomainsCritical bool `json:"permitted_dns_domains_critical"`
PermittedDNSDomains []string `json:"permitted_dns_domains"`
ExcludedDNSDomains []string `json:"excluded_dns_domains"`
PermittedIPRanges []*net.IPNet `json:"permitted_ip_ranges"`
ExcludedIPRanges []*net.IPNet `json:"excluded_ip_ranges"`
PermittedEmailAddresses []string `json:"permitted_email_addresses"`
ExcludedEmailAddresses []string `json:"excluded_email_addresses"`
PermittedURIDomains []string `json:"permitted_uri_domains"`
ExcludedURIDomains []string `json:"excluded_uri_domains"`
}

// A SigningProfile stores information that the CA needs to store
Expand Down
44 changes: 44 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,49 @@ var validLocalConfigsWithCAConstraint = []string{
}
}
}`,
`{
"signing": {
"default": {
"usages": ["digital signature", "email protection"],
"ca_constraint": {
"is_ca": true,
"max_path_len_zero": true,
"permitted_dns_domains_critical": true,
"permitted_dns_domains": [
".example.com"
],
"excluded_dns_domains": [
".example.com"
],
"permitted_ip_ranges": [
{
"IP": "192.168.0.0",
"Mask": "//8AAA=="
}
],
"excluded_ip_ranges": [
{
"IP": "172.16.0.0",
"Mask": "//AAAA=="
}
],
"permitted_email_addresses": [
"[email protected]"
],
"excluded_email_addresses": [
"[email protected]"
],
"permitted_uri_domains": [
".example.com"
],
"excluded_uri_domains": [
"host.hurzel.com"
]
},
"expiry": "8000h"
}
}
}`,
}

var copyExtensionWantedlLocalConfig = `
Expand Down Expand Up @@ -443,6 +486,7 @@ func TestLoadFile(t *testing.T) {
"testdata/valid_config.json",
"testdata/valid_config_auth.json",
"testdata/valid_config_no_default.json",
"testdata/valid_config_ca_constraints.json",
"testdata/valid_config_auth_no_default.json",
}

Expand Down
46 changes: 46 additions & 0 deletions config/testdata/valid_config_ca_constraints.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"signing": {
"default": {
"usages": [
"digital signature",
"email protection"
],
"ca_constraint": {
"is_ca": true,
"max_path_len_zero": true,
"permitted_dns_domains_critical": true,
"permitted_dns_domains": [
".example.com"
],
"excluded_dns_domains": [
".example.com"
],
"permitted_ip_ranges": [
{
"IP": "192.168.0.0",
"Mask": "//8AAA=="
}
],
"excluded_ip_ranges": [
{
"IP": "172.16.0.0",
"Mask": "//AAAA=="
}
],
"permitted_email_addresses": [
"[email protected]"
],
"excluded_email_addresses": [
"[email protected]"
],
"permitted_uri_domains": [
".example.com"
],
"excluded_uri_domains": [
"host.hurzel.com"
]
},
"expiry": "8000h"
}
}
}
11 changes: 11 additions & 0 deletions doc/cmd/cfssl.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ blank.
Notice the extra "max_path_len_zero" field: Without it, the
intermediate CA certificate will have no pathlen constraint.

+ RFC 5280 4.2.1.10 Name Constraints
+ permitted_dns_domains_critical
+ permitted_dns_domains
+ excluded_dns_domains
+ permitted_ip_ranges
+ excluded_ip_ranges
+ permitted_email_addresses
+ excluded_email_addresses
+ permitted_uri_domains
+ excluded_uri_domains

+ ocsp_no_check: this should be true if the id-pkix-ocsp-nocheck
extension should be used (RFC 2560 4.2.2.2.1).

Expand Down
115 changes: 114 additions & 1 deletion signer/local/local_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
"github.com/cloudflare/cfssl/helpers"
"github.com/cloudflare/cfssl/log"
"github.com/cloudflare/cfssl/signer"
"github.com/google/certificate-transparency-go"
ct "github.com/google/certificate-transparency-go"
"github.com/zmap/zlint/v3/lint"
)

Expand Down Expand Up @@ -963,6 +963,119 @@ func TestCASignPathlen(t *testing.T) {
}
}

func TestCAConstraints(t *testing.T) {
var caCerts = []string{testCaFile, testECDSACaFile}
var caKeys = []string{testCaKeyFile, testECDSACaKeyFile}
var interCSRs = []string{ecdsaInterCSR, rsaInterCSR}
var interKeys = []string{ecdsaInterKey, rsaInterKey}
var CAPolicy = &config.Signing{
Default: &config.SigningProfile{
Usage: []string{"cert sign", "crl sign"},
ExpiryString: "1h",
Expiry: 1 * time.Hour,
CAConstraint: config.CAConstraint{
IsCA: true,
MaxPathLenZero: true,
PermittedDNSDomainsCritical: true,
PermittedDNSDomains: []string{".sub.cloudflare-inter.com"},
ExcludedDNSDomains: []string{".forbidden.cloudflare-inter.com"},
PermittedIPRanges: []*net.IPNet{{
IP: net.IP{0xc0, 0xa8, 0x0, 0x0},
Mask: net.IPMask{0xff, 0xff, 0x0, 0x0}}},
ExcludedIPRanges: []*net.IPNet{{
IP: net.IP{172, 16, 0x0, 0x0},
Mask: net.IPMask{0xff, 0xf0, 0x0, 0x0}}},
PermittedEmailAddresses: []string{"[email protected]"},
ExcludedEmailAddresses: []string{".illegal.com"},
PermittedURIDomains: []string{".example.com"},
ExcludedURIDomains: []string{"host.illegal.com"}},
},
}
var hostname = "cloudflare-inter.com"
// Each RSA or ECDSA root CA issues two intermediate CAs (one ECDSA and one RSA).
// For each intermediate CA, use it to issue additional RSA and ECDSA intermediate CSRs.
for i, caFile := range caCerts {
caKeyFile := caKeys[i]
s := newCustomSigner(t, caFile, caKeyFile)
s.policy = CAPolicy
for j, csr := range interCSRs {
csrBytes, _ := ioutil.ReadFile(csr)
certBytes, err := s.Sign(signer.SignRequest{Hosts: signer.SplitHosts(hostname), Request: string(csrBytes)})
if err != nil {
t.Fatal(err)
}
interCert, err := helpers.ParseCertificatePEM(certBytes)
if err != nil {
t.Fatal(err)
}
keyBytes, _ := ioutil.ReadFile(interKeys[j])
interKey, _ := helpers.ParsePrivateKeyPEM(keyBytes)
interSigner := &Signer{
ca: interCert,
priv: interKey,
policy: CAPolicy,
sigAlgo: signer.DefaultSigAlgo(interKey),
}
for _, anotherCSR := range interCSRs {
anotherCSRBytes, _ := ioutil.ReadFile(anotherCSR)
bytes, err := interSigner.Sign(
signer.SignRequest{
Hosts: signer.SplitHosts(hostname),
Request: string(anotherCSRBytes),
})
if err != nil {
t.Fatal(err)
}
cert, err := helpers.ParseCertificatePEM(bytes)
if err != nil {
t.Fatal(err)
}
if cert.SignatureAlgorithm != interSigner.SigAlgo() {
t.Fatal("Cert Signature Algorithm does not match the issuer.")
}
if cert.MaxPathLen != 0 {
t.Fatal("CA Cert Max Path is not zero.")
}
if cert.MaxPathLenZero != true {
t.Fatal("CA Cert Max Path is not zero.")
}
if cert.PermittedDNSDomainsCritical != true {
t.Fatal("CA Cert Permitted DNS Domains Critical is not true..")
}
if !reflect.DeepEqual(cert.PermittedDNSDomains, []string{".sub.cloudflare-inter.com"}) {
t.Fatal("CA Cert Permitted DNS Domains is not equal.")
}
if !reflect.DeepEqual(cert.ExcludedDNSDomains, []string{".forbidden.cloudflare-inter.com"}) {
t.Fatal("CA Cert Excluded DNS Domains is not equal.")
}
if !reflect.DeepEqual(cert.PermittedIPRanges, []*net.IPNet{{
IP: net.IP{0xc0, 0xa8, 0x0, 0x0},
Mask: net.IPMask{0xff, 0xff, 0x0, 0x0}}}) {
t.Fatal("CA Cert Permitted IP Ranges is not equal.")
}
if !reflect.DeepEqual(cert.ExcludedIPRanges, []*net.IPNet{{
IP: net.IP{172, 16, 0x0, 0x0},
Mask: net.IPMask{0xff, 0xf0, 0x0, 0x0}}}) {
t.Fatal("CA Cert Excluded IP Ranges is not equal.")
}
if !reflect.DeepEqual(cert.PermittedEmailAddresses, []string{"[email protected]"}) {
t.Fatal("CA Cert Permitted Email Addresses is not equal.")
}
if !reflect.DeepEqual(cert.ExcludedEmailAddresses, []string{".illegal.com"}) {
t.Fatal("CA Cert Excluded Email Addresses is not equal.")
}
if !reflect.DeepEqual(cert.PermittedURIDomains, []string{".example.com"}) {
t.Fatal("CA Cert Permitted URI Domains is not equal.")
}
if !reflect.DeepEqual(cert.ExcludedURIDomains, []string{"host.illegal.com"}) {
t.Fatal("CA Cert Excluded URI Domains is not equal.")
}
}
}
}

}

func TestNoWhitelistSign(t *testing.T) {
csrPEM, err := ioutil.ReadFile(fullSubjectCSR)
if err != nil {
Expand Down
9 changes: 9 additions & 0 deletions signer/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,15 @@ func FillTemplate(template *x509.Certificate, defaultProfile, profile *config.Si
template.DNSNames = nil
template.EmailAddresses = nil
template.URIs = nil
template.PermittedDNSDomainsCritical = profile.CAConstraint.PermittedDNSDomainsCritical
template.PermittedDNSDomains = profile.CAConstraint.PermittedDNSDomains
template.ExcludedDNSDomains = profile.CAConstraint.ExcludedDNSDomains
template.PermittedIPRanges = profile.CAConstraint.PermittedIPRanges
template.ExcludedIPRanges = profile.CAConstraint.ExcludedIPRanges
template.PermittedEmailAddresses = profile.CAConstraint.PermittedEmailAddresses
template.ExcludedEmailAddresses = profile.CAConstraint.ExcludedEmailAddresses
template.PermittedURIDomains = profile.CAConstraint.PermittedURIDomains
template.ExcludedURIDomains = profile.CAConstraint.ExcludedURIDomains
}
template.SubjectKeyId = ski

Expand Down

0 comments on commit 85559a5

Please sign in to comment.