Add support for x.509 Name Serial Number attribute in subject of certificates (#4694)

This commit is contained in:
Marcin Wielgoszewski
2018-06-04 23:18:39 -04:00
committed by Jeff Mitchell
parent 063d9ed756
commit a8f343c32e
7 changed files with 212 additions and 24 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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,

View File

@@ -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,
} }