vault/builtin/logical/pki/cert_util_test.go
Victor Rodriguez 48cec9729d
Enforce PKI issuer constraints. (#29045)
Add environment variable VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION.

Setting VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION=true will disable the cert
issuance/signing verification.
2024-11-27 18:34:26 +01:00

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