From 935e09b143e9c73c0cf8165fa402a6cad188190e Mon Sep 17 00:00:00 2001 From: Sarat Khilar Date: Mon, 18 Oct 2021 08:31:37 +0530 Subject: [PATCH] Added notAfter and support Y10K expiry for IEEE 802.1AR-2018 --- builtin/logical/pki/backend_test.go | 137 +++++++++++++++++++++++++- builtin/logical/pki/cert_util.go | 29 +++++- builtin/logical/pki/cert_util_test.go | 12 +-- builtin/logical/pki/fields.go | 7 +- builtin/logical/pki/path_roles.go | 9 +- builtin/logical/pki/path_root.go | 1 + changelog/12795.txt | 3 + 7 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 changelog/12795.txt diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 60833c5d20df..45e56196e4f0 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -146,6 +146,135 @@ func TestPKI_RequireCN(t *testing.T) { } } +func TestPKI_DeviceCert(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("pki", &api.MountInput{ + Type: "pki", + Config: api.MountConfigInput{ + DefaultLeaseTTL: "16h", + MaxLeaseTTL: "32h", + }, + }) + if err != nil { + t.Fatal(err) + } + + resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ + "common_name": "myvault.com", + "not_after": "9999-12-31T23:59:59Z", + }) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("expected ca info") + } + var certBundle certutil.CertBundle + err = mapstructure.Decode(resp.Data, &certBundle) + if err != nil { + t.Fatal(err) + } + + parsedCertBundle, err := certBundle.ToParsedCertBundle() + if err != nil { + t.Fatal(err) + } + cert := parsedCertBundle.Certificate + notAfter := cert.NotAfter.Format(time.RFC3339) + if notAfter != "9999-12-31T23:59:59Z" { + t.Fatal(fmt.Errorf("not after from certificate is not matching with input parameter")) + } + + // Create a role which does require CN (default) + _, err = client.Logical().Write("pki/roles/example", map[string]interface{}{ + "allowed_domains": "foobar.com,zipzap.com,abc.com,xyz.com", + "allow_bare_domains": true, + "allow_subdomains": true, + "not_after": "9999-12-31T23:59:59Z", + }) + if err != nil { + t.Fatal(err) + } + + // Issue a cert with require_cn set to true and with common name supplied. + // It should succeed. + resp, err = client.Logical().Write("pki/issue/example", map[string]interface{}{ + "common_name": "foobar.com", + }) + if err != nil { + t.Fatal(err) + } + err = mapstructure.Decode(resp.Data, &certBundle) + if err != nil { + t.Fatal(err) + } + + parsedCertBundle, err = certBundle.ToParsedCertBundle() + if err != nil { + t.Fatal(err) + } + cert = parsedCertBundle.Certificate + notAfter = cert.NotAfter.Format(time.RFC3339) + if notAfter != "9999-12-31T23:59:59Z" { + t.Fatal(fmt.Errorf("not after from certificate is not matching with input parameter")) + } + +} + +func TestBackend_InvalidParameter(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("pki", &api.MountInput{ + Type: "pki", + Config: api.MountConfigInput{ + DefaultLeaseTTL: "16h", + MaxLeaseTTL: "32h", + }, + }) + if err != nil { + t.Fatal(err) + } + + _, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ + "common_name": "myvault.com", + "not_after": "9999-12-31T23:59:59Z", + "ttl": "25h", + }) + if err == nil { + t.Fatal(err) + } + + _, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ + "common_name": "myvault.com", + "not_after": "9999-12-31T23:59:59", + }) + if err == nil { + t.Fatal(err) + } +} func TestBackend_CSRValues(t *testing.T) { initTest.Do(setCerts) defaultLeaseTTLVal := time.Hour * 24 @@ -691,10 +820,10 @@ func generateCSRSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[s // Generates steps to test out various role permutations func generateRoleSteps(t *testing.T, useCSRs bool) []logicaltest.TestStep { roleVals := roleEntry{ - MaxTTL: 12 * time.Hour, - KeyType: "rsa", - KeyBits: 2048, - RequireCN: true, + MaxTTL: 12 * time.Hour, + KeyType: "rsa", + KeyBits: 2048, + RequireCN: true, SignatureBits: 256, } issueVals := certutil.IssueData{} diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 69fbb07d963a..03547fb966dd 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -332,8 +332,8 @@ func validateNames(b *backend, data *inputBundle, names []string) string { if data.role.AllowBareDomains && (strings.EqualFold(sanitizedName, currDomain) || (isEmail && strings.EqualFold(emailDomain, currDomain)) || - // Handle the use case of AllowedDomain being an email address - (isEmail && strings.EqualFold(name, currDomain))) { + // Handle the use case of AllowedDomain being an email address + (isEmail && strings.EqualFold(name, currDomain))) { valid = true break } @@ -1034,8 +1034,23 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn var ttl time.Duration var maxTTL time.Duration var notAfter time.Time + var err error { ttl = time.Duration(data.apiData.Get("ttl").(int)) * time.Second + notAfterAlt := data.role.NotAfter + if notAfterAlt == "" { + notAfterAltRaw, ok := data.apiData.GetOk("not_after") + if ok { + notAfterAlt = notAfterAltRaw.(string) + } + + } + if ttl > 0 && notAfterAlt != "" { + return nil, errutil.UserError{ + Err: fmt.Sprintf( + "Either ttl or not_after should be provided. Both should not be provided in the same request."), + } + } if ttl == 0 && data.role.TTL > 0 { ttl = data.role.TTL @@ -1055,8 +1070,14 @@ func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAIn ttl = maxTTL } - notAfter = time.Now().Add(ttl) - + if notAfterAlt != "" { + notAfter, err = time.Parse(time.RFC3339, notAfterAlt) + if err != nil { + return nil, errutil.UserError{Err: err.Error()} + } + } else { + notAfter = time.Now().Add(ttl) + } // If it's not self-signed, verify that the issued certificate won't be // valid past the lifetime of the CA certificate if caSign != nil && diff --git a/builtin/logical/pki/cert_util_test.go b/builtin/logical/pki/cert_util_test.go index d27cb7d6d27a..2d8dd04dd241 100644 --- a/builtin/logical/pki/cert_util_test.go +++ b/builtin/logical/pki/cert_util_test.go @@ -163,9 +163,9 @@ func TestPki_PermitFQDNs(t *testing.T) { fields := addCACommonFields(map[string]*framework.FieldSchema{}) cases := map[string]struct { - input *inputBundle + input *inputBundle expectedDnsNames []string - expectedEmails []string + expectedEmails []string }{ "base valid case": { input: &inputBundle{ @@ -183,7 +183,7 @@ func TestPki_PermitFQDNs(t *testing.T) { }, }, expectedDnsNames: []string{"example.com."}, - expectedEmails: []string{}, + expectedEmails: []string{}, }, "case insensitivity validation": { input: &inputBundle{ @@ -202,7 +202,7 @@ func TestPki_PermitFQDNs(t *testing.T) { }, }, expectedDnsNames: []string{"Example.Net", "eXaMPLe.COM"}, - expectedEmails: []string{}, + expectedEmails: []string{}, }, "case email as AllowedDomain with bare domains": { input: &inputBundle{ @@ -220,7 +220,7 @@ func TestPki_PermitFQDNs(t *testing.T) { }, }, expectedDnsNames: []string{}, - expectedEmails: []string{"test@testemail.com"}, + expectedEmails: []string{"test@testemail.com"}, }, "case email common name with bare domains": { input: &inputBundle{ @@ -238,7 +238,7 @@ func TestPki_PermitFQDNs(t *testing.T) { }, }, expectedDnsNames: []string{}, - expectedEmails: []string{"test@testemail.com"}, + expectedEmails: []string{"test@testemail.com"}, }, } diff --git a/builtin/logical/pki/fields.go b/builtin/logical/pki/fields.go index 5680a5b2d7d6..9e191f3ad082 100644 --- a/builtin/logical/pki/fields.go +++ b/builtin/logical/pki/fields.go @@ -228,6 +228,11 @@ this value.`, more than one, specify alternative names in the alt_names map using OID 2.5.4.5.`, } + fields["not_after"] = &framework.FieldSchema{ + Type: framework.TypeString, + Description: `Set the not after field of the certificate with specified date value. + The value format should be given in UTC format YYYY-MM-ddTHH:MM:SSZ`, + } return fields } @@ -255,7 +260,7 @@ the key_type.`, } fields["signature_bits"] = &framework.FieldSchema{ - Type: framework.TypeInt, + Type: framework.TypeInt, Default: 256, Description: `The number of bits to use in the signature algorithm. Defaults to 256 for SHA256. diff --git a/builtin/logical/pki/path_roles.go b/builtin/logical/pki/path_roles.go index 4cfa666cfcc4..a240a6a3c691 100644 --- a/builtin/logical/pki/path_roles.go +++ b/builtin/logical/pki/path_roles.go @@ -377,6 +377,11 @@ for "generate_lease".`, Value: 30, }, }, + "not_after": { + Type: framework.TypeString, + Description: `Set the not after field of the certificate with specified date value. + The value format should be given in UTC format YYYY-MM-ddTHH:MM:SSZ`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -587,6 +592,7 @@ func (b *backend) pathRoleCreate(ctx context.Context, req *logical.Request, data PolicyIdentifiers: data.Get("policy_identifiers").([]string), BasicConstraintsValidForNonCA: data.Get("basic_constraints_valid_for_non_ca").(bool), NotBeforeDuration: time.Duration(data.Get("not_before_duration").(int)) * time.Second, + NotAfter: data.Get("not_after").(string), } allowedOtherSANs := data.Get("allowed_other_sans").([]string) @@ -787,7 +793,7 @@ type roleEntry struct { 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"` NotBeforeDuration time.Duration `json:"not_before_duration" mapstructure:"not_before_duration"` - + NotAfter string `json:"not_after" mapstructure:"not_after"` // Used internally for signing intermediates AllowExpirationPastCA bool } @@ -833,6 +839,7 @@ func (r *roleEntry) ToResponseData() map[string]interface{} { "policy_identifiers": r.PolicyIdentifiers, "basic_constraints_valid_for_non_ca": r.BasicConstraintsValidForNonCA, "not_before_duration": int64(r.NotBeforeDuration.Seconds()), + "not_after": r.NotAfter, } if r.MaxPathLength != nil { responseData["max_path_length"] = r.MaxPathLength diff --git a/builtin/logical/pki/path_root.go b/builtin/logical/pki/path_root.go index 253168810d0e..ea45eab6ae41 100644 --- a/builtin/logical/pki/path_root.go +++ b/builtin/logical/pki/path_root.go @@ -282,6 +282,7 @@ func (b *backend) pathCASignIntermediate(ctx context.Context, req *logical.Reque AllowedURISANs: []string{"*"}, AllowedSerialNumbers: []string{"*"}, AllowExpirationPastCA: true, + NotAfter: data.Get("not_after").(string), } if cn := data.Get("common_name").(string); len(cn) == 0 { diff --git a/changelog/12795.txt b/changelog/12795.txt new file mode 100644 index 000000000000..fb4ebcc5586f --- /dev/null +++ b/changelog/12795.txt @@ -0,0 +1,3 @@ +```release-note:feature +core/pki: Support Y10K value in notAfter field to be compliant with IEEE 802.1AR-2018 standard +``` \ No newline at end of file