Skip to content

Commit e11d73b

Browse files
Enforce PKI issuer constraints. (#29045) (#29046)
Add environment variable VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION. Setting VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION=true will disable the cert issuance/signing verification. Co-authored-by: Victor Rodriguez <vrizo@hashicorp.com>
1 parent 7fb4e6f commit e11d73b

File tree

7 files changed

+409
-177
lines changed

7 files changed

+409
-177
lines changed

builtin/logical/pki/cert_util_test.go

+148-40
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"net"
1212
"net/url"
13+
"os"
1314
"reflect"
1415
"strings"
1516
"testing"
@@ -275,6 +276,112 @@ type parseCertificateTestCase struct {
275276
wantErr bool
276277
}
277278

279+
// TestDisableVerifyCertificateEnvVar verifies that env var VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION
280+
// can be used to disable cert verification.
281+
func TestDisableVerifyCertificateEnvVar(t *testing.T) {
282+
caData := map[string]any{
283+
// Copied from the "full CA" test case of TestParseCertificate,
284+
// with tweaked permitted_dns_domains and ttl
285+
"common_name": "the common name",
286+
"alt_names": "user@example.com,admin@example.com,example.com,www.example.com",
287+
"ip_sans": "1.2.3.4,1.2.3.5",
288+
"uri_sans": "https://example.com,https://www.example.com",
289+
"other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com",
290+
"ttl": "3h",
291+
"max_path_length": 2,
292+
"permitted_dns_domains": ".example.com,.www.example.com",
293+
"ou": "unit1, unit2",
294+
"organization": "org1, org2",
295+
"country": "US, CA",
296+
"locality": "locality1, locality2",
297+
"province": "province1, province2",
298+
"street_address": "street_address1, street_address2",
299+
"postal_code": "postal_code1, postal_code2",
300+
"not_before_duration": "45s",
301+
"key_type": "rsa",
302+
"use_pss": true,
303+
"key_bits": 2048,
304+
"signature_bits": 384,
305+
}
306+
307+
roleData := map[string]any{
308+
"allow_any_name": true,
309+
"cn_validations": "disabled",
310+
"allow_ip_sans": true,
311+
"allowed_other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:*@example.com",
312+
"allowed_uri_sans": "https://example.com,https://www.example.com",
313+
"allowed_user_ids": "*",
314+
"not_before_duration": "45s",
315+
"signature_bits": 384,
316+
"key_usage": "KeyAgreement",
317+
"ext_key_usage": "ServerAuth",
318+
"ext_key_usage_oids": "1.3.6.1.5.5.7.3.67,1.3.6.1.5.5.7.3.68",
319+
"client_flag": false,
320+
"server_flag": false,
321+
"policy_identifiers": "1.2.3.4.5.6.7.8.9.0",
322+
}
323+
324+
certData := map[string]any{
325+
// using the same order as in https://developer.hashicorp.com/vault/api-docs/secret/pki#generate-certificate-and-key
326+
"common_name": "the common name non ca",
327+
"alt_names": "user@example.com,admin@example.com,example.com,www.example.com",
328+
"ip_sans": "1.2.3.4,1.2.3.5",
329+
"uri_sans": "https://example.com,https://www.example.com",
330+
"other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com",
331+
"ttl": "2h",
332+
// format
333+
// private_key_format
334+
"exclude_cn_from_sans": true,
335+
// not_after
336+
// remove_roots_from_chain
337+
"user_ids": "humanoid,robot",
338+
}
339+
340+
defer func() {
341+
os.Unsetenv("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION")
342+
}()
343+
344+
b, s := CreateBackendWithStorage(t)
345+
346+
// Create the CA
347+
resp, err := CBWrite(b, s, "root/generate/internal", caData)
348+
require.NoError(t, err)
349+
require.NotNil(t, resp)
350+
351+
// Create the role
352+
resp, err = CBWrite(b, s, "roles/test", roleData)
353+
require.NoError(t, err)
354+
require.NotNil(t, resp)
355+
356+
// Try to create the cert -- should fail verification, since example.com is not allowed
357+
t.Run("no VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION env var", func(t *testing.T) {
358+
resp, err = CBWrite(b, s, "issue/test", certData)
359+
require.ErrorContains(t, err, `DNS name "example.com" is not permitted by any constraint`)
360+
})
361+
362+
// Try to create the cert -- should fail verification, since example.com is not allowed
363+
t.Run("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION=false", func(t *testing.T) {
364+
os.Setenv("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION", "false")
365+
resp, err = CBWrite(b, s, "issue/test", certData)
366+
require.ErrorContains(t, err, `DNS name "example.com" is not permitted by any constraint`)
367+
})
368+
369+
// Create the cert, should succeed with the disable env var set
370+
t.Run("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION=true", func(t *testing.T) {
371+
os.Setenv("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION", "true")
372+
resp, err = CBWrite(b, s, "issue/test", certData)
373+
require.NoError(t, err)
374+
require.NotNil(t, resp)
375+
})
376+
377+
// Invalid env var
378+
t.Run("invalid VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION", func(t *testing.T) {
379+
os.Setenv("VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION", "invalid")
380+
resp, err = CBWrite(b, s, "issue/test", certData)
381+
require.ErrorContains(t, err, "failed parsing environment variable VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION")
382+
})
383+
}
384+
278385
func TestParseCertificate(t *testing.T) {
279386
t.Parallel()
280387

@@ -364,7 +471,7 @@ func TestParseCertificate(t *testing.T) {
364471
"other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com",
365472
"ttl": "2h",
366473
"max_path_length": 2,
367-
"permitted_dns_domains": ".example.com,.www.example.com",
474+
"permitted_dns_domains": "example.com,.example.com,.www.example.com",
368475
"ou": "unit1, unit2",
369476
"organization": "org1, org2",
370477
"country": "US, CA",
@@ -409,7 +516,7 @@ func TestParseCertificate(t *testing.T) {
409516
UsePSS: true,
410517
ForceAppendCaChain: false,
411518
UseCSRValues: false,
412-
PermittedDNSDomains: []string{".example.com", ".www.example.com"},
519+
PermittedDNSDomains: []string{"example.com", ".example.com", ".www.example.com"},
413520
URLs: nil,
414521
MaxPathLength: 2,
415522
NotBeforeDuration: 45 * time.Second,
@@ -433,7 +540,7 @@ func TestParseCertificate(t *testing.T) {
433540
"serial_number": "",
434541
"ttl": "2h0m45s",
435542
"max_path_length": 2,
436-
"permitted_dns_domains": ".example.com,.www.example.com",
543+
"permitted_dns_domains": "example.com,.example.com,.www.example.com",
437544
"use_pss": true,
438545
"key_type": "rsa",
439546
"key_bits": 2048,
@@ -532,49 +639,50 @@ func TestParseCertificate(t *testing.T) {
532639
},
533640
}
534641
for _, tt := range tests {
642+
t.Run(tt.name, func(t *testing.T) {
643+
b, s := CreateBackendWithStorage(t)
535644

536-
b, s := CreateBackendWithStorage(t)
645+
var cert *x509.Certificate
646+
issueTime := time.Now()
647+
if tt.wantParams.IsCA {
648+
resp, err := CBWrite(b, s, "root/generate/internal", tt.data)
649+
require.NoError(t, err)
650+
require.NotNil(t, resp)
537651

538-
var cert *x509.Certificate
539-
issueTime := time.Now()
540-
if tt.wantParams.IsCA {
541-
resp, err := CBWrite(b, s, "root/generate/internal", tt.data)
542-
require.NoError(t, err)
543-
require.NotNil(t, resp)
652+
certData := resp.Data["certificate"].(string)
653+
cert, err = parsing.ParseCertificateFromString(certData)
654+
require.NoError(t, err)
655+
require.NotNil(t, cert)
656+
} else {
657+
// use the "simple CA" data to create the internal CA
658+
caData := tests[1].data
659+
caData["ttl"] = "3h"
660+
resp, err := CBWrite(b, s, "root/generate/internal", caData)
661+
require.NoError(t, err)
662+
require.NotNil(t, resp)
544663

545-
certData := resp.Data["certificate"].(string)
546-
cert, err = parsing.ParseCertificateFromString(certData)
547-
require.NoError(t, err)
548-
require.NotNil(t, cert)
549-
} else {
550-
// use the "simple CA" data to create the internal CA
551-
caData := tests[1].data
552-
caData["ttl"] = "3h"
553-
resp, err := CBWrite(b, s, "root/generate/internal", caData)
554-
require.NoError(t, err)
555-
require.NotNil(t, resp)
664+
// create a role
665+
resp, err = CBWrite(b, s, "roles/test", tt.roleData)
666+
require.NoError(t, err)
667+
require.NotNil(t, resp)
556668

557-
// create a role
558-
resp, err = CBWrite(b, s, "roles/test", tt.roleData)
559-
require.NoError(t, err)
560-
require.NotNil(t, resp)
669+
// create the cert
670+
resp, err = CBWrite(b, s, "issue/test", tt.data)
671+
require.NoError(t, err)
672+
require.NotNil(t, resp)
561673

562-
// create the cert
563-
resp, err = CBWrite(b, s, "issue/test", tt.data)
564-
require.NoError(t, err)
565-
require.NotNil(t, resp)
566-
567-
certData := resp.Data["certificate"].(string)
568-
cert, err = parsing.ParseCertificateFromString(certData)
569-
require.NoError(t, err)
570-
require.NotNil(t, cert)
571-
}
674+
certData := resp.Data["certificate"].(string)
675+
cert, err = parsing.ParseCertificateFromString(certData)
676+
require.NoError(t, err)
677+
require.NotNil(t, cert)
678+
}
572679

573-
t.Run(tt.name+" parameters", func(t *testing.T) {
574-
testParseCertificateToCreationParameters(t, issueTime, tt, cert)
575-
})
576-
t.Run(tt.name+" fields", func(t *testing.T) {
577-
testParseCertificateToFields(t, issueTime, tt, cert)
680+
t.Run(tt.name+" parameters", func(t *testing.T) {
681+
testParseCertificateToCreationParameters(t, issueTime, tt, cert)
682+
})
683+
t.Run(tt.name+" fields", func(t *testing.T) {
684+
testParseCertificateToFields(t, issueTime, tt, cert)
685+
})
578686
})
579687
}
580688
}
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package issuing
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"strconv"
10+
"time"
11+
12+
ctx509 "github.com/google/certificate-transparency-go/x509"
13+
"github.com/hashicorp/vault/sdk/helper/certutil"
14+
)
15+
16+
// disableVerifyCertificateEnvVar is an environment variable that can be used to disable the
17+
// verification done when issuing or signing certificates that was added by VAULT-22013. It
18+
// is meant as a scape hatch to avoid breaking deployments that the new verification would
19+
// break.
20+
const disableVerifyCertificateEnvVar = "VAULT_DISABLE_PKI_CONSTRAINTS_VERIFICATION"
21+
22+
func isCertificateVerificationDisabled() (bool, error) {
23+
disableRaw, ok := os.LookupEnv(disableVerifyCertificateEnvVar)
24+
if !ok {
25+
return false, nil
26+
}
27+
28+
disable, err := strconv.ParseBool(disableRaw)
29+
if err != nil {
30+
return false, fmt.Errorf("failed parsing environment variable %s: %w", disableVerifyCertificateEnvVar, err)
31+
}
32+
33+
return disable, nil
34+
}
35+
36+
func VerifyCertificate(parsedBundle *certutil.ParsedCertBundle) error {
37+
if verificationDisabled, err := isCertificateVerificationDisabled(); err != nil {
38+
return err
39+
} else if verificationDisabled {
40+
return nil
41+
}
42+
43+
certChainPool := ctx509.NewCertPool()
44+
for _, certificate := range parsedBundle.CAChain {
45+
cert, err := convertCertificate(certificate.Bytes)
46+
if err != nil {
47+
return err
48+
}
49+
certChainPool.AddCert(cert)
50+
}
51+
52+
// Validation Code, assuming we need to validate the entire chain of constraints
53+
54+
// Note that we use github.com/google/certificate-transparency-go/x509 to perform certificate verification,
55+
// since that library provides options to disable checks that the standard library does not.
56+
57+
options := ctx509.VerifyOptions{
58+
Intermediates: nil, // We aren't verifying the chain here, this would do more work
59+
Roots: certChainPool,
60+
CurrentTime: time.Time{},
61+
KeyUsages: nil,
62+
MaxConstraintComparisions: 0, // This means infinite
63+
DisableTimeChecks: true,
64+
DisableEKUChecks: true,
65+
DisableCriticalExtensionChecks: false,
66+
DisableNameChecks: false,
67+
DisablePathLenChecks: false,
68+
DisableNameConstraintChecks: false,
69+
}
70+
71+
certificate, err := convertCertificate(parsedBundle.CertificateBytes)
72+
if err != nil {
73+
return err
74+
}
75+
76+
_, err = certificate.Verify(options)
77+
return err
78+
}
79+
80+
func convertCertificate(certBytes []byte) (*ctx509.Certificate, error) {
81+
ret, err := ctx509.ParseCertificate(certBytes)
82+
if err != nil {
83+
return nil, fmt.Errorf("cannot convert certificate for validation: %w", err)
84+
}
85+
return ret, nil
86+
}

builtin/logical/pki/path_issue_sign.go

+4
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,10 @@ func (b *backend) pathIssueSignCert(ctx context.Context, req *logical.Request, d
432432
}
433433
}
434434

435+
if err := issuing.VerifyCertificate(parsedBundle); err != nil {
436+
return nil, err
437+
}
438+
435439
generateLease := false
436440
if role.GenerateLease != nil && *role.GenerateLease {
437441
generateLease = true

changelog/29045.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:change
2+
secrets/pki: Enforce the issuer constraint extensions (extended key usage, name constraints, issuer name) when issuing or signing leaf certificates.
3+
```

0 commit comments

Comments
 (0)