mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 03:58:01 +00:00
Add support for x.509 Name Serial Number attribute in subject of certificates (#4694)
This commit is contained in:
committed by
Jeff Mitchell
parent
063d9ed756
commit
a8f343c32e
@@ -3221,6 +3221,129 @@ func TestBackend_OID_SANs(t *testing.T) {
|
|||||||
t.Logf("certificate 3 to check:\n%s", certStr)
|
t.Logf("certificate 3 to check:\n%s", certStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackend_AllowedSerialNumbers(t *testing.T) {
|
||||||
|
coreConfig := &vault.CoreConfig{
|
||||||
|
LogicalBackends: map[string]logical.Factory{
|
||||||
|
"pki": Factory,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||||
|
HandlerFunc: vaulthttp.Handler,
|
||||||
|
})
|
||||||
|
cluster.Start()
|
||||||
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
|
client := cluster.Cores[0].Client
|
||||||
|
var err error
|
||||||
|
err = client.Sys().Mount("root", &api.MountInput{
|
||||||
|
Type: "pki",
|
||||||
|
Config: api.MountConfigInput{
|
||||||
|
DefaultLeaseTTL: "16h",
|
||||||
|
MaxLeaseTTL: "60h",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *api.Secret
|
||||||
|
var certStr string
|
||||||
|
var block *pem.Block
|
||||||
|
var cert *x509.Certificate
|
||||||
|
|
||||||
|
_, err = client.Logical().Write("root/root/generate/internal", map[string]interface{}{
|
||||||
|
"ttl": "40h",
|
||||||
|
"common_name": "myvault.com",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First test that Serial Numbers are not allowed
|
||||||
|
_, err = client.Logical().Write("root/roles/test", map[string]interface{}{
|
||||||
|
"allow_any_name": true,
|
||||||
|
"enforce_hostnames": false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = client.Logical().Write("root/issue/test", map[string]interface{}{
|
||||||
|
"common_name": "foobar",
|
||||||
|
"ttl": "1h",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = client.Logical().Write("root/issue/test", map[string]interface{}{
|
||||||
|
"common_name": "foobar",
|
||||||
|
"ttl": "1h",
|
||||||
|
"serial_number": "foobar",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the role to allow serial numbers
|
||||||
|
_, err = client.Logical().Write("root/roles/test", map[string]interface{}{
|
||||||
|
"allow_any_name": true,
|
||||||
|
"enforce_hostnames": false,
|
||||||
|
"allowed_serial_numbers": "f00*,b4r*",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = client.Logical().Write("root/issue/test", map[string]interface{}{
|
||||||
|
"common_name": "foobar",
|
||||||
|
"ttl": "1h",
|
||||||
|
// Not a valid serial number
|
||||||
|
"serial_number": "foobar",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid for first possibility
|
||||||
|
resp, err = client.Logical().Write("root/issue/test", map[string]interface{}{
|
||||||
|
"common_name": "foobar",
|
||||||
|
"serial_number": "f00bar",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
certStr = resp.Data["certificate"].(string)
|
||||||
|
block, _ = pem.Decode([]byte(certStr))
|
||||||
|
cert, err = x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cert.Subject.SerialNumber != "f00bar" {
|
||||||
|
t.Fatalf("unexpected Subject SerialNumber %s", cert.Subject.SerialNumber)
|
||||||
|
}
|
||||||
|
t.Logf("certificate 1 to check:\n%s", certStr)
|
||||||
|
|
||||||
|
// Valid for second possibility
|
||||||
|
resp, err = client.Logical().Write("root/issue/test", map[string]interface{}{
|
||||||
|
"common_name": "foobar",
|
||||||
|
"serial_number": "b4rf00",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
certStr = resp.Data["certificate"].(string)
|
||||||
|
block, _ = pem.Decode([]byte(certStr))
|
||||||
|
cert, err = x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cert.Subject.SerialNumber != "b4rf00" {
|
||||||
|
t.Fatalf("unexpected Subject SerialNumber %s", cert.Subject.SerialNumber)
|
||||||
|
}
|
||||||
|
t.Logf("certificate 2 to check:\n%s", certStr)
|
||||||
|
}
|
||||||
|
|
||||||
func setCerts() {
|
func setCerts() {
|
||||||
cak, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
cak, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -29,20 +29,21 @@ func (b *backend) getGenerationParams(
|
|||||||
}
|
}
|
||||||
|
|
||||||
role = &roleEntry{
|
role = &roleEntry{
|
||||||
TTL: time.Duration(data.Get("ttl").(int)) * time.Second,
|
TTL: time.Duration(data.Get("ttl").(int)) * time.Second,
|
||||||
KeyType: data.Get("key_type").(string),
|
KeyType: data.Get("key_type").(string),
|
||||||
KeyBits: data.Get("key_bits").(int),
|
KeyBits: data.Get("key_bits").(int),
|
||||||
AllowLocalhost: true,
|
AllowLocalhost: true,
|
||||||
AllowAnyName: true,
|
AllowAnyName: true,
|
||||||
AllowIPSANs: true,
|
AllowIPSANs: true,
|
||||||
EnforceHostnames: false,
|
EnforceHostnames: false,
|
||||||
OU: data.Get("ou").([]string),
|
AllowedSerialNumbers: []string{"*"},
|
||||||
Organization: data.Get("organization").([]string),
|
OU: data.Get("ou").([]string),
|
||||||
Country: data.Get("country").([]string),
|
Organization: data.Get("organization").([]string),
|
||||||
Locality: data.Get("locality").([]string),
|
Country: data.Get("country").([]string),
|
||||||
Province: data.Get("province").([]string),
|
Locality: data.Get("locality").([]string),
|
||||||
StreetAddress: data.Get("street_address").([]string),
|
Province: data.Get("province").([]string),
|
||||||
PostalCode: data.Get("postal_code").([]string),
|
StreetAddress: data.Get("street_address").([]string),
|
||||||
|
PostalCode: data.Get("postal_code").([]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
if role.KeyType == "rsa" && role.KeyBits < 2048 {
|
if role.KeyType == "rsa" && role.KeyBits < 2048 {
|
||||||
|
|||||||
@@ -498,6 +498,29 @@ func parseOtherSANs(others []string) (map[string][]string, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateSerialNumber(data *dataBundle, serialNumber string) string {
|
||||||
|
valid := false
|
||||||
|
if len(data.role.AllowedSerialNumbers) > 0 {
|
||||||
|
for _, currSerialNumber := range data.role.AllowedSerialNumbers {
|
||||||
|
if currSerialNumber == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strings.Contains(currSerialNumber, "*") &&
|
||||||
|
glob.Glob(currSerialNumber, serialNumber)) ||
|
||||||
|
currSerialNumber == serialNumber {
|
||||||
|
valid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return serialNumber
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func generateCert(ctx context.Context,
|
func generateCert(ctx context.Context,
|
||||||
b *backend,
|
b *backend,
|
||||||
data *dataBundle,
|
data *dataBundle,
|
||||||
@@ -695,6 +718,7 @@ func generateCreationBundle(b *backend, data *dataBundle) error {
|
|||||||
|
|
||||||
// Read in names -- CN, DNS and email addresses
|
// Read in names -- CN, DNS and email addresses
|
||||||
var cn string
|
var cn string
|
||||||
|
var ridSerialNumber string
|
||||||
dnsNames := []string{}
|
dnsNames := []string{}
|
||||||
emailAddresses := []string{}
|
emailAddresses := []string{}
|
||||||
{
|
{
|
||||||
@@ -708,6 +732,13 @@ func generateCreationBundle(b *backend, data *dataBundle) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ridSerialNumber = data.apiData.Get("serial_number").(string)
|
||||||
|
|
||||||
|
// only take serial number from CSR if one was not supplied via API
|
||||||
|
if ridSerialNumber == "" && data.csr != nil {
|
||||||
|
ridSerialNumber = data.csr.Subject.SerialNumber
|
||||||
|
}
|
||||||
|
|
||||||
if data.csr != nil && data.role.UseCSRSANs {
|
if data.csr != nil && data.role.UseCSRSANs {
|
||||||
dnsNames = data.csr.DNSNames
|
dnsNames = data.csr.DNSNames
|
||||||
emailAddresses = data.csr.EmailAddresses
|
emailAddresses = data.csr.EmailAddresses
|
||||||
@@ -774,6 +805,14 @@ func generateCreationBundle(b *backend, data *dataBundle) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ridSerialNumber != "" {
|
||||||
|
badName := validateSerialNumber(data, ridSerialNumber)
|
||||||
|
if len(badName) != 0 {
|
||||||
|
return errutil.UserError{Err: fmt.Sprintf(
|
||||||
|
"serial_number %s not allowed by this role", badName)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for bad email and/or DNS names
|
// Check for bad email and/or DNS names
|
||||||
badName := validateNames(data, dnsNames)
|
badName := validateNames(data, dnsNames)
|
||||||
if len(badName) != 0 {
|
if len(badName) != 0 {
|
||||||
@@ -845,6 +884,7 @@ func generateCreationBundle(b *backend, data *dataBundle) error {
|
|||||||
|
|
||||||
subject := pkix.Name{
|
subject := pkix.Name{
|
||||||
CommonName: cn,
|
CommonName: cn,
|
||||||
|
SerialNumber: ridSerialNumber,
|
||||||
Country: strutil.RemoveDuplicates(data.role.Country, false),
|
Country: strutil.RemoveDuplicates(data.role.Country, false),
|
||||||
Organization: strutil.RemoveDuplicates(data.role.Organization, false),
|
Organization: strutil.RemoveDuplicates(data.role.Organization, false),
|
||||||
OrganizationalUnit: strutil.RemoveDuplicates(data.role.OU, false),
|
OrganizationalUnit: strutil.RemoveDuplicates(data.role.OU, false),
|
||||||
|
|||||||
@@ -75,6 +75,13 @@ is enabled for the role, this may contain
|
|||||||
email addresses.`,
|
email addresses.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fields["serial_number"] = &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: `The requested serial number, if any. If you want
|
||||||
|
more than one, specify alternative names in
|
||||||
|
the alt_names map using OID 2.5.4.5.`,
|
||||||
|
}
|
||||||
|
|
||||||
fields["ttl"] = &framework.FieldSchema{
|
fields["ttl"] = &framework.FieldSchema{
|
||||||
Type: framework.TypeDurationSecond,
|
Type: framework.TypeDurationSecond,
|
||||||
Description: `The requested Time To Live for the certificate;
|
Description: `The requested Time To Live for the certificate;
|
||||||
@@ -162,6 +169,13 @@ this value.`,
|
|||||||
this value.`,
|
this value.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fields["serial_number"] = &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: `The requested serial number, if any. If you want
|
||||||
|
more than one, specify alternative names in
|
||||||
|
the alt_names map using OID 2.5.4.5.`,
|
||||||
|
}
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,16 +128,17 @@ func (b *backend) pathSignVerbatim(ctx context.Context, req *logical.Request, da
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry := &roleEntry{
|
entry := &roleEntry{
|
||||||
TTL: b.System().DefaultLeaseTTL(),
|
TTL: b.System().DefaultLeaseTTL(),
|
||||||
MaxTTL: b.System().MaxLeaseTTL(),
|
MaxTTL: b.System().MaxLeaseTTL(),
|
||||||
AllowLocalhost: true,
|
AllowLocalhost: true,
|
||||||
AllowAnyName: true,
|
AllowAnyName: true,
|
||||||
AllowIPSANs: true,
|
AllowIPSANs: true,
|
||||||
EnforceHostnames: false,
|
EnforceHostnames: false,
|
||||||
KeyType: "any",
|
KeyType: "any",
|
||||||
UseCSRCommonName: true,
|
UseCSRCommonName: true,
|
||||||
UseCSRSANs: true,
|
UseCSRSANs: true,
|
||||||
GenerateLease: new(bool),
|
AllowedSerialNumbers: []string{"*"},
|
||||||
|
GenerateLease: new(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
*entry.GenerateLease = false
|
*entry.GenerateLease = false
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ Any valid IP is accepted.`,
|
|||||||
Description: `If set, an array of allowed other names to put in SANs. These values support globbing.`,
|
Description: `If set, an array of allowed other names to put in SANs. These values support globbing.`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"allowed_serial_numbers": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeCommaStringSlice,
|
||||||
|
Description: `If set, an array of allowed serial numbers to put in Subject. These values support globbing.`,
|
||||||
|
},
|
||||||
|
|
||||||
"server_flag": &framework.FieldSchema{
|
"server_flag": &framework.FieldSchema{
|
||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Default: true,
|
Default: true,
|
||||||
@@ -467,6 +472,7 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data
|
|||||||
GenerateLease: new(bool),
|
GenerateLease: new(bool),
|
||||||
NoStore: data.Get("no_store").(bool),
|
NoStore: data.Get("no_store").(bool),
|
||||||
RequireCN: data.Get("require_cn").(bool),
|
RequireCN: data.Get("require_cn").(bool),
|
||||||
|
AllowedSerialNumbers: data.Get("allowed_serial_numbers").([]string),
|
||||||
PolicyIdentifiers: data.Get("policy_identifiers").([]string),
|
PolicyIdentifiers: data.Get("policy_identifiers").([]string),
|
||||||
BasicConstraintsValidForNonCA: data.Get("basic_constraints_valid_for_non_ca").(bool),
|
BasicConstraintsValidForNonCA: data.Get("basic_constraints_valid_for_non_ca").(bool),
|
||||||
}
|
}
|
||||||
@@ -602,6 +608,7 @@ type roleEntry struct {
|
|||||||
NoStore bool `json:"no_store" mapstructure:"no_store"`
|
NoStore bool `json:"no_store" mapstructure:"no_store"`
|
||||||
RequireCN bool `json:"require_cn" mapstructure:"require_cn"`
|
RequireCN bool `json:"require_cn" mapstructure:"require_cn"`
|
||||||
AllowedOtherSANs []string `json:"allowed_other_sans" mapstructure:"allowed_other_sans"`
|
AllowedOtherSANs []string `json:"allowed_other_sans" mapstructure:"allowed_other_sans"`
|
||||||
|
AllowedSerialNumbers []string `json:"allowed_serial_numbers" mapstructure:"allowed_serial_numbers"`
|
||||||
PolicyIdentifiers []string `json:"policy_identifiers" mapstructure:"policy_identifiers"`
|
PolicyIdentifiers []string `json:"policy_identifiers" mapstructure:"policy_identifiers"`
|
||||||
ExtKeyUsageOIDs []string `json:"ext_key_usage_oids" mapstructure:"ext_key_usage_oids"`
|
ExtKeyUsageOIDs []string `json:"ext_key_usage_oids" mapstructure:"ext_key_usage_oids"`
|
||||||
BasicConstraintsValidForNonCA bool `json:"basic_constraints_valid_for_non_ca" mapstructure:"basic_constraints_valid_for_non_ca"`
|
BasicConstraintsValidForNonCA bool `json:"basic_constraints_valid_for_non_ca" mapstructure:"basic_constraints_valid_for_non_ca"`
|
||||||
@@ -642,6 +649,7 @@ func (r *roleEntry) ToResponseData() map[string]interface{} {
|
|||||||
"postal_code": r.PostalCode,
|
"postal_code": r.PostalCode,
|
||||||
"no_store": r.NoStore,
|
"no_store": r.NoStore,
|
||||||
"allowed_other_sans": r.AllowedOtherSANs,
|
"allowed_other_sans": r.AllowedOtherSANs,
|
||||||
|
"allowed_serial_numbers": r.AllowedSerialNumbers,
|
||||||
"require_cn": r.RequireCN,
|
"require_cn": r.RequireCN,
|
||||||
"policy_identifiers": r.PolicyIdentifiers,
|
"policy_identifiers": r.PolicyIdentifiers,
|
||||||
"basic_constraints_valid_for_non_ca": r.BasicConstraintsValidForNonCA,
|
"basic_constraints_valid_for_non_ca": r.BasicConstraintsValidForNonCA,
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ func (b *backend) pathCASignIntermediate(ctx context.Context, req *logical.Reque
|
|||||||
AllowIPSANs: true,
|
AllowIPSANs: true,
|
||||||
EnforceHostnames: false,
|
EnforceHostnames: false,
|
||||||
KeyType: "any",
|
KeyType: "any",
|
||||||
|
AllowedSerialNumbers: []string{"*"},
|
||||||
AllowExpirationPastCA: true,
|
AllowExpirationPastCA: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user