mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-10 00:27:02 +02:00
Add environment variable VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION. Setting VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION=true will disable the cert issuance/signing verification.
1060 lines
38 KiB
Go
1060 lines
38 KiB
Go
// 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"))
|
|
}
|
|
}
|
|
}
|