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)
}
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 {

View File

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

View File

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

View File

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

View File

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

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.`,
},
"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,

View File

@@ -268,6 +268,7 @@ func (b *backend) pathCASignIntermediate(ctx context.Context, req *logical.Reque
AllowIPSANs: true,
EnforceHostnames: false,
KeyType: "any",
AllowedSerialNumbers: []string{"*"},
AllowExpirationPastCA: true,
}