mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
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:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
3
changelog/29245.txt
Normal 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.
|
||||||
|
```
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
layout: docs
|
layout: docs
|
||||||
page_title: 'PKI secrets engine considerations'
|
page_title: 'PKI secrets engine considerations'
|
||||||
description: >-
|
description: >-
|
||||||
Understand the important considerations and guidance before using the PKI secrets engine to generate certificates before using the PKI secrets engine.
|
Understand the important considerations and guidance before using the PKI secrets engine to generate certificates before using the PKI secrets engine.
|
||||||
---
|
---
|
||||||
|
|
||||||
# PKI secrets engine considerations
|
# PKI secrets engine considerations
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user