// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package pki import ( "context" "crypto/x509" "crypto/x509/pkix" "fmt" "net" "net/url" "os" "reflect" "strings" "testing" "time" "github.com/go-test/deep" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/vault/builtin/logical/pki/issuing" "github.com/hashicorp/vault/builtin/logical/pki/parsing" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/certutil" "github.com/hashicorp/vault/sdk/logical" "github.com/stretchr/testify/require" ) func TestPki_FetchCertBySerial(t *testing.T) { t.Parallel() b, storage := CreateBackendWithStorage(t) sc := b.makeStorageContext(ctx, storage) cases := map[string]struct { Req *logical.Request Prefix string Serial string }{ "valid cert": { &logical.Request{ Storage: storage, }, issuing.PathCerts, "00:00:00:00:00:00:00:00", }, "revoked cert": { &logical.Request{ Storage: storage, }, "revoked/", "11:11:11:11:11:11:11:11", }, } // Test for colon-based paths in storage for name, tc := range cases { storageKey := fmt.Sprintf("%s%s", tc.Prefix, tc.Serial) err := storage.Put(context.Background(), &logical.StorageEntry{ Key: storageKey, Value: []byte("some data"), }) if err != nil { t.Fatalf("error writing to storage on %s colon-based storage path: %s", name, err) } certEntry, err := fetchCertBySerial(sc, tc.Prefix, tc.Serial) if err != nil { t.Fatalf("error on %s for colon-based storage path: %s", name, err) } // Check for non-nil on valid/revoked certs if certEntry == nil { t.Fatalf("nil on %s for colon-based storage path", name) } // Ensure that cert serials are converted/updated after fetch expectedKey := tc.Prefix + normalizeSerial(tc.Serial) se, err := storage.Get(context.Background(), expectedKey) if err != nil { t.Fatalf("error on %s for colon-based storage path:%s", name, err) } if strings.Compare(expectedKey, se.Key) != 0 { t.Fatalf("expected: %s, got: %s", expectedKey, certEntry.Key) } } // Reset storage storage = &logical.InmemStorage{} // Test for hyphen-base paths in storage for name, tc := range cases { storageKey := tc.Prefix + normalizeSerial(tc.Serial) err := storage.Put(context.Background(), &logical.StorageEntry{ Key: storageKey, Value: []byte("some data"), }) if err != nil { t.Fatalf("error writing to storage on %s hyphen-based storage path: %s", name, err) } certEntry, err := fetchCertBySerial(sc, tc.Prefix, tc.Serial) if err != nil || certEntry == nil { t.Fatalf("error on %s for hyphen-based storage path: err: %v, entry: %v", name, err, certEntry) } } } // Demonstrate that multiple OUs in the name are handled in an // order-preserving way. func TestPki_MultipleOUs(t *testing.T) { t.Parallel() b, _ := CreateBackendWithStorage(t) fields := addCACommonFields(map[string]*framework.FieldSchema{}) apiData := &framework.FieldData{ Schema: fields, Raw: map[string]interface{}{ "cn": "example.com", "ttl": 3600, }, } input := &inputBundle{ apiData: apiData, role: &issuing.RoleEntry{ MaxTTL: 3600, OU: []string{"Z", "E", "V"}, }, } cb, _, err := generateCreationBundle(b.System(), input, nil, nil) if err != nil { t.Fatalf("Error: %v", err) } expected := []string{"Z", "E", "V"} actual := cb.Params.Subject.OrganizationalUnit if !reflect.DeepEqual(expected, actual) { t.Fatalf("Expected %v, got %v", expected, actual) } } func TestPki_PermitFQDNs(t *testing.T) { t.Parallel() b, _ := CreateBackendWithStorage(t) fields := addCACommonFields(map[string]*framework.FieldSchema{}) cases := map[string]struct { input *inputBundle expectedDnsNames []string expectedEmails []string }{ "base valid case": { input: &inputBundle{ apiData: &framework.FieldData{ Schema: fields, Raw: map[string]interface{}{ "common_name": "example.com.", "ttl": 3600, }, }, role: &issuing.RoleEntry{ AllowAnyName: true, MaxTTL: 3600, EnforceHostnames: true, }, }, expectedDnsNames: []string{"example.com."}, expectedEmails: []string{}, }, "case insensitivity validation": { input: &inputBundle{ apiData: &framework.FieldData{ Schema: fields, Raw: map[string]interface{}{ "common_name": "Example.Net", "alt_names": "eXaMPLe.COM", "ttl": 3600, }, }, role: &issuing.RoleEntry{ AllowedDomains: []string{"example.net", "EXAMPLE.COM"}, AllowBareDomains: true, MaxTTL: 3600, }, }, expectedDnsNames: []string{"Example.Net", "eXaMPLe.COM"}, expectedEmails: []string{}, }, "case insensitivity subdomain validation": { input: &inputBundle{ apiData: &framework.FieldData{ Schema: fields, Raw: map[string]interface{}{ "common_name": "SUB.EXAMPLE.COM", "ttl": 3600, }, }, role: &issuing.RoleEntry{ AllowedDomains: []string{"example.com", "*.Example.com"}, AllowGlobDomains: true, MaxTTL: 3600, }, }, expectedDnsNames: []string{"SUB.EXAMPLE.COM"}, expectedEmails: []string{}, }, "case email as AllowedDomain with bare domains": { input: &inputBundle{ apiData: &framework.FieldData{ Schema: fields, Raw: map[string]interface{}{ "common_name": "test@testemail.com", "ttl": 3600, }, }, role: &issuing.RoleEntry{ AllowedDomains: []string{"test@testemail.com"}, AllowBareDomains: true, MaxTTL: 3600, }, }, expectedDnsNames: []string{}, expectedEmails: []string{"test@testemail.com"}, }, "case email common name with bare domains": { input: &inputBundle{ apiData: &framework.FieldData{ Schema: fields, Raw: map[string]interface{}{ "common_name": "test@testemail.com", "ttl": 3600, }, }, role: &issuing.RoleEntry{ AllowedDomains: []string{"testemail.com"}, AllowBareDomains: true, MaxTTL: 3600, }, }, expectedDnsNames: []string{}, expectedEmails: []string{"test@testemail.com"}, }, } for name, testCase := range cases { name := name testCase := testCase t.Run(name, func(t *testing.T) { cb, _, err := generateCreationBundle(b.System(), testCase.input, nil, nil) if err != nil { t.Fatalf("Error: %v", err) } actualDnsNames := cb.Params.DNSNames if !reflect.DeepEqual(testCase.expectedDnsNames, actualDnsNames) { t.Fatalf("Expected dns names %v, got %v", testCase.expectedDnsNames, actualDnsNames) } actualEmails := cb.Params.EmailAddresses if !reflect.DeepEqual(testCase.expectedEmails, actualEmails) { t.Fatalf("Expected email addresses %v, got %v", testCase.expectedEmails, actualEmails) } }) } } type parseCertificateTestCase struct { name string data map[string]interface{} roleData map[string]interface{} // if a role is to be created ttl time.Duration wantParams certutil.CreationParameters wantFields map[string]interface{} wantErr bool } // TestDisableVerifyCertificateEnvVar verifies that env var VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION // can be used to disable cert verification. func TestDisableVerifyCertificateEnvVar(t *testing.T) { caData := map[string]any{ // Copied from the "full CA" test case of TestParseCertificate, // with tweaked permitted_dns_domains and ttl "common_name": "the common name", "alt_names": "user@example.com,admin@example.com,example.com,www.example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", "ttl": "3h", "max_path_length": 2, "permitted_dns_domains": ".example.com,.www.example.com", "ou": "unit1, unit2", "organization": "org1, org2", "country": "US, CA", "locality": "locality1, locality2", "province": "province1, province2", "street_address": "street_address1, street_address2", "postal_code": "postal_code1, postal_code2", "not_before_duration": "45s", "key_type": "rsa", "use_pss": true, "key_bits": 2048, "signature_bits": 384, } roleData := map[string]any{ "allow_any_name": true, "cn_validations": "disabled", "allow_ip_sans": true, "allowed_other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:*@example.com", "allowed_uri_sans": "https://example.com,https://www.example.com", "allowed_user_ids": "*", "not_before_duration": "45s", "signature_bits": 384, "key_usage": "KeyAgreement", "ext_key_usage": "ServerAuth", "ext_key_usage_oids": "1.3.6.1.5.5.7.3.67,1.3.6.1.5.5.7.3.68", "client_flag": false, "server_flag": false, "policy_identifiers": "1.2.3.4.5.6.7.8.9.0", } certData := map[string]any{ // using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#generate-certificate-and-key "common_name": "the common name non ca", "alt_names": "user@example.com,admin@example.com,example.com,www.example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", "ttl": "2h", // format // private_key_format "exclude_cn_from_sans": true, // not_after // remove_roots_from_chain "user_ids": "humanoid,robot", } defer func() { os.Unsetenv("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION") }() b, s := CreateBackendWithStorage(t) // Create the CA resp, err := CBWrite(b, s, "root/generate/internal", caData) require.NoError(t, err) require.NotNil(t, resp) // Create the role resp, err = CBWrite(b, s, "roles/test", roleData) require.NoError(t, err) require.NotNil(t, resp) // Try to create the cert -- should fail verification, since example.com is not allowed t.Run("no VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION env var", func(t *testing.T) { resp, err = CBWrite(b, s, "issue/test", certData) require.ErrorContains(t, err, `DNS name "example.com" is not permitted by any constraint`) }) // Try to create the cert -- should fail verification, since example.com is not allowed t.Run("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION=false", func(t *testing.T) { os.Setenv("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION", "false") resp, err = CBWrite(b, s, "issue/test", certData) require.ErrorContains(t, err, `DNS name "example.com" is not permitted by any constraint`) }) // Create the cert, should succeed with the disable env var set t.Run("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION=true", func(t *testing.T) { os.Setenv("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION", "true") resp, err = CBWrite(b, s, "issue/test", certData) require.NoError(t, err) require.NotNil(t, resp) }) // Invalid env var t.Run("invalid VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION", func(t *testing.T) { os.Setenv("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION", "invalid") resp, err = CBWrite(b, s, "issue/test", certData) require.ErrorContains(t, err, "failed parsing environment variable VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION") }) } func TestParseCertificate(t *testing.T) { t.Parallel() parseURL := func(s string) *url.URL { u, err := url.Parse(s) if err != nil { t.Fatal(err) } return u } tests := []*parseCertificateTestCase{ { name: "simple CA", data: map[string]interface{}{ "common_name": "the common name", "key_type": "ec", "key_bits": 384, "ttl": "1h", "not_before_duration": "30s", "street_address": "", }, ttl: 1 * time.Hour, wantParams: certutil.CreationParameters{ Subject: pkix.Name{ CommonName: "the common name", }, DNSNames: nil, EmailAddresses: nil, IPAddresses: nil, URIs: nil, OtherSANs: make(map[string][]string), IsCA: true, KeyType: "ec", KeyBits: 384, NotAfter: time.Time{}, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, ExtKeyUsage: 0, ExtKeyUsageOIDs: nil, PolicyIdentifiers: nil, BasicConstraintsValidForNonCA: false, SignatureBits: 384, UsePSS: false, ForceAppendCaChain: false, UseCSRValues: false, PermittedDNSDomains: nil, URLs: nil, MaxPathLength: -1, NotBeforeDuration: 30, SKID: []byte("We'll assert that it is not nil as an special case"), }, wantFields: map[string]interface{}{ "common_name": "the common name", "alt_names": "", "ip_sans": "", "uri_sans": "", "other_sans": "", "signature_bits": 384, "exclude_cn_from_sans": true, "ou": "", "organization": "", "country": "", "locality": "", "province": "", "street_address": "", "postal_code": "", "serial_number": "", "ttl": "1h0m30s", "max_path_length": -1, "permitted_dns_domains": "", "use_pss": false, "key_type": "ec", "key_bits": 384, "skid": "We'll assert that it is not nil as an special case", }, wantErr: false, }, { // Note that this test's data is used to create the internal CA used by test "full non CA cert" name: "full CA", data: map[string]interface{}{ // using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#sign-certificate "common_name": "the common name", "alt_names": "user@example.com,admin@example.com,example.com,www.example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", "ttl": "2h", "max_path_length": 2, "permitted_dns_domains": "example.com,.example.com,.www.example.com", "ou": "unit1, unit2", "organization": "org1, org2", "country": "US, CA", "locality": "locality1, locality2", "province": "province1, province2", "street_address": "street_address1, street_address2", "postal_code": "postal_code1, postal_code2", "not_before_duration": "45s", "key_type": "rsa", "use_pss": true, "key_bits": 2048, "signature_bits": 384, // TODO(kitography): Specify key usage }, ttl: 2 * time.Hour, wantParams: certutil.CreationParameters{ Subject: pkix.Name{ CommonName: "the common name", OrganizationalUnit: []string{"unit1", "unit2"}, Organization: []string{"org1", "org2"}, Country: []string{"CA", "US"}, Locality: []string{"locality1", "locality2"}, Province: []string{"province1", "province2"}, StreetAddress: []string{"street_address1", "street_address2"}, PostalCode: []string{"postal_code1", "postal_code2"}, }, DNSNames: []string{"example.com", "www.example.com"}, EmailAddresses: []string{"admin@example.com", "user@example.com"}, IPAddresses: []net.IP{[]byte{1, 2, 3, 4}, []byte{1, 2, 3, 5}}, URIs: []*url.URL{parseURL("https://example.com"), parseURL("https://www.example.com")}, OtherSANs: map[string][]string{"1.3.6.1.4.1.311.20.2.3": {"caadmin@example.com"}}, IsCA: true, KeyType: "rsa", KeyBits: 2048, NotAfter: time.Time{}, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, ExtKeyUsage: 0, ExtKeyUsageOIDs: nil, PolicyIdentifiers: nil, BasicConstraintsValidForNonCA: false, SignatureBits: 384, UsePSS: true, ForceAppendCaChain: false, UseCSRValues: false, PermittedDNSDomains: []string{"example.com", ".example.com", ".www.example.com"}, URLs: nil, MaxPathLength: 2, NotBeforeDuration: 45 * time.Second, SKID: []byte("We'll assert that it is not nil as an special case"), }, wantFields: map[string]interface{}{ "common_name": "the common name", "alt_names": "example.com,www.example.com,admin@example.com,user@example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", "signature_bits": 384, "exclude_cn_from_sans": true, "ou": "unit1,unit2", "organization": "org1,org2", "country": "CA,US", "locality": "locality1,locality2", "province": "province1,province2", "street_address": "street_address1,street_address2", "postal_code": "postal_code1,postal_code2", "serial_number": "", "ttl": "2h0m45s", "max_path_length": 2, "permitted_dns_domains": "example.com,.example.com,.www.example.com", "use_pss": true, "key_type": "rsa", "key_bits": 2048, "skid": "We'll assert that it is not nil as an special case", }, wantErr: false, }, { // Note that we use the data of test "full CA" to create the internal CA needed for this test name: "full non CA cert", data: map[string]interface{}{ // using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#generate-certificate-and-key "common_name": "the common name non ca", "alt_names": "user@example.com,admin@example.com,example.com,www.example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", "ttl": "2h", // format // private_key_format "exclude_cn_from_sans": true, // not_after // remove_roots_from_chain "user_ids": "humanoid,robot", }, roleData: map[string]interface{}{ "allow_any_name": true, "cn_validations": "disabled", "allow_ip_sans": true, "allowed_other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:*@example.com", "allowed_uri_sans": "https://example.com,https://www.example.com", "allowed_user_ids": "*", "not_before_duration": "45s", "signature_bits": 384, "key_usage": "KeyAgreement", "ext_key_usage": "ServerAuth", "ext_key_usage_oids": "1.3.6.1.5.5.7.3.67,1.3.6.1.5.5.7.3.68", "client_flag": false, "server_flag": false, "policy_identifiers": "1.2.3.4.5.6.7.8.9.0", }, ttl: 2 * time.Hour, wantParams: certutil.CreationParameters{ Subject: pkix.Name{ CommonName: "the common name non ca", }, DNSNames: []string{"example.com", "www.example.com"}, EmailAddresses: []string{"admin@example.com", "user@example.com"}, IPAddresses: []net.IP{[]byte{1, 2, 3, 4}, []byte{1, 2, 3, 5}}, URIs: []*url.URL{parseURL("https://example.com"), parseURL("https://www.example.com")}, OtherSANs: map[string][]string{"1.3.6.1.4.1.311.20.2.3": {"caadmin@example.com"}}, IsCA: false, KeyType: "rsa", KeyBits: 2048, NotAfter: time.Time{}, KeyUsage: x509.KeyUsageKeyAgreement, ExtKeyUsage: 0, // Please Ignore ExtKeyUsageOIDs: []string{"1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.67", "1.3.6.1.5.5.7.3.68"}, PolicyIdentifiers: []string{"1.2.3.4.5.6.7.8.9.0"}, BasicConstraintsValidForNonCA: false, SignatureBits: 384, UsePSS: false, ForceAppendCaChain: false, UseCSRValues: false, PermittedDNSDomains: nil, URLs: nil, MaxPathLength: 0, NotBeforeDuration: 45, SKID: []byte("We'll assert that it is not nil as an special case"), }, wantFields: map[string]interface{}{ "common_name": "the common name non ca", "alt_names": "example.com,www.example.com,admin@example.com,user@example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", "signature_bits": 384, "exclude_cn_from_sans": true, "ou": "", "organization": "", "country": "", "locality": "", "province": "", "street_address": "", "postal_code": "", "serial_number": "", "ttl": "2h0m45s", "max_path_length": 0, "permitted_dns_domains": "", "use_pss": false, "key_type": "rsa", "key_bits": 2048, "skid": "We'll assert that it is not nil as an special case", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b, s := CreateBackendWithStorage(t) var cert *x509.Certificate issueTime := time.Now() if tt.wantParams.IsCA { resp, err := CBWrite(b, s, "root/generate/internal", tt.data) require.NoError(t, err) require.NotNil(t, resp) certData := resp.Data["certificate"].(string) cert, err = parsing.ParseCertificateFromString(certData) require.NoError(t, err) require.NotNil(t, cert) } else { // use the "simple CA" data to create the internal CA caData := tests[1].data caData["ttl"] = "3h" resp, err := CBWrite(b, s, "root/generate/internal", caData) require.NoError(t, err) require.NotNil(t, resp) // create a role resp, err = CBWrite(b, s, "roles/test", tt.roleData) require.NoError(t, err) require.NotNil(t, resp) // create the cert resp, err = CBWrite(b, s, "issue/test", tt.data) require.NoError(t, err) require.NotNil(t, resp) certData := resp.Data["certificate"].(string) cert, err = parsing.ParseCertificateFromString(certData) require.NoError(t, err) require.NotNil(t, cert) } t.Run(tt.name+" parameters", func(t *testing.T) { testParseCertificateToCreationParameters(t, issueTime, tt, cert) }) t.Run(tt.name+" fields", func(t *testing.T) { testParseCertificateToFields(t, issueTime, tt, cert) }) }) } } func testParseCertificateToCreationParameters(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, cert *x509.Certificate) { params, err := certutil.ParseCertificateToCreationParameters(*cert) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) ignoreBasicConstraintsValidForNonCA := tt.wantParams.IsCA var diff []string for _, d := range deep.Equal(tt.wantParams, params) { switch { case strings.HasPrefix(d, "SKID"): continue case strings.HasPrefix(d, "BasicConstraintsValidForNonCA") && ignoreBasicConstraintsValidForNonCA: continue case strings.HasPrefix(d, "NotBeforeDuration"): continue case strings.HasPrefix(d, "NotAfter"): continue } diff = append(diff, d) } if diff != nil { t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.Join(diff, "\n")) } require.NotNil(t, params.SKID) require.GreaterOrEqual(t, params.NotBeforeDuration, tt.wantParams.NotBeforeDuration, "NotBeforeDuration want: %s got: %s", tt.wantParams.NotBeforeDuration, params.NotBeforeDuration) require.GreaterOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(-1*time.Minute), "NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter) require.LessOrEqual(t, params.NotAfter, issueTime.Add(tt.ttl).Add(1*time.Minute), "NotAfter want: %s got: %s", tt.wantParams.NotAfter, params.NotAfter) } } func testParseCertificateToFields(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, cert *x509.Certificate) { fields, err := certutil.ParseCertificateToFields(*cert) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) require.NotNil(t, fields["skid"]) delete(fields, "skid") delete(tt.wantFields, "skid") { // Sometimes TTL comes back as 1s off, so we'll allow that expectedTTL, err := parseutil.ParseDurationSecond(tt.wantFields["ttl"].(string)) require.NoError(t, err) actualTTL, err := parseutil.ParseDurationSecond(fields["ttl"].(string)) require.NoError(t, err) diff := expectedTTL - actualTTL require.LessOrEqual(t, actualTTL, expectedTTL, // NotAfter is generated before NotBefore so the time.Now of notBefore may be later, shrinking our calculated TTL during very slow tests "ttl should be, if off, smaller than expected want: %s got: %s", tt.wantFields["ttl"], fields["ttl"]) require.LessOrEqual(t, diff, 30*time.Second, // Test can be slow, allow more off in the other direction "ttl must be at most 30s off, want: %s got: %s", tt.wantFields["ttl"], fields["ttl"]) delete(fields, "ttl") delete(tt.wantFields, "ttl") } if diff := deep.Equal(tt.wantFields, fields); diff != nil { t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) } } } func TestParseCsr(t *testing.T) { t.Parallel() parseURL := func(s string) *url.URL { u, err := url.Parse(s) if err != nil { t.Fatal(err) } return u } tests := []*parseCertificateTestCase{ { name: "simple CSR", data: map[string]interface{}{ "common_name": "the common name", "key_type": "ec", "key_bits": 384, "ttl": "1h", "not_before_duration": "30s", "street_address": "", }, ttl: 1 * time.Hour, wantParams: certutil.CreationParameters{ Subject: pkix.Name{ CommonName: "the common name", }, DNSNames: nil, EmailAddresses: nil, IPAddresses: nil, URIs: nil, OtherSANs: make(map[string][]string), IsCA: false, KeyType: "ec", KeyBits: 384, NotAfter: time.Time{}, KeyUsage: 0, ExtKeyUsage: 0, ExtKeyUsageOIDs: nil, PolicyIdentifiers: nil, BasicConstraintsValidForNonCA: false, SignatureBits: 384, UsePSS: false, ForceAppendCaChain: false, UseCSRValues: false, PermittedDNSDomains: nil, URLs: nil, MaxPathLength: 0, NotBeforeDuration: 0, SKID: nil, }, wantFields: map[string]interface{}{ "common_name": "the common name", "ou": "", "organization": "", "country": "", "locality": "", "province": "", "street_address": "", "postal_code": "", "alt_names": "", "ip_sans": "", "uri_sans": "", "other_sans": "", "exclude_cn_from_sans": true, "key_type": "ec", "key_bits": 384, "signature_bits": 384, "use_pss": false, "serial_number": "", "add_basic_constraints": false, }, wantErr: false, }, { name: "full CSR with basic constraints", data: map[string]interface{}{ // using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#generate-intermediate-csr "common_name": "the common name", "alt_names": "user@example.com,admin@example.com,example.com,www.example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", // format // private_key_format "key_type": "rsa", "key_bits": 2048, "key_name": "the-key-name", // key_ref "signature_bits": 384, // exclude_cn_from_sans "ou": "unit1, unit2", "organization": "org1, org2", "country": "US, CA", "locality": "locality1, locality2", "province": "province1, province2", "street_address": "street_address1, street_address2", "postal_code": "postal_code1, postal_code2", "serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", "add_basic_constraints": true, }, ttl: 2 * time.Hour, wantParams: certutil.CreationParameters{ Subject: pkix.Name{ CommonName: "the common name", OrganizationalUnit: []string{"unit1", "unit2"}, Organization: []string{"org1", "org2"}, Country: []string{"CA", "US"}, Locality: []string{"locality1", "locality2"}, Province: []string{"province1", "province2"}, StreetAddress: []string{"street_address1", "street_address2"}, PostalCode: []string{"postal_code1", "postal_code2"}, SerialNumber: "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", }, DNSNames: []string{"example.com", "www.example.com"}, EmailAddresses: []string{"admin@example.com", "user@example.com"}, IPAddresses: []net.IP{[]byte{1, 2, 3, 4}, []byte{1, 2, 3, 5}}, URIs: []*url.URL{parseURL("https://example.com"), parseURL("https://www.example.com")}, OtherSANs: map[string][]string{"1.3.6.1.4.1.311.20.2.3": {"caadmin@example.com"}}, IsCA: true, KeyType: "rsa", KeyBits: 2048, NotAfter: time.Time{}, KeyUsage: 0, // TODO(kitography): Verify with Kit ExtKeyUsage: 0, // TODO(kitography): Verify with Kit ExtKeyUsageOIDs: nil, // TODO(kitography): Verify with Kit PolicyIdentifiers: nil, // TODO(kitography): Verify with Kit BasicConstraintsValidForNonCA: true, SignatureBits: 384, UsePSS: false, ForceAppendCaChain: false, UseCSRValues: false, PermittedDNSDomains: nil, URLs: nil, MaxPathLength: -1, NotBeforeDuration: 0, SKID: nil, }, wantFields: map[string]interface{}{ "common_name": "the common name", "ou": "unit1,unit2", "organization": "org1,org2", "country": "CA,US", "locality": "locality1,locality2", "province": "province1,province2", "street_address": "street_address1,street_address2", "postal_code": "postal_code1,postal_code2", "alt_names": "example.com,www.example.com,admin@example.com,user@example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", "exclude_cn_from_sans": true, "key_type": "rsa", "key_bits": 2048, "signature_bits": 384, "use_pss": false, "serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", "add_basic_constraints": true, }, wantErr: false, }, { name: "full CSR without basic constraints", data: map[string]interface{}{ // using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#generate-intermediate-csr "common_name": "the common name", "alt_names": "user@example.com,admin@example.com,example.com,www.example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", // format // private_key_format "key_type": "rsa", "key_bits": 2048, "key_name": "the-key-name", // key_ref "signature_bits": 384, // exclude_cn_from_sans "ou": "unit1, unit2", "organization": "org1, org2", "country": "CA,US", "locality": "locality1, locality2", "province": "province1, province2", "street_address": "street_address1, street_address2", "postal_code": "postal_code1, postal_code2", "serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", "add_basic_constraints": false, }, ttl: 2 * time.Hour, wantParams: certutil.CreationParameters{ Subject: pkix.Name{ CommonName: "the common name", OrganizationalUnit: []string{"unit1", "unit2"}, Organization: []string{"org1", "org2"}, Country: []string{"CA", "US"}, Locality: []string{"locality1", "locality2"}, Province: []string{"province1", "province2"}, StreetAddress: []string{"street_address1", "street_address2"}, PostalCode: []string{"postal_code1", "postal_code2"}, SerialNumber: "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", }, DNSNames: []string{"example.com", "www.example.com"}, EmailAddresses: []string{"admin@example.com", "user@example.com"}, IPAddresses: []net.IP{[]byte{1, 2, 3, 4}, []byte{1, 2, 3, 5}}, URIs: []*url.URL{parseURL("https://example.com"), parseURL("https://www.example.com")}, OtherSANs: map[string][]string{"1.3.6.1.4.1.311.20.2.3": {"caadmin@example.com"}}, IsCA: false, KeyType: "rsa", KeyBits: 2048, NotAfter: time.Time{}, KeyUsage: 0, ExtKeyUsage: 0, ExtKeyUsageOIDs: nil, PolicyIdentifiers: nil, BasicConstraintsValidForNonCA: false, SignatureBits: 384, UsePSS: false, ForceAppendCaChain: false, UseCSRValues: false, PermittedDNSDomains: nil, URLs: nil, MaxPathLength: 0, NotBeforeDuration: 0, SKID: nil, }, wantFields: map[string]interface{}{ "common_name": "the common name", "ou": "unit1,unit2", "organization": "org1,org2", "country": "CA,US", "locality": "locality1,locality2", "province": "province1,province2", "street_address": "street_address1,street_address2", "postal_code": "postal_code1,postal_code2", "alt_names": "example.com,www.example.com,admin@example.com,user@example.com", "ip_sans": "1.2.3.4,1.2.3.5", "uri_sans": "https://example.com,https://www.example.com", "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF-8:caadmin@example.com", "exclude_cn_from_sans": true, "key_type": "rsa", "key_bits": 2048, "signature_bits": 384, "use_pss": false, "serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8", "add_basic_constraints": false, }, wantErr: false, }, } for _, tt := range tests { b, s := CreateBackendWithStorage(t) issueTime := time.Now() resp, err := CBWrite(b, s, "intermediate/generate/internal", tt.data) require.NoError(t, err) require.NotNil(t, resp) csrData := resp.Data["csr"].(string) csr, err := parsing.ParseCertificateRequestFromString(csrData) require.NoError(t, err) require.NotNil(t, csr) t.Run(tt.name+" parameters", func(t *testing.T) { testParseCsrToCreationParameters(t, issueTime, tt, csr) }) t.Run(tt.name+" fields", func(t *testing.T) { testParseCsrToFields(t, issueTime, tt, csr) }) } } func testParseCsrToCreationParameters(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, csr *x509.CertificateRequest) { params, err := certutil.ParseCsrToCreationParameters(*csr) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) if diff := deep.Equal(tt.wantParams, params); diff != nil { t.Errorf("testParseCertificateToCreationParameters() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) } } } func testParseCsrToFields(t *testing.T, issueTime time.Time, tt *parseCertificateTestCase, csr *x509.CertificateRequest) { fields, err := certutil.ParseCsrToFields(*csr) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) if diff := deep.Equal(tt.wantFields, fields); diff != nil { t.Errorf("testParseCertificateToFields() diff: %s", strings.ReplaceAll(strings.Join(diff, "\n"), "map", "\nmap")) } } }