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 {
data.Params.IsCA = isCA
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 {
// Generating a self-signed root certificate. Since we have no
@@ -399,6 +412,21 @@ func generateCert(sc *storageContext,
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.
// It skips some sanity checks.
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)
}
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 {
return false
}

View File

@@ -273,7 +273,7 @@ type parseCertificateTestCase struct {
ttl time.Duration
wantParams certutil.CreationParameters
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
@@ -387,12 +387,16 @@ func TestParseCertificate(t *testing.T) {
parseURL := func(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
return u
}
convertIps := func(ipRanges ...string) []*net.IPNet {
ret, err := convertIpRanges(ipRanges)
require.NoError(t, err)
return ret
}
tests := []*parseCertificateTestCase{
{
name: "simple CA",
@@ -452,12 +456,18 @@ func TestParseCertificate(t *testing.T) {
"ttl": "1h0m30s",
"max_path_length": -1,
"permitted_dns_domains": "",
"excluded_dns_domains": "",
"permitted_ip_ranges": "",
"excluded_ip_ranges": "",
"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"
@@ -472,6 +482,13 @@ func TestParseCertificate(t *testing.T) {
"ttl": "2h",
"max_path_length": 2,
"permitted_dns_domains": "example.com,.example.com,.www.example.com",
"excluded_dns_domains": "bad.example.com,reallybad.com",
"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
"excluded_ip_ranges": "127.0.0.1/16,2001:4860:4860::8888/32",
"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",
"ou": "unit1, unit2",
"organization": "org1, org2",
"country": "US, CA",
@@ -517,6 +534,13 @@ func TestParseCertificate(t *testing.T) {
ForceAppendCaChain: false,
UseCSRValues: false,
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,
MaxPathLength: 2,
NotBeforeDuration: 45 * time.Second,
@@ -541,12 +565,18 @@ func TestParseCertificate(t *testing.T) {
"ttl": "2h0m45s",
"max_path_length": 2,
"permitted_dns_domains": "example.com,.example.com,.www.example.com",
"excluded_dns_domains": "bad.example.com,reallybad.com",
"permitted_ip_ranges": "192.0.2.0/24,76.76.21.0/24,2001:4860::/32",
"excluded_ip_ranges": "127.0.0.0/16,2001:4860::/32",
"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
@@ -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
"common_name": "the common name non ca",
"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",
"other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com",
"ttl": "2h",
@@ -589,7 +619,7 @@ func TestParseCertificate(t *testing.T) {
},
DNSNames: []string{"example.com", "www.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")},
OtherSANs: map[string][]string{"1.3.6.1.4.1.311.20.2.3": {"caadmin@example.com"}},
IsCA: false,
@@ -614,7 +644,7 @@ func TestParseCertificate(t *testing.T) {
wantFields: map[string]interface{}{
"common_name": "the common name non ca",
"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",
"other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com",
"signature_bits": 384,
@@ -630,12 +660,102 @@ func TestParseCertificate(t *testing.T) {
"ttl": "2h0m45s",
"max_path_length": 0,
"permitted_dns_domains": "",
"excluded_dns_domains": "",
"permitted_ip_ranges": "",
"excluded_ip_ranges": "",
"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 {
@@ -668,6 +788,9 @@ func TestParseCertificate(t *testing.T) {
// create the cert
resp, err = CBWrite(b, s, "issue/test", tt.data)
if tt.wantIssuanceErr != "" {
require.ErrorContains(t, err, tt.wantIssuanceErr)
} else {
require.NoError(t, err)
require.NotNil(t, resp)
@@ -676,7 +799,11 @@ func TestParseCertificate(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, cert)
}
}
if tt.wantIssuanceErr != "" {
return
}
t.Run(tt.name+" parameters", func(t *testing.T) {
testParseCertificateToCreationParameters(t, issueTime, tt, cert)
})
@@ -690,9 +817,6 @@ func TestParseCertificate(t *testing.T) {
func testParseCertificateToCreationParameters(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, cert *x509.Certificate) {
params, err := certutil.ParseCertificateToCreationParameters(*cert)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
ignoreBasicConstraintsValidForNonCA := tt.wantParams.IsCA
@@ -723,14 +847,10 @@ func testParseCertificateToCreationParameters(t *testing.T, issueTime time.Time,
"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) {
fields, err := certutil.ParseCertificateToFields(*cert)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, fields["skid"])
@@ -756,7 +876,6 @@ func testParseCertificateToFields(t *testing.T, issueTime time.Time, tt *parseCe
if diff := deep.Equal(tt.wantFields, fields); diff != nil {
t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap"))
}
}
}
func TestParseCsr(t *testing.T) {
@@ -831,7 +950,6 @@ func TestParseCsr(t *testing.T) {
"serial_number": "",
"add_basic_constraints": false,
},
wantErr: false,
},
{
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",
"add_basic_constraints": true,
},
wantErr: false,
},
{
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",
"add_basic_constraints": false,
},
wantErr: false,
},
}
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) {
params, err := certutil.ParseCsrToCreationParameters(*csr)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
if diff := deep.Equal(tt.wantParams, params); diff != nil {
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) {
fields, err := certutil.ParseCsrToFields(*csr)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
if diff := deep.Equal(tt.wantFields, fields); diff != nil {
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",
},
}
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)

View File

@@ -9,6 +9,7 @@ import (
"crypto/rsa"
"crypto/x509"
"fmt"
"net"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/errutil"
@@ -21,6 +22,13 @@ type SignCertInput interface {
IsCA() bool
UseCSRValues() bool
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 {
@@ -113,6 +121,38 @@ func (b BasicSignCertInput) GetPermittedDomains() []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) {
if role == nil {
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() {
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 {
for _, ext := range csr.Extensions {
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
if len(data.Params.PermittedDNSDomains) > 0 {
certTemplate.PermittedDNSDomains = data.Params.PermittedDNSDomains
certTemplate.PermittedDNSDomains = append(certTemplate.PermittedDNSDomains, data.Params.PermittedDNSDomains...)
certTemplate.ExcludedDNSDomains = append(certTemplate.ExcludedDNSDomains, data.Params.ExcludedDNSDomains...)
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)
@@ -1352,10 +1360,15 @@ func signCertificate(data *CreationBundle, randReader io.Reader) (*ParsedCertBun
certTemplate.IsCA = false
}
if len(data.Params.PermittedDNSDomains) > 0 {
certTemplate.PermittedDNSDomains = data.Params.PermittedDNSDomains
certTemplate.ExcludedDNSDomains = append(certTemplate.ExcludedDNSDomains, data.Params.ExcludedDNSDomains...)
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/excluded fields are empty
certTemplate.PermittedDNSDomainsCritical = true
}
certBytes, err = x509.CreateCertificate(randReader, certTemplate, caCert, data.CSR.PublicKey, data.SigningBundle.PrivateKey)
if err != nil {
@@ -1793,6 +1806,14 @@ func ParseCertificateToCreationParameters(certificate x509.Certificate) (creatio
// ForceAppendCaChain
// UseCSRValues
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
MaxPathLength: certificate.MaxPathLen,
NotBeforeDuration: time.Now().Sub(certificate.NotBefore), // Assumes Certificate was created this moment
@@ -1952,6 +1973,13 @@ func ParseCertificateToFields(certificate x509.Certificate) (map[string]interfac
"ttl": (certificate.NotAfter.Sub(certificate.NotBefore)).String(),
"max_path_length": certificate.MaxPathLen,
"permitted_dns_domains": strings.Join(certificate.PermittedDNSDomains, ","),
"excluded_dns_domains": strings.Join(certificate.ExcludedDNSDomains, ","),
"permitted_ip_ranges": strings.Join(ipRangesToStrings(certificate.PermittedIPRanges), ","),
"excluded_ip_ranges": strings.Join(ipRangesToStrings(certificate.ExcludedIPRanges), ","),
"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()),
@@ -1961,6 +1989,14 @@ func ParseCertificateToFields(certificate x509.Certificate) (map[string]interfac
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) {
for _, ext := range exts {
if ext.Id.Equal(ExtensionBasicConstraintsOID) {

View File

@@ -809,6 +809,13 @@ type CreationParameters struct {
// Only used when signing a CA cert
UseCSRValues bool
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 *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
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
subject field of the resulting certificate. This is a comma-separated string
or JSON array.

View File

@@ -346,6 +346,13 @@ $ vault secrets tune \
-audit-non-hmac-request-keys=street_address \
-audit-non-hmac-request-keys=postal_code \
-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=ext_key_usage_oids \
-audit-non-hmac-request-keys=csr \

View File

@@ -223,10 +223,11 @@ performance of easier-to-rotate intermediates and certificates (such as
TLS intermediates).
Vault supports the use of both the [`allowed_domains` parameter on
Roles](/vault/api-docs/secret/pki#allowed_domains) and the [`permitted_dns_domains`
parameter to set the Name Constraints extension](/vault/api-docs/secret/pki#permitted_dns_domains)
on root and intermediate generation. This allows for several layers of
separation of concerns between TLS-based services.
Roles](/vault/api-docs/secret/pki#allowed_domains) and the sef of parameters for
permitted and excluded DNS domains, IP ranges, email addresses and URI domains
to set the [Name Constraints extension](/vault/api-docs/secret/pki#permitted_dns_domains)
on root and intermediate generation. This allows for several layers of separation of
concerns between TLS-based services.
### 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,
- `postal_code` - the subject's postal code,
- `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
- `ext_key_usage_oids` - the extended key usage OIDs for the requested certificate.