mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +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)
|
||||
}
|
||||
|
||||
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() {
|
||||
cak, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
|
||||
@@ -29,20 +29,21 @@ func (b *backend) getGenerationParams(
|
||||
}
|
||||
|
||||
role = &roleEntry{
|
||||
TTL: time.Duration(data.Get("ttl").(int)) * time.Second,
|
||||
KeyType: data.Get("key_type").(string),
|
||||
KeyBits: data.Get("key_bits").(int),
|
||||
AllowLocalhost: true,
|
||||
AllowAnyName: true,
|
||||
AllowIPSANs: true,
|
||||
EnforceHostnames: false,
|
||||
OU: data.Get("ou").([]string),
|
||||
Organization: data.Get("organization").([]string),
|
||||
Country: data.Get("country").([]string),
|
||||
Locality: data.Get("locality").([]string),
|
||||
Province: data.Get("province").([]string),
|
||||
StreetAddress: data.Get("street_address").([]string),
|
||||
PostalCode: data.Get("postal_code").([]string),
|
||||
TTL: time.Duration(data.Get("ttl").(int)) * time.Second,
|
||||
KeyType: data.Get("key_type").(string),
|
||||
KeyBits: data.Get("key_bits").(int),
|
||||
AllowLocalhost: true,
|
||||
AllowAnyName: true,
|
||||
AllowIPSANs: true,
|
||||
EnforceHostnames: false,
|
||||
AllowedSerialNumbers: []string{"*"},
|
||||
OU: data.Get("ou").([]string),
|
||||
Organization: data.Get("organization").([]string),
|
||||
Country: data.Get("country").([]string),
|
||||
Locality: data.Get("locality").([]string),
|
||||
Province: data.Get("province").([]string),
|
||||
StreetAddress: data.Get("street_address").([]string),
|
||||
PostalCode: data.Get("postal_code").([]string),
|
||||
}
|
||||
|
||||
if role.KeyType == "rsa" && role.KeyBits < 2048 {
|
||||
|
||||
@@ -498,6 +498,29 @@ func parseOtherSANs(others []string) (map[string][]string, error) {
|
||||
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,
|
||||
b *backend,
|
||||
data *dataBundle,
|
||||
@@ -695,6 +718,7 @@ func generateCreationBundle(b *backend, data *dataBundle) error {
|
||||
|
||||
// Read in names -- CN, DNS and email addresses
|
||||
var cn string
|
||||
var ridSerialNumber string
|
||||
dnsNames := []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 {
|
||||
dnsNames = data.csr.DNSNames
|
||||
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
|
||||
badName := validateNames(data, dnsNames)
|
||||
if len(badName) != 0 {
|
||||
@@ -845,6 +884,7 @@ func generateCreationBundle(b *backend, data *dataBundle) error {
|
||||
|
||||
subject := pkix.Name{
|
||||
CommonName: cn,
|
||||
SerialNumber: ridSerialNumber,
|
||||
Country: strutil.RemoveDuplicates(data.role.Country, false),
|
||||
Organization: strutil.RemoveDuplicates(data.role.Organization, false),
|
||||
OrganizationalUnit: strutil.RemoveDuplicates(data.role.OU, false),
|
||||
|
||||
@@ -75,6 +75,13 @@ is enabled for the role, this may contain
|
||||
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{
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: `The requested Time To Live for the certificate;
|
||||
@@ -162,6 +169,13 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -128,16 +128,17 @@ func (b *backend) pathSignVerbatim(ctx context.Context, req *logical.Request, da
|
||||
}
|
||||
|
||||
entry := &roleEntry{
|
||||
TTL: b.System().DefaultLeaseTTL(),
|
||||
MaxTTL: b.System().MaxLeaseTTL(),
|
||||
AllowLocalhost: true,
|
||||
AllowAnyName: true,
|
||||
AllowIPSANs: true,
|
||||
EnforceHostnames: false,
|
||||
KeyType: "any",
|
||||
UseCSRCommonName: true,
|
||||
UseCSRSANs: true,
|
||||
GenerateLease: new(bool),
|
||||
TTL: b.System().DefaultLeaseTTL(),
|
||||
MaxTTL: b.System().MaxLeaseTTL(),
|
||||
AllowLocalhost: true,
|
||||
AllowAnyName: true,
|
||||
AllowIPSANs: true,
|
||||
EnforceHostnames: false,
|
||||
KeyType: "any",
|
||||
UseCSRCommonName: true,
|
||||
UseCSRSANs: true,
|
||||
AllowedSerialNumbers: []string{"*"},
|
||||
GenerateLease: new(bool),
|
||||
}
|
||||
|
||||
*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.`,
|
||||
},
|
||||
|
||||
"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{
|
||||
Type: framework.TypeBool,
|
||||
Default: true,
|
||||
@@ -467,6 +472,7 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data
|
||||
GenerateLease: new(bool),
|
||||
NoStore: data.Get("no_store").(bool),
|
||||
RequireCN: data.Get("require_cn").(bool),
|
||||
AllowedSerialNumbers: data.Get("allowed_serial_numbers").([]string),
|
||||
PolicyIdentifiers: data.Get("policy_identifiers").([]string),
|
||||
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"`
|
||||
RequireCN bool `json:"require_cn" mapstructure:"require_cn"`
|
||||
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"`
|
||||
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"`
|
||||
@@ -642,6 +649,7 @@ func (r *roleEntry) ToResponseData() map[string]interface{} {
|
||||
"postal_code": r.PostalCode,
|
||||
"no_store": r.NoStore,
|
||||
"allowed_other_sans": r.AllowedOtherSANs,
|
||||
"allowed_serial_numbers": r.AllowedSerialNumbers,
|
||||
"require_cn": r.RequireCN,
|
||||
"policy_identifiers": r.PolicyIdentifiers,
|
||||
"basic_constraints_valid_for_non_ca": r.BasicConstraintsValidForNonCA,
|
||||
|
||||
@@ -268,6 +268,7 @@ func (b *backend) pathCASignIntermediate(ctx context.Context, req *logical.Reque
|
||||
AllowIPSANs: true,
|
||||
EnforceHostnames: false,
|
||||
KeyType: "any",
|
||||
AllowedSerialNumbers: []string{"*"},
|
||||
AllowExpirationPastCA: true,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user