Support all fields of the name constraints extension when generating CA certificates (#29245)

Support all fields of the name constraints extension when generating CA certs.

The PKI secrets engine only provided parameter permitted_dns_domains to create
the name constraints extension when generating CA certificates.

Add the following parameters to provide full support for the extension:

  * permitted_email_addresses
  * permitted_ip_ranges
  * permitted_uri_domains
  * excluded_dns_domains
  * excluded_email_addresses
  * excluded_ip_ranges
  * excluded_uri_domains

Specifying any combination of these parameters will trigger the creation of the
name constraints extension as per RFC 5280 section 4.2.1.10.
This commit is contained in:
Victor Rodriguez
2024-12-20 20:55:25 +01:00
committed by GitHub
parent df73491763
commit b9e949bf73
10 changed files with 587 additions and 217 deletions

View File

@@ -348,6 +348,19 @@ func generateCert(sc *storageContext,
if isCA { if isCA {
data.Params.IsCA = isCA data.Params.IsCA = isCA
data.Params.PermittedDNSDomains = input.apiData.Get("permitted_dns_domains").([]string) data.Params.PermittedDNSDomains = input.apiData.Get("permitted_dns_domains").([]string)
data.Params.ExcludedDNSDomains = input.apiData.Get("excluded_dns_domains").([]string)
data.Params.PermittedIPRanges, err = convertIpRanges(input.apiData.Get("permitted_ip_ranges").([]string))
if err != nil {
return nil, nil, errutil.UserError{Err: fmt.Sprintf("invalid permitted_ip_ranges value: %s", err)}
}
data.Params.ExcludedIPRanges, err = convertIpRanges(input.apiData.Get("excluded_ip_ranges").([]string))
if err != nil {
return nil, nil, errutil.UserError{Err: fmt.Sprintf("invalid excluded_ip_ranges value: %s", err)}
}
data.Params.PermittedEmailAddresses = input.apiData.Get("permitted_email_addresses").([]string)
data.Params.ExcludedEmailAddresses = input.apiData.Get("excluded_email_addresses").([]string)
data.Params.PermittedURIDomains = input.apiData.Get("permitted_uri_domains").([]string)
data.Params.ExcludedURIDomains = input.apiData.Get("excluded_uri_domains").([]string)
if data.SigningBundle == nil { if data.SigningBundle == nil {
// Generating a self-signed root certificate. Since we have no // Generating a self-signed root certificate. Since we have no
@@ -399,6 +412,21 @@ func generateCert(sc *storageContext,
return parsedBundle, warnings, nil return parsedBundle, warnings, nil
} }
// convertIpRanges parses each string in the input slice as an IP network. Input
// strings are expected to be in the CIDR notation of IP address and prefix length
// like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291.
func convertIpRanges(ipRanges []string) ([]*net.IPNet, error) {
var ret []*net.IPNet
for _, ipRange := range ipRanges {
_, ipnet, err := net.ParseCIDR(ipRange)
if err != nil {
return nil, fmt.Errorf("error parsing IP range %q: %w", ipRange, err)
}
ret = append(ret, ipnet)
}
return ret, nil
}
// N.B.: This is only meant to be used for generating intermediate CAs. // N.B.: This is only meant to be used for generating intermediate CAs.
// It skips some sanity checks. // It skips some sanity checks.
func generateIntermediateCSR(sc *storageContext, input *inputBundle, randomSource io.Reader) (*certutil.ParsedCSRBundle, []string, error) { func generateIntermediateCSR(sc *storageContext, input *inputBundle, randomSource io.Reader) (*certutil.ParsedCSRBundle, []string, error) {
@@ -472,6 +500,34 @@ func (i SignCertInputFromDataFields) GetPermittedDomains() []string {
return i.data.Get("permitted_dns_domains").([]string) return i.data.Get("permitted_dns_domains").([]string)
} }
func (i SignCertInputFromDataFields) GetExcludedDomains() []string {
return i.data.Get("excluded_dns_domains").([]string)
}
func (i SignCertInputFromDataFields) GetPermittedIpRanges() ([]*net.IPNet, error) {
return convertIpRanges(i.data.Get("permitted_ip_ranges").([]string))
}
func (i SignCertInputFromDataFields) GetExcludedIpRanges() ([]*net.IPNet, error) {
return convertIpRanges(i.data.Get("excluded_ip_ranges").([]string))
}
func (i SignCertInputFromDataFields) GetPermittedEmailAddresses() []string {
return i.data.Get("permitted_email_addresses").([]string)
}
func (i SignCertInputFromDataFields) GetExcludedEmailAddresses() []string {
return i.data.Get("excluded_email_addresses").([]string)
}
func (i SignCertInputFromDataFields) GetPermittedUriDomains() []string {
return i.data.Get("permitted_uri_domains").([]string)
}
func (i SignCertInputFromDataFields) GetExcludedUriDomains() []string {
return i.data.Get("excluded_uri_domains").([]string)
}
func (i SignCertInputFromDataFields) IgnoreCSRSignature() bool { func (i SignCertInputFromDataFields) IgnoreCSRSignature() bool {
return false return false
} }

View File

@@ -267,13 +267,13 @@ func TestPki_PermitFQDNs(t *testing.T) {
} }
type parseCertificateTestCase struct { type parseCertificateTestCase struct {
name string name string
data map[string]interface{} data map[string]interface{}
roleData map[string]interface{} // if a role is to be created roleData map[string]interface{} // if a role is to be created
ttl time.Duration ttl time.Duration
wantParams certutil.CreationParameters wantParams certutil.CreationParameters
wantFields map[string]interface{} wantFields map[string]interface{}
wantErr bool wantIssuanceErr string // If not empty, require.ErrorContains will be used on this string
} }
// TestDisableVerifyCertificateEnvVar verifies that env var VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION // TestDisableVerifyCertificateEnvVar verifies that env var VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION
@@ -387,12 +387,16 @@ func TestParseCertificate(t *testing.T) {
parseURL := func(s string) *url.URL { parseURL := func(s string) *url.URL {
u, err := url.Parse(s) u, err := url.Parse(s)
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
return u return u
} }
convertIps := func(ipRanges ...string) []*net.IPNet {
ret, err := convertIpRanges(ipRanges)
require.NoError(t, err)
return ret
}
tests := []*parseCertificateTestCase{ tests := []*parseCertificateTestCase{
{ {
name: "simple CA", name: "simple CA",
@@ -434,56 +438,69 @@ func TestParseCertificate(t *testing.T) {
SKID: []byte("We'll assert that it is not nil as an special case"), SKID: []byte("We'll assert that it is not nil as an special case"),
}, },
wantFields: map[string]interface{}{ wantFields: map[string]interface{}{
"common_name": "the common name", "common_name": "the common name",
"alt_names": "", "alt_names": "",
"ip_sans": "", "ip_sans": "",
"uri_sans": "", "uri_sans": "",
"other_sans": "", "other_sans": "",
"signature_bits": 384, "signature_bits": 384,
"exclude_cn_from_sans": true, "exclude_cn_from_sans": true,
"ou": "", "ou": "",
"organization": "", "organization": "",
"country": "", "country": "",
"locality": "", "locality": "",
"province": "", "province": "",
"street_address": "", "street_address": "",
"postal_code": "", "postal_code": "",
"serial_number": "", "serial_number": "",
"ttl": "1h0m30s", "ttl": "1h0m30s",
"max_path_length": -1, "max_path_length": -1,
"permitted_dns_domains": "", "permitted_dns_domains": "",
"use_pss": false, "excluded_dns_domains": "",
"key_type": "ec", "permitted_ip_ranges": "",
"key_bits": 384, "excluded_ip_ranges": "",
"skid": "We'll assert that it is not nil as an special case", "permitted_email_addresses": "",
"excluded_email_addresses": "",
"permitted_uri_domains": "",
"excluded_uri_domains": "",
"use_pss": false,
"key_type": "ec",
"key_bits": 384,
"skid": "We'll assert that it is not nil as an special case",
}, },
wantErr: false,
}, },
{ {
// Note that this test's data is used to create the internal CA used by test "full non CA cert" // Note that this test's data is used to create the internal CA used by test "full non CA cert"
name: "full CA", name: "full CA",
data: map[string]interface{}{ data: map[string]interface{}{
// using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#sign-certificate // using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#sign-certificate
"common_name": "the common name", "common_name": "the common name",
"alt_names": "user@example.com,admin@example.com,example.com,www.example.com", "alt_names": "user@example.com,admin@example.com,example.com,www.example.com",
"ip_sans": "1.2.3.4,1.2.3.5", "ip_sans": "1.2.3.4,1.2.3.5",
"uri_sans": "https://example.com,https://www.example.com", "uri_sans": "https://example.com,https://www.example.com",
"other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com",
"ttl": "2h", "ttl": "2h",
"max_path_length": 2, "max_path_length": 2,
"permitted_dns_domains": "example.com,.example.com,.www.example.com", "permitted_dns_domains": "example.com,.example.com,.www.example.com",
"ou": "unit1, unit2", "excluded_dns_domains": "bad.example.com,reallybad.com",
"organization": "org1, org2", "permitted_ip_ranges": "192.0.2.1/24,76.76.21.21/24,2001:4860:4860::8889/32", // Note that while an IP address if specified here, it is the network address that will be stored
"country": "US, CA", "excluded_ip_ranges": "127.0.0.1/16,2001:4860:4860::8888/32",
"locality": "locality1, locality2", "permitted_email_addresses": "info@example.com,user@example.com,admin@example.com",
"province": "province1, province2", "excluded_email_addresses": "root@example.com,robots@example.com",
"street_address": "street_address1, street_address2", "permitted_uri_domains": "example.com,www.example.com",
"postal_code": "postal_code1, postal_code2", "excluded_uri_domains": "ftp.example.com,gopher.www.example.com",
"not_before_duration": "45s", "ou": "unit1, unit2",
"key_type": "rsa", "organization": "org1, org2",
"use_pss": true, "country": "US, CA",
"key_bits": 2048, "locality": "locality1, locality2",
"signature_bits": 384, "province": "province1, province2",
"street_address": "street_address1, street_address2",
"postal_code": "postal_code1, postal_code2",
"not_before_duration": "45s",
"key_type": "rsa",
"use_pss": true,
"key_bits": 2048,
"signature_bits": 384,
// TODO(kitography): Specify key usage // TODO(kitography): Specify key usage
}, },
ttl: 2 * time.Hour, ttl: 2 * time.Hour,
@@ -517,36 +534,49 @@ func TestParseCertificate(t *testing.T) {
ForceAppendCaChain: false, ForceAppendCaChain: false,
UseCSRValues: false, UseCSRValues: false,
PermittedDNSDomains: []string{"example.com", ".example.com", ".www.example.com"}, PermittedDNSDomains: []string{"example.com", ".example.com", ".www.example.com"},
ExcludedDNSDomains: []string{"bad.example.com", "reallybad.com"},
PermittedIPRanges: convertIps("192.0.2.0/24", "76.76.21.0/24", "2001:4860::/32"), // Note that we stored the network address rather than the specific IP address
ExcludedIPRanges: convertIps("127.0.0.0/16", "2001:4860::/32"),
PermittedEmailAddresses: []string{"info@example.com", "user@example.com", "admin@example.com"},
ExcludedEmailAddresses: []string{"root@example.com", "robots@example.com"},
PermittedURIDomains: []string{"example.com", "www.example.com"},
ExcludedURIDomains: []string{"ftp.example.com", "gopher.www.example.com"},
URLs: nil, URLs: nil,
MaxPathLength: 2, MaxPathLength: 2,
NotBeforeDuration: 45 * time.Second, NotBeforeDuration: 45 * time.Second,
SKID: []byte("We'll assert that it is not nil as an special case"), SKID: []byte("We'll assert that it is not nil as an special case"),
}, },
wantFields: map[string]interface{}{ wantFields: map[string]interface{}{
"common_name": "the common name", "common_name": "the common name",
"alt_names": "example.com,www.example.com,admin@example.com,user@example.com", "alt_names": "example.com,www.example.com,admin@example.com,user@example.com",
"ip_sans": "1.2.3.4,1.2.3.5", "ip_sans": "1.2.3.4,1.2.3.5",
"uri_sans": "https://example.com,https://www.example.com", "uri_sans": "https://example.com,https://www.example.com",
"other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com",
"signature_bits": 384, "signature_bits": 384,
"exclude_cn_from_sans": true, "exclude_cn_from_sans": true,
"ou": "unit1,unit2", "ou": "unit1,unit2",
"organization": "org1,org2", "organization": "org1,org2",
"country": "CA,US", "country": "CA,US",
"locality": "locality1,locality2", "locality": "locality1,locality2",
"province": "province1,province2", "province": "province1,province2",
"street_address": "street_address1,street_address2", "street_address": "street_address1,street_address2",
"postal_code": "postal_code1,postal_code2", "postal_code": "postal_code1,postal_code2",
"serial_number": "", "serial_number": "",
"ttl": "2h0m45s", "ttl": "2h0m45s",
"max_path_length": 2, "max_path_length": 2,
"permitted_dns_domains": "example.com,.example.com,.www.example.com", "permitted_dns_domains": "example.com,.example.com,.www.example.com",
"use_pss": true, "excluded_dns_domains": "bad.example.com,reallybad.com",
"key_type": "rsa", "permitted_ip_ranges": "192.0.2.0/24,76.76.21.0/24,2001:4860::/32",
"key_bits": 2048, "excluded_ip_ranges": "127.0.0.0/16,2001:4860::/32",
"skid": "We'll assert that it is not nil as an special case", "permitted_email_addresses": "info@example.com,user@example.com,admin@example.com",
"excluded_email_addresses": "root@example.com,robots@example.com",
"permitted_uri_domains": "example.com,www.example.com",
"excluded_uri_domains": "ftp.example.com,gopher.www.example.com",
"use_pss": true,
"key_type": "rsa",
"key_bits": 2048,
"skid": "We'll assert that it is not nil as an special case",
}, },
wantErr: false,
}, },
{ {
// Note that we use the data of test "full CA" to create the internal CA needed for this test // Note that we use the data of test "full CA" to create the internal CA needed for this test
@@ -555,7 +585,7 @@ func TestParseCertificate(t *testing.T) {
// using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#generate-certificate-and-key // using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#generate-certificate-and-key
"common_name": "the common name non ca", "common_name": "the common name non ca",
"alt_names": "user@example.com,admin@example.com,example.com,www.example.com", "alt_names": "user@example.com,admin@example.com,example.com,www.example.com",
"ip_sans": "1.2.3.4,1.2.3.5", "ip_sans": "192.0.2.1,192.0.2.2", // These must be permitted by the full CA
"uri_sans": "https://example.com,https://www.example.com", "uri_sans": "https://example.com,https://www.example.com",
"other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com",
"ttl": "2h", "ttl": "2h",
@@ -589,7 +619,7 @@ func TestParseCertificate(t *testing.T) {
}, },
DNSNames: []string{"example.com", "www.example.com"}, DNSNames: []string{"example.com", "www.example.com"},
EmailAddresses: []string{"admin@example.com", "user@example.com"}, EmailAddresses: []string{"admin@example.com", "user@example.com"},
IPAddresses: []net.IP{[]byte{1, 2, 3, 4}, []byte{1, 2, 3, 5}}, IPAddresses: []net.IP{[]byte{192, 0, 2, 1}, []byte{192, 0, 2, 2}},
URIs: []*url.URL{parseURL("https://example.com"), parseURL("https://www.example.com")}, URIs: []*url.URL{parseURL("https://example.com"), parseURL("https://www.example.com")},
OtherSANs: map[string][]string{"1.3.6.1.4.1.311.20.2.3": {"caadmin@example.com"}}, OtherSANs: map[string][]string{"1.3.6.1.4.1.311.20.2.3": {"caadmin@example.com"}},
IsCA: false, IsCA: false,
@@ -612,30 +642,120 @@ func TestParseCertificate(t *testing.T) {
SKID: []byte("We'll assert that it is not nil as an special case"), SKID: []byte("We'll assert that it is not nil as an special case"),
}, },
wantFields: map[string]interface{}{ wantFields: map[string]interface{}{
"common_name": "the common name non ca", "common_name": "the common name non ca",
"alt_names": "example.com,www.example.com,admin@example.com,user@example.com", "alt_names": "example.com,www.example.com,admin@example.com,user@example.com",
"ip_sans": "1.2.3.4,1.2.3.5", "ip_sans": "192.0.2.1,192.0.2.2",
"uri_sans": "https://example.com,https://www.example.com", "uri_sans": "https://example.com,https://www.example.com",
"other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com",
"signature_bits": 384, "signature_bits": 384,
"exclude_cn_from_sans": true, "exclude_cn_from_sans": true,
"ou": "", "ou": "",
"organization": "", "organization": "",
"country": "", "country": "",
"locality": "", "locality": "",
"province": "", "province": "",
"street_address": "", "street_address": "",
"postal_code": "", "postal_code": "",
"serial_number": "", "serial_number": "",
"ttl": "2h0m45s", "ttl": "2h0m45s",
"max_path_length": 0, "max_path_length": 0,
"permitted_dns_domains": "", "permitted_dns_domains": "",
"use_pss": false, "excluded_dns_domains": "",
"key_type": "rsa", "permitted_ip_ranges": "",
"key_bits": 2048, "excluded_ip_ranges": "",
"skid": "We'll assert that it is not nil as an special case", "permitted_email_addresses": "",
"excluded_email_addresses": "",
"permitted_uri_domains": "",
"excluded_uri_domains": "",
"use_pss": false,
"key_type": "rsa",
"key_bits": 2048,
"skid": "We'll assert that it is not nil as an special case",
}, },
wantErr: false, },
{
name: "DNS domain not permitted",
data: map[string]interface{}{
"common_name": "the common name non ca",
"alt_names": "badexample.com",
"ttl": "2h",
},
ttl: 2 * time.Hour,
roleData: map[string]interface{}{
"allow_any_name": true,
"cn_validations": "disabled",
},
wantIssuanceErr: `DNS name "badexample.com" is not permitted by any constraint`,
},
{
name: "DNS domain explicitly excluded",
data: map[string]interface{}{
"common_name": "the common name non ca",
"alt_names": "bad.example.com",
"ttl": "2h",
},
ttl: 2 * time.Hour,
roleData: map[string]interface{}{
"allow_any_name": true,
"cn_validations": "disabled",
},
wantIssuanceErr: `DNS name "bad.example.com" is excluded by constraint "bad.example.com"`,
},
{
name: "IP address not permitted",
data: map[string]interface{}{
"common_name": "the common name non ca",
"ip_sans": "192.0.3.1",
"ttl": "2h",
},
ttl: 2 * time.Hour,
roleData: map[string]interface{}{
"allow_any_name": true,
"cn_validations": "disabled",
},
wantIssuanceErr: `IP address "192.0.3.1" is not permitted by any constraint`,
},
{
name: "IP address explicitly excluded",
data: map[string]interface{}{
"common_name": "the common name non ca",
"ip_sans": "127.0.0.123",
"ttl": "2h",
},
ttl: 2 * time.Hour,
roleData: map[string]interface{}{
"allow_any_name": true,
"cn_validations": "disabled",
},
wantIssuanceErr: `IP address "127.0.0.123" is excluded by constraint "127.0.0.0/16"`,
},
{
name: "email address not permitted",
data: map[string]interface{}{
"common_name": "the common name non ca",
"alt_names": "random@example.com",
"ttl": "2h",
},
ttl: 2 * time.Hour,
roleData: map[string]interface{}{
"allow_any_name": true,
"cn_validations": "disabled",
},
wantIssuanceErr: `email address "random@example.com" is not permitted by any constraint`,
},
{
name: "email address explicitly excluded",
data: map[string]interface{}{
"common_name": "the common name non ca",
"alt_names": "root@example.com",
"ttl": "2h",
},
ttl: 2 * time.Hour,
roleData: map[string]interface{}{
"allow_any_name": true,
"cn_validations": "disabled",
},
wantIssuanceErr: `email address "root@example.com" is excluded by constraint "root@example.com"`,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@@ -668,15 +788,22 @@ func TestParseCertificate(t *testing.T) {
// create the cert // create the cert
resp, err = CBWrite(b, s, "issue/test", tt.data) resp, err = CBWrite(b, s, "issue/test", tt.data)
require.NoError(t, err) if tt.wantIssuanceErr != "" {
require.NotNil(t, resp) require.ErrorContains(t, err, tt.wantIssuanceErr)
} else {
require.NoError(t, err)
require.NotNil(t, resp)
certData := resp.Data["certificate"].(string) certData := resp.Data["certificate"].(string)
cert, err = parsing.ParseCertificateFromString(certData) cert, err = parsing.ParseCertificateFromString(certData)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, cert) require.NotNil(t, cert)
}
} }
if tt.wantIssuanceErr != "" {
return
}
t.Run(tt.name+" parameters", func(t *testing.T) { t.Run(tt.name+" parameters", func(t *testing.T) {
testParseCertificateToCreationParameters(t, issueTime, tt, cert) testParseCertificateToCreationParameters(t, issueTime, tt, cert)
}) })
@@ -690,72 +817,64 @@ func TestParseCertificate(t *testing.T) {
func testParseCertificateToCreationParameters(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, cert *x509.Certificate) { func testParseCertificateToCreationParameters(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, cert *x509.Certificate) {
params, err := certutil.ParseCertificateToCreationParameters(*cert) params, err := certutil.ParseCertificateToCreationParameters(*cert)
if tt.wantErr { require.NoError(t, err)
require.Error(t, err)
} else {
require.NoError(t, err)
ignoreBasicConstraintsValidForNonCA := tt.wantParams.IsCA ignoreBasicConstraintsValidForNonCA := tt.wantParams.IsCA
var diff []string var diff []string
for _, d := range deep.Equal(tt.wantParams, params) { for _, d := range deep.Equal(tt.wantParams, params) {
switch { switch {
case strings.HasPrefix(d, "SKID"): case strings.HasPrefix(d, "SKID"):
continue continue
case strings.HasPrefix(d, "BasicConstraintsValidForNonCA") && ignoreBasicConstraintsValidForNonCA: case strings.HasPrefix(d, "BasicConstraintsValidForNonCA") && ignoreBasicConstraintsValidForNonCA:
continue continue
case strings.HasPrefix(d, "NotBeforeDuration"): case strings.HasPrefix(d, "NotBeforeDuration"):
continue continue
case strings.HasPrefix(d, "NotAfter"): case strings.HasPrefix(d, "NotAfter"):
continue continue
}
diff = append(diff, d)
} }
if diff != nil { diff = append(diff, d)
t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.Join(diff, "\n"))
}
require.NotNil(t, params.SKID)
require.GreaterOrEqual(t, params.NotBeforeDuration, tt.wantParams.NotBeforeDuration,
"NotBeforeDuration want: %s got: %s", tt.wantParams.NotBeforeDuration, params.NotBeforeDuration)
require.GreaterOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(-1*time.Minute),
"NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter)
require.LessOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(1*time.Minute),
"NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter)
} }
if diff != nil {
t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.Join(diff, "\n"))
}
require.NotNil(t, params.SKID)
require.GreaterOrEqual(t, params.NotBeforeDuration, tt.wantParams.NotBeforeDuration,
"NotBeforeDuration want: %s got: %s", tt.wantParams.NotBeforeDuration, params.NotBeforeDuration)
require.GreaterOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(-1*time.Minute),
"NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter)
require.LessOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(1*time.Minute),
"NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter)
} }
func testParseCertificateToFields(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, cert *x509.Certificate) { func testParseCertificateToFields(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, cert *x509.Certificate) {
fields, err := certutil.ParseCertificateToFields(*cert) fields, err := certutil.ParseCertificateToFields(*cert)
if tt.wantErr { require.NoError(t, err)
require.Error(t, err)
} else { require.NotNil(t, fields["skid"])
delete(fields, "skid")
delete(tt.wantFields, "skid")
{
// Sometimes TTL comes back as 1s off, so we'll allow that
expectedTTL, err := parseutil.ParseDurationSecond(tt.wantFields["ttl"].(string))
require.NoError(t, err)
actualTTL, err := parseutil.ParseDurationSecond(fields["ttl"].(string))
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, fields["skid"]) diff := expectedTTL - actualTTL
delete(fields, "skid") require.LessOrEqual(t, actualTTL, expectedTTL, // NotAfter is generated before NotBefore so the time.Now of notBefore may be later, shrinking our calculated TTL during very slow tests
delete(tt.wantFields, "skid") "ttl should be, if off, smaller than expected want: %s got: %s", tt.wantFields["ttl"], fields["ttl"])
require.LessOrEqual(t, diff, 30*time.Second, // Test can be slow, allow more off in the other direction
"ttl must be at most 30s off, want: %s got: %s", tt.wantFields["ttl"], fields["ttl"])
delete(fields, "ttl")
delete(tt.wantFields, "ttl")
}
{ if diff := deep.Equal(tt.wantFields, fields); diff != nil {
// Sometimes TTL comes back as 1s off, so we'll allow that t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap"))
expectedTTL, err := parseutil.ParseDurationSecond(tt.wantFields["ttl"].(string))
require.NoError(t, err)
actualTTL, err := parseutil.ParseDurationSecond(fields["ttl"].(string))
require.NoError(t, err)
diff := expectedTTL - actualTTL
require.LessOrEqual(t, actualTTL, expectedTTL, // NotAfter is generated before NotBefore so the time.Now of notBefore may be later, shrinking our calculated TTL during very slow tests
"ttl should be, if off, smaller than expected want: %s got: %s", tt.wantFields["ttl"], fields["ttl"])
require.LessOrEqual(t, diff, 30*time.Second, // Test can be slow, allow more off in the other direction
"ttl must be at most 30s off, want: %s got: %s", tt.wantFields["ttl"], fields["ttl"])
delete(fields, "ttl")
delete(tt.wantFields, "ttl")
}
if diff := deep.Equal(tt.wantFields, fields); diff != nil {
t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap"))
}
} }
} }
@@ -831,7 +950,6 @@ func TestParseCsr(t *testing.T) {
"serial_number": "", "serial_number": "",
"add_basic_constraints": false, "add_basic_constraints": false,
}, },
wantErr: false,
}, },
{ {
name: "full CSR with basic constraints", name: "full CSR with basic constraints",
@@ -918,7 +1036,6 @@ func TestParseCsr(t *testing.T) {
"serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", "serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8",
"add_basic_constraints": true, "add_basic_constraints": true,
}, },
wantErr: false,
}, },
{ {
name: "full CSR without basic constraints", name: "full CSR without basic constraints",
@@ -1005,7 +1122,6 @@ func TestParseCsr(t *testing.T) {
"serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", "serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8",
"add_basic_constraints": false, "add_basic_constraints": false,
}, },
wantErr: false,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@@ -1034,26 +1150,18 @@ func TestParseCsr(t *testing.T) {
func testParseCsrToCreationParameters(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, csr *x509.CertificateRequest) { func testParseCsrToCreationParameters(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, csr *x509.CertificateRequest) {
params, err := certutil.ParseCsrToCreationParameters(*csr) params, err := certutil.ParseCsrToCreationParameters(*csr)
if tt.wantErr { require.NoError(t, err)
require.Error(t, err)
} else {
require.NoError(t, err)
if diff := deep.Equal(tt.wantParams, params); diff != nil { if diff := deep.Equal(tt.wantParams, params); diff != nil {
t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap"))
}
} }
} }
func testParseCsrToFields(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, csr *x509.CertificateRequest) { func testParseCsrToFields(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, csr *x509.CertificateRequest) {
fields, err := certutil.ParseCsrToFields(*csr) fields, err := certutil.ParseCsrToFields(*csr)
if tt.wantErr { require.NoError(t, err)
require.Error(t, err)
} else {
require.NoError(t, err)
if diff := deep.Equal(tt.wantFields, fields); diff != nil { if diff := deep.Equal(tt.wantFields, fields); diff != nil {
t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap"))
}
} }
} }

View File

@@ -390,6 +390,60 @@ func addCAIssueFields(fields map[string]*framework.FieldSchema) map[string]*fram
Name: "Permitted DNS Domains", Name: "Permitted DNS Domains",
}, },
} }
fields["excluded_dns_domains"] = &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: `Domains for which this certificate is not allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Excluded DNS Domains",
},
}
fields["permitted_ip_ranges"] = &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: `IP ranges for which this certificate is allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).
Ranges must be specified in the notation of IP address and prefix length, like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291.`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Permitted IP ranges",
},
}
fields["excluded_ip_ranges"] = &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: `IP ranges for which this certificate is not allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).
Ranges must be specified in the notation of IP address and prefix length, like "192.0.2.0/24" or "2001:db8::/32", as defined in RFC 4632 and RFC 4291.`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Excluded IP ranges",
},
}
fields["permitted_email_addresses"] = &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: `Email addresses for which this certificate is allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Permitted email adresses",
},
}
fields["excluded_email_addresses"] = &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: `Email addresses for which this certificate is not allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Excluded email addresses",
},
}
fields["permitted_uri_domains"] = &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: `URI domains for which this certificate is allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Permitted URI domains",
},
}
fields["excluded_uri_domains"] = &framework.FieldSchema{
Type: framework.TypeCommaStringSlice,
Description: `URI domains for which this certificate is not allowed to sign or issue child certificates (see https://tools.ietf.org/html/rfc5280#section-4.2.1.10).`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Excluded URI domains",
},
}
fields = addIssuerNameField(fields) fields = addIssuerNameField(fields)

View File

@@ -9,6 +9,7 @@ import (
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"net"
"github.com/hashicorp/vault/sdk/helper/certutil" "github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/errutil" "github.com/hashicorp/vault/sdk/helper/errutil"
@@ -21,6 +22,13 @@ type SignCertInput interface {
IsCA() bool IsCA() bool
UseCSRValues() bool UseCSRValues() bool
GetPermittedDomains() []string GetPermittedDomains() []string
GetExcludedDomains() []string
GetPermittedIpRanges() ([]*net.IPNet, error)
GetExcludedIpRanges() ([]*net.IPNet, error)
GetPermittedEmailAddresses() []string
GetExcludedEmailAddresses() []string
GetPermittedUriDomains() []string
GetExcludedUriDomains() []string
} }
func NewBasicSignCertInput(csr *x509.CertificateRequest, isCA, useCSRValues bool) BasicSignCertInput { func NewBasicSignCertInput(csr *x509.CertificateRequest, isCA, useCSRValues bool) BasicSignCertInput {
@@ -113,6 +121,38 @@ func (b BasicSignCertInput) GetPermittedDomains() []string {
return []string{} return []string{}
} }
func (b BasicSignCertInput) GetExcludedDomains() []string {
return []string{}
}
// GetPermittedIpRanges returns the permitted IP ranges for the name constraints extension.
// ignore-nil-nil-function-check
func (b BasicSignCertInput) GetPermittedIpRanges() ([]*net.IPNet, error) {
return nil, nil
}
// GetExcludedIpRanges returns the excluded IP ranges for the name constraints extension.
// ignore-nil-nil-function-check
func (b BasicSignCertInput) GetExcludedIpRanges() ([]*net.IPNet, error) {
return nil, nil
}
func (b BasicSignCertInput) GetPermittedEmailAddresses() []string {
return []string{}
}
func (b BasicSignCertInput) GetExcludedEmailAddresses() []string {
return []string{}
}
func (b BasicSignCertInput) GetPermittedUriDomains() []string {
return []string{}
}
func (b BasicSignCertInput) GetExcludedUriDomains() []string {
return []string{}
}
func SignCert(b logical.SystemView, role *RoleEntry, entityInfo EntityInfo, caSign *certutil.CAInfoBundle, signInput SignCertInput) (*certutil.ParsedCertBundle, []string, error) { func SignCert(b logical.SystemView, role *RoleEntry, entityInfo EntityInfo, caSign *certutil.CAInfoBundle, signInput SignCertInput) (*certutil.ParsedCertBundle, []string, error) {
if role == nil { if role == nil {
return nil, nil, errutil.InternalError{Err: "no role found in data bundle"} return nil, nil, errutil.InternalError{Err: "no role found in data bundle"}
@@ -284,6 +324,19 @@ func SignCert(b logical.SystemView, role *RoleEntry, entityInfo EntityInfo, caSi
if signInput.IsCA() { if signInput.IsCA() {
creation.Params.PermittedDNSDomains = signInput.GetPermittedDomains() creation.Params.PermittedDNSDomains = signInput.GetPermittedDomains()
creation.Params.ExcludedDNSDomains = signInput.GetExcludedDomains()
creation.Params.PermittedIPRanges, err = signInput.GetPermittedIpRanges()
if err != nil {
return nil, nil, errutil.UserError{Err: fmt.Sprintf("error parsinng permitted IP ranges: %v", err)}
}
creation.Params.ExcludedIPRanges, err = signInput.GetExcludedIpRanges()
if err != nil {
return nil, nil, errutil.UserError{Err: fmt.Sprintf("error parsinng excluded IP ranges: %v", err)}
}
creation.Params.PermittedEmailAddresses = signInput.GetPermittedEmailAddresses()
creation.Params.ExcludedEmailAddresses = signInput.GetExcludedEmailAddresses()
creation.Params.PermittedURIDomains = signInput.GetPermittedUriDomains()
creation.Params.ExcludedURIDomains = signInput.GetExcludedUriDomains()
} else { } else {
for _, ext := range csr.Extensions { for _, ext := range csr.Extensions {
if ext.Id.Equal(certutil.ExtensionBasicConstraintsOID) { if ext.Id.Equal(certutil.ExtensionBasicConstraintsOID) {

3
changelog/29245.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
secrets/pki: Complete the set of name constraints parameters by adding permitted_email_addresses, permitted_ip_ranges, permitted_uri_domains, excluded_dns_domains, excluded_email_addresses, excluded_ip_ranges, and excluded_uri_domains; this makes it possible for the name constraints extension to be fully specified when creating root and intermediate CA certificates.
```

View File

@@ -939,10 +939,18 @@ func createCertificate(data *CreationBundle, randReader io.Reader, privateKeyGen
} }
// This will only be filled in from the generation paths // This will only be filled in from the generation paths
if len(data.Params.PermittedDNSDomains) > 0 { certTemplate.PermittedDNSDomains = append(certTemplate.PermittedDNSDomains, data.Params.PermittedDNSDomains...)
certTemplate.PermittedDNSDomains = data.Params.PermittedDNSDomains certTemplate.ExcludedDNSDomains = append(certTemplate.ExcludedDNSDomains, data.Params.ExcludedDNSDomains...)
certTemplate.PermittedDNSDomainsCritical = true certTemplate.PermittedIPRanges = append(certTemplate.PermittedIPRanges, data.Params.PermittedIPRanges...)
} certTemplate.ExcludedIPRanges = append(certTemplate.ExcludedIPRanges, data.Params.ExcludedIPRanges...)
certTemplate.PermittedEmailAddresses = append(certTemplate.PermittedEmailAddresses, data.Params.PermittedEmailAddresses...)
certTemplate.ExcludedEmailAddresses = append(certTemplate.ExcludedEmailAddresses, data.Params.ExcludedEmailAddresses...)
certTemplate.PermittedURIDomains = append(certTemplate.PermittedURIDomains, data.Params.PermittedURIDomains...)
certTemplate.ExcludedURIDomains = append(certTemplate.ExcludedURIDomains, data.Params.ExcludedURIDomains...)
// Note that it is harmless to set PermittedDNSDomainsCritical even if all other
// permitted or excluded fields are empty, as the name constraints extension won't be created in
// that case
certTemplate.PermittedDNSDomainsCritical = true
AddPolicyIdentifiers(data, certTemplate) AddPolicyIdentifiers(data, certTemplate)
@@ -1352,10 +1360,15 @@ func signCertificate(data *CreationBundle, randReader io.Reader) (*ParsedCertBun
certTemplate.IsCA = false certTemplate.IsCA = false
} }
if len(data.Params.PermittedDNSDomains) > 0 { certTemplate.ExcludedDNSDomains = append(certTemplate.ExcludedDNSDomains, data.Params.ExcludedDNSDomains...)
certTemplate.PermittedDNSDomains = data.Params.PermittedDNSDomains certTemplate.PermittedIPRanges = append(certTemplate.PermittedIPRanges, data.Params.PermittedIPRanges...)
certTemplate.PermittedDNSDomainsCritical = true certTemplate.ExcludedIPRanges = append(certTemplate.ExcludedIPRanges, data.Params.ExcludedIPRanges...)
} certTemplate.PermittedEmailAddresses = append(certTemplate.PermittedEmailAddresses, data.Params.PermittedEmailAddresses...)
certTemplate.ExcludedEmailAddresses = append(certTemplate.ExcludedEmailAddresses, data.Params.ExcludedEmailAddresses...)
certTemplate.PermittedURIDomains = append(certTemplate.PermittedURIDomains, data.Params.PermittedURIDomains...)
certTemplate.ExcludedURIDomains = append(certTemplate.ExcludedURIDomains, data.Params.ExcludedURIDomains...)
// Note that it is harmless to set PermittedDNSDomainsCritical even if all other permitted/excluded fields are empty
certTemplate.PermittedDNSDomainsCritical = true
certBytes, err = x509.CreateCertificate(randReader, certTemplate, caCert, data.CSR.PublicKey, data.SigningBundle.PrivateKey) certBytes, err = x509.CreateCertificate(randReader, certTemplate, caCert, data.CSR.PublicKey, data.SigningBundle.PrivateKey)
if err != nil { if err != nil {
@@ -1792,7 +1805,15 @@ func ParseCertificateToCreationParameters(certificate x509.Certificate) (creatio
// The following two values are on creation parameters, but are impossible to parse from the certificate // The following two values are on creation parameters, but are impossible to parse from the certificate
// ForceAppendCaChain // ForceAppendCaChain
// UseCSRValues // UseCSRValues
PermittedDNSDomains: certificate.PermittedDNSDomains, PermittedDNSDomains: certificate.PermittedDNSDomains,
ExcludedDNSDomains: certificate.ExcludedDNSDomains,
PermittedIPRanges: certificate.PermittedIPRanges,
ExcludedIPRanges: certificate.ExcludedIPRanges,
PermittedEmailAddresses: certificate.PermittedEmailAddresses,
ExcludedEmailAddresses: certificate.ExcludedEmailAddresses,
PermittedURIDomains: certificate.PermittedURIDomains,
ExcludedURIDomains: certificate.ExcludedURIDomains,
// URLs: punting on this for now // URLs: punting on this for now
MaxPathLength: certificate.MaxPathLen, MaxPathLength: certificate.MaxPathLen,
NotBeforeDuration: time.Now().Sub(certificate.NotBefore), // Assumes Certificate was created this moment NotBeforeDuration: time.Now().Sub(certificate.NotBefore), // Assumes Certificate was created this moment
@@ -1934,33 +1955,48 @@ func ParseCertificateToFields(certificate x509.Certificate) (map[string]interfac
} }
templateData := map[string]interface{}{ templateData := map[string]interface{}{
"common_name": certificate.Subject.CommonName, "common_name": certificate.Subject.CommonName,
"alt_names": MakeAltNamesCommaSeparatedString(certificate.DNSNames, certificate.EmailAddresses), "alt_names": MakeAltNamesCommaSeparatedString(certificate.DNSNames, certificate.EmailAddresses),
"ip_sans": MakeIpAddressCommaSeparatedString(certificate.IPAddresses), "ip_sans": MakeIpAddressCommaSeparatedString(certificate.IPAddresses),
"uri_sans": MakeUriCommaSeparatedString(certificate.URIs), "uri_sans": MakeUriCommaSeparatedString(certificate.URIs),
"other_sans": otherSans, "other_sans": otherSans,
"signature_bits": FindSignatureBits(certificate.SignatureAlgorithm), "signature_bits": FindSignatureBits(certificate.SignatureAlgorithm),
"exclude_cn_from_sans": DetermineExcludeCnFromCertSans(certificate), "exclude_cn_from_sans": DetermineExcludeCnFromCertSans(certificate),
"ou": makeCommaSeparatedString(certificate.Subject.OrganizationalUnit), "ou": makeCommaSeparatedString(certificate.Subject.OrganizationalUnit),
"organization": makeCommaSeparatedString(certificate.Subject.Organization), "organization": makeCommaSeparatedString(certificate.Subject.Organization),
"country": makeCommaSeparatedString(certificate.Subject.Country), "country": makeCommaSeparatedString(certificate.Subject.Country),
"locality": makeCommaSeparatedString(certificate.Subject.Locality), "locality": makeCommaSeparatedString(certificate.Subject.Locality),
"province": makeCommaSeparatedString(certificate.Subject.Province), "province": makeCommaSeparatedString(certificate.Subject.Province),
"street_address": makeCommaSeparatedString(certificate.Subject.StreetAddress), "street_address": makeCommaSeparatedString(certificate.Subject.StreetAddress),
"postal_code": makeCommaSeparatedString(certificate.Subject.PostalCode), "postal_code": makeCommaSeparatedString(certificate.Subject.PostalCode),
"serial_number": certificate.Subject.SerialNumber, "serial_number": certificate.Subject.SerialNumber,
"ttl": (certificate.NotAfter.Sub(certificate.NotBefore)).String(), "ttl": (certificate.NotAfter.Sub(certificate.NotBefore)).String(),
"max_path_length": certificate.MaxPathLen, "max_path_length": certificate.MaxPathLen,
"permitted_dns_domains": strings.Join(certificate.PermittedDNSDomains, ","), "permitted_dns_domains": strings.Join(certificate.PermittedDNSDomains, ","),
"use_pss": IsPSS(certificate.SignatureAlgorithm), "excluded_dns_domains": strings.Join(certificate.ExcludedDNSDomains, ","),
"skid": hex.EncodeToString(certificate.SubjectKeyId), "permitted_ip_ranges": strings.Join(ipRangesToStrings(certificate.PermittedIPRanges), ","),
"key_type": GetKeyType(certificate.PublicKeyAlgorithm.String()), "excluded_ip_ranges": strings.Join(ipRangesToStrings(certificate.ExcludedIPRanges), ","),
"key_bits": FindBitLength(certificate.PublicKey), "permitted_email_addresses": strings.Join(certificate.PermittedEmailAddresses, ","),
"excluded_email_addresses": strings.Join(certificate.ExcludedEmailAddresses, ","),
"permitted_uri_domains": strings.Join(certificate.PermittedURIDomains, ","),
"excluded_uri_domains": strings.Join(certificate.ExcludedURIDomains, ","),
"use_pss": IsPSS(certificate.SignatureAlgorithm),
"skid": hex.EncodeToString(certificate.SubjectKeyId),
"key_type": GetKeyType(certificate.PublicKeyAlgorithm.String()),
"key_bits": FindBitLength(certificate.PublicKey),
} }
return templateData, nil return templateData, nil
} }
func ipRangesToStrings(ipRanges []*net.IPNet) []string {
var ret []string
for _, ipRange := range ipRanges {
ret = append(ret, ipRange.String())
}
return ret
}
func getBasicConstraintsFromExtension(exts []pkix.Extension) (found bool, isCA bool, maxPathLength int, err error) { func getBasicConstraintsFromExtension(exts []pkix.Extension) (found bool, isCA bool, maxPathLength int, err error) {
for _, ext := range exts { for _, ext := range exts {
if ext.Id.Equal(ExtensionBasicConstraintsOID) { if ext.Id.Equal(ExtensionBasicConstraintsOID) {

View File

@@ -807,8 +807,15 @@ type CreationParameters struct {
ForceAppendCaChain bool ForceAppendCaChain bool
// Only used when signing a CA cert // Only used when signing a CA cert
UseCSRValues bool UseCSRValues bool
PermittedDNSDomains []string PermittedDNSDomains []string
ExcludedDNSDomains []string
PermittedIPRanges []*net.IPNet
ExcludedIPRanges []*net.IPNet
PermittedEmailAddresses []string
ExcludedEmailAddresses []string
PermittedURIDomains []string
ExcludedURIDomains []string
// URLs to encode into the certificate // URLs to encode into the certificate
URLs *URLEntries URLs *URLEntries

View File

@@ -774,6 +774,44 @@ intermediary goes beyond the prescribed length.
the domain, as per [RFC 5280 Section 4.2.1.10 - Name the domain, as per [RFC 5280 Section 4.2.1.10 - Name
Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10) Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10)
- `excluded_dns_domains` `(string: "")` - A comma separated string (or, string
array) containing DNS domains for which certificates are not allowed to be issued
or signed by this CA certificate. Supports subdomains via a `.` in front of
the domain, as per [RFC 5280 Section 4.2.1.10 - Name
Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10)
- `permitted_ip_ranges` `(string: "")` - A comma separated string (or, string
array) containing IP ranges for which certificates are allowed to be issued or
signed by this CA certificate. IP ranges must be in the CIDR notation of IP
address and prefix length like "192.0.2.0/24" or "2001:db8::/32", as defined
in RFC 4632 and RFC 4291.
- `excluded_ip_ranges` `(string: "")` - A comma separated string (or, string
array) containing IP ranges for which certificates are not allowed to be
issued or signed by this CA certificate. IP ranges must be in the CIDR
notation of IP address and prefix length like "192.0.2.0/24" or
"2001:db8::/32", as defined in RFC 4632 and RFC 4291.
- `permitted_email_addresses` `(string: "")` - A comma separated string (or, string
array) containing email addresses for which certificates are allowed to be issued or
signed by this CA certificate.
- `excluded_email_addresses` `(string: "")` - A comma separated string (or,
string array) containing email addresses for which certificates are not
allowed to be issued or signed by this CA certificate.
- `permitted_uri_domains` `(string: "")` - A comma separated string (or, string
array) containing fully qualified domain names for which certificates are
allowed to be issued or signed by this CA certificate. Supports subdomains via
a `.` in front of the domain, as per [RFC 5280 Section 4.2.1.10 - Name
Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10)
- `excluded_uri_domains` `(string: "")` - A comma separated string (or, string
array) containing fully qualified domain names for which certificates are not
allowed to be issued or signed by this CA certificate. Supports subdomains via
a `.` in front of the domain, as per [RFC 5280 Section 4.2.1.10 - Name
Constraints](https://tools.ietf.org/html/rfc5280#section-4.2.1.10)
- `ou` `(string: "")` - Specifies the OU (OrganizationalUnit) values in the - `ou` `(string: "")` - Specifies the OU (OrganizationalUnit) values in the
subject field of the resulting certificate. This is a comma-separated string subject field of the resulting certificate. This is a comma-separated string
or JSON array. or JSON array.

View File

@@ -346,6 +346,13 @@ $ vault secrets tune \
-audit-non-hmac-request-keys=street_address \ -audit-non-hmac-request-keys=street_address \
-audit-non-hmac-request-keys=postal_code \ -audit-non-hmac-request-keys=postal_code \
-audit-non-hmac-request-keys=permitted_dns_domains \ -audit-non-hmac-request-keys=permitted_dns_domains \
-audit-non-hmac-request-keys=permitted_email_addresses \
-audit-non-hmac-request-keys=permitted_ip_ranges \
-audit-non-hmac-request-keys=permitted_uri_domains \
-audit-non-hmac-request-keys=excluded_dns_domains \
-audit-non-hmac-request-keys=excluded_email_addresses \
-audit-non-hmac-request-keys=excluded_ip_ranges \
-audit-non-hmac-request-keys=excluded_uri_domains \
-audit-non-hmac-request-keys=policy_identifiers \ -audit-non-hmac-request-keys=policy_identifiers \
-audit-non-hmac-request-keys=ext_key_usage_oids \ -audit-non-hmac-request-keys=ext_key_usage_oids \
-audit-non-hmac-request-keys=csr \ -audit-non-hmac-request-keys=csr \

View File

@@ -223,10 +223,11 @@ performance of easier-to-rotate intermediates and certificates (such as
TLS intermediates). TLS intermediates).
Vault supports the use of both the [`allowed_domains` parameter on Vault supports the use of both the [`allowed_domains` parameter on
Roles](/vault/api-docs/secret/pki#allowed_domains) and the [`permitted_dns_domains` Roles](/vault/api-docs/secret/pki#allowed_domains) and the sef of parameters for
parameter to set the Name Constraints extension](/vault/api-docs/secret/pki#permitted_dns_domains) permitted and excluded DNS domains, IP ranges, email addresses and URI domains
on root and intermediate generation. This allows for several layers of to set the [Name Constraints extension](/vault/api-docs/secret/pki#permitted_dns_domains)
separation of concerns between TLS-based services. on root and intermediate generation. This allows for several layers of separation of
concerns between TLS-based services.
### Cross-Signed intermediates ### Cross-Signed intermediates
@@ -780,6 +781,13 @@ Some suggested keys to un-HMAC for requests are as follows:
- `street_address` - the subject's street address, - `street_address` - the subject's street address,
- `postal_code` - the subject's postal code, - `postal_code` - the subject's postal code,
- `permitted_dns_domains` - permitted DNS domains, - `permitted_dns_domains` - permitted DNS domains,
- `permitted_ip_ranges` - permitted IP ranges,
- `permitted_email_addresses` - permitted email addresses,
- `permitted_uri_domains` - permitted URI domains,
- `excluded_dns_domains` - excluded DNS domains,
- `excluded_email_addresses` - excluded email addresses,
- `excluded_ip_ranges` - excluded IP ranges,
- `excluded_uri_domains` - excluded URI domains,
- `policy_identifiers` - the requested policy identifiers when creating a role, and - `policy_identifiers` - the requested policy identifiers when creating a role, and
- `ext_key_usage_oids` - the extended key usage OIDs for the requested certificate. - `ext_key_usage_oids` - the extended key usage OIDs for the requested certificate.