Skip to content

Commit 1aca094

Browse files
backport of commit 95a16db
1 parent bda6150 commit 1aca094

File tree

13 files changed

+204
-10
lines changed

13 files changed

+204
-10
lines changed

builtin/logical/pki/backend_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -7224,6 +7224,94 @@ func TestGenerateRootCAWithAIA(t *testing.T) {
72247224
requireSuccessNonNilResponse(t, resp, err, "expected root generation to succeed")
72257225
}
72267226

7227+
// TestIssuance_AlwaysEnforceErr validates that we properly return an error in all request
7228+
// types that go beyond the issuer's NotAfter
7229+
func TestIssuance_AlwaysEnforceErr(t *testing.T) {
7230+
t.Parallel()
7231+
b, s := CreateBackendWithStorage(t)
7232+
7233+
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
7234+
"common_name": "root myvault.com",
7235+
"key_type": "ec",
7236+
"ttl": "10h",
7237+
"issuer_name": "root-ca",
7238+
"key_name": "root-key",
7239+
})
7240+
requireSuccessNonNilResponse(t, resp, err, "expected root generation to succeed")
7241+
7242+
resp, err = CBPatch(b, s, "issuer/root-ca", map[string]interface{}{
7243+
"leaf_not_after_behavior": "always_enforce_err",
7244+
})
7245+
requireSuccessNonNilResponse(t, resp, err, "failed updating root issuer with always_enforce_err")
7246+
7247+
resp, err = CBWrite(b, s, "roles/test-role", map[string]interface{}{
7248+
"allow_any_name": true,
7249+
"key_type": "ec",
7250+
"allowed_serial_numbers": "*",
7251+
})
7252+
7253+
expectedErrContains := "cannot satisfy request, as TTL would result in notAfter"
7254+
7255+
// Make sure we fail on CA issuance requests now
7256+
t.Run("ca-issuance", func(t *testing.T) {
7257+
resp, err = CBWrite(b, s, "intermediate/generate/internal", map[string]interface{}{
7258+
"common_name": "myint.com",
7259+
})
7260+
requireSuccessNonNilResponse(t, resp, err, "failed generating intermediary CSR")
7261+
requireFieldsSetInResp(t, resp, "csr")
7262+
csr := resp.Data["csr"]
7263+
7264+
_, err = CBWrite(b, s, "issuer/root-ca/sign-intermediate", map[string]interface{}{
7265+
"csr": csr,
7266+
"use_csr_values": true,
7267+
"ttl": "60h",
7268+
})
7269+
require.ErrorContains(t, err, expectedErrContains, "sign-intermediate should have failed as root issuer leaf behavior is set to always_enforce_err")
7270+
7271+
// Make sure it works if we are under
7272+
resp, err = CBWrite(b, s, "issuer/root-ca/sign-intermediate", map[string]interface{}{
7273+
"csr": csr,
7274+
"use_csr_values": true,
7275+
"ttl": "30m",
7276+
})
7277+
requireSuccessNonNilResponse(t, resp, err, "sign-intermediate should have passed with a lower TTL value and always_enforce_err")
7278+
})
7279+
7280+
// Make sure we fail on leaf csr signing leaf as we always did for 'err'
7281+
t.Run("sign-leaf-csr", func(t *testing.T) {
7282+
_, csrPem := generateTestCsr(t, certutil.ECPrivateKey, 256)
7283+
7284+
resp, err = CBWrite(b, s, "issuer/root-ca/sign/test-role", map[string]interface{}{
7285+
"ttl": "60h",
7286+
"csr": csrPem,
7287+
})
7288+
require.ErrorContains(t, err, expectedErrContains, "expected error from sign csr got: %v", resp)
7289+
7290+
// Make sure it works if we are under
7291+
resp, err = CBWrite(b, s, "issuer/root-ca/sign/test-role", map[string]interface{}{
7292+
"ttl": "30m",
7293+
"csr": csrPem,
7294+
})
7295+
requireSuccessNonNilResponse(t, resp, err, "sign should have succeeded with a lower TTL and always_enforce_err")
7296+
})
7297+
7298+
// Make sure we fail on leaf csr signing leaf as we always did for 'err'
7299+
t.Run("issue-leaf-csr", func(t *testing.T) {
7300+
resp, err = CBWrite(b, s, "issuer/root-ca/issue/test-role", map[string]interface{}{
7301+
"ttl": "60h",
7302+
"common_name": "leaf.example.com",
7303+
})
7304+
require.ErrorContains(t, err, expectedErrContains, "expected error from issue got: %v", resp)
7305+
7306+
// Make sure it works if we are under
7307+
resp, err = CBWrite(b, s, "issuer/root-ca/issue/test-role", map[string]interface{}{
7308+
"ttl": "30m",
7309+
"common_name": "leaf.example.com",
7310+
})
7311+
requireSuccessNonNilResponse(t, resp, err, "issue should have worked with a lower TTL and always_enforce_err")
7312+
})
7313+
}
7314+
72277315
var (
72287316
initTest sync.Once
72297317
rsaCAKey string

builtin/logical/pki/issuing/issue_common.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1008,7 +1008,7 @@ func ApplyIssuerLeafNotAfterBehavior(caSign *certutil.CAInfoBundle, notAfter tim
10081008
// Explicitly do nothing.
10091009
case certutil.TruncateNotAfterBehavior:
10101010
notAfter = caSign.Certificate.NotAfter
1011-
case certutil.ErrNotAfterBehavior:
1011+
case certutil.ErrNotAfterBehavior, certutil.AlwaysEnforceErr:
10121012
fallthrough
10131013
default:
10141014
return time.Time{}, errutil.UserError{Err: fmt.Sprintf(

builtin/logical/pki/path_acme_order.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,8 @@ func issueCertFromCsr(ac *acmeContext, csr *x509.CertificateRequest) (*certutil.
529529
}
530530

531531
// ACME issued cert will override the TTL values to truncate to the issuer's
532-
// expiration if we go beyond, no matter the setting
532+
// expiration if we go beyond, no matter the setting.
533+
// Note that if set to certutil.AlwaysEnforceErr we will error out
533534
if signingBundle.LeafNotAfterBehavior == certutil.ErrNotAfterBehavior {
534535
signingBundle.LeafNotAfterBehavior = certutil.TruncateNotAfterBehavior
535536
}

builtin/logical/pki/path_acme_test.go

+79
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,85 @@ func TestAcmeConfigChecksPublicAcmeEnv(t *testing.T) {
710710
require.NoError(t, err)
711711
}
712712

713+
// TestAcmeHonorsAlwaysEnforceErr verifies that we get an error and not truncated if the issuer's
714+
// leaf_not_after_behavior is set to always_enforce_err
715+
func TestAcmeHonorsAlwaysEnforceErr(t *testing.T) {
716+
t.Parallel()
717+
718+
cluster, client, _ := setupAcmeBackend(t)
719+
defer cluster.Cleanup()
720+
721+
testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
722+
defer cancel()
723+
724+
mount := "pki"
725+
resp, err := client.Logical().WriteWithContext(context.Background(), mount+"/issuers/generate/intermediate/internal",
726+
map[string]interface{}{
727+
"key_name": "short-key",
728+
"key_type": "ec",
729+
"common_name": "test.com",
730+
})
731+
require.NoError(t, err, "failed creating intermediary CSR")
732+
intermediateCSR := resp.Data["csr"].(string)
733+
734+
// Sign the intermediate CSR using /pki
735+
resp, err = client.Logical().Write(mount+"/issuer/root-ca/sign-intermediate", map[string]interface{}{
736+
"csr": intermediateCSR,
737+
"ttl": "10m",
738+
"max_ttl": "1h",
739+
})
740+
require.NoError(t, err, "failed signing intermediary CSR")
741+
intermediateCertPEM := resp.Data["certificate"].(string)
742+
743+
// Configure the intermediate cert as the CA in /pki2
744+
resp, err = client.Logical().Write(mount+"/issuers/import/cert", map[string]interface{}{
745+
"pem_bundle": intermediateCertPEM,
746+
})
747+
require.NoError(t, err, "failed importing intermediary cert")
748+
importedIssuersRaw := resp.Data["imported_issuers"].([]interface{})
749+
require.Len(t, importedIssuersRaw, 1)
750+
shortCaUuid := importedIssuersRaw[0].(string)
751+
752+
_, err = client.Logical().Write(mount+"/issuer/"+shortCaUuid, map[string]interface{}{
753+
"leaf_not_after_behavior": "always_enforce_err",
754+
"issuer_name": "short-ca",
755+
})
756+
require.NoError(t, err, "failed updating issuer name")
757+
758+
baseAcmeURL := "/v1/pki/issuer/short-ca/acme/"
759+
accountKey, err := rsa.GenerateKey(rand.Reader, 2048)
760+
require.NoError(t, err, "failed creating rsa key")
761+
762+
acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey)
763+
764+
// Create new account
765+
t.Logf("Testing register on %s", baseAcmeURL)
766+
acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true })
767+
require.NoError(t, err, "failed registering account")
768+
769+
// Create an order
770+
t.Logf("Testing Authorize Order on %s", baseAcmeURL)
771+
identifiers := []string{"*.localdomain"}
772+
order, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{
773+
{Type: "dns", Value: identifiers[0]},
774+
})
775+
require.NoError(t, err, "failed creating order")
776+
777+
// HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow
778+
// test.
779+
markAuthorizationSuccess(t, client, acmeClient, acct, order)
780+
781+
// Build a proper CSR, with the correct name and signed with a different key works.
782+
goodCr := &x509.CertificateRequest{DNSNames: []string{identifiers[0]}}
783+
csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
784+
require.NoError(t, err, "failed generated key for CSR")
785+
csr, err := x509.CreateCertificateRequest(rand.Reader, goodCr, csrKey)
786+
require.NoError(t, err, "failed generating csr")
787+
788+
_, _, err = acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, true)
789+
require.ErrorContains(t, err, "cannot satisfy request, as TTL would result in notAfter", "failed finalizing order")
790+
}
791+
713792
// TestAcmeTruncatesToIssuerExpiry make sure that if the selected issuer's expiry is shorter than the
714793
// CSR's selected TTL value in ACME and the issuer's leaf_not_after_behavior setting is set to Err,
715794
// we will override the configured behavior and truncate to the issuer's NotAfter

builtin/logical/pki/path_fetch_issuers.go

+4
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,8 @@ func (b *backend) pathUpdateIssuer(ctx context.Context, req *logical.Request, da
529529
switch rawLeafBehavior {
530530
case "err":
531531
newLeafBehavior = certutil.ErrNotAfterBehavior
532+
case "always_enforce_err":
533+
newLeafBehavior = certutil.AlwaysEnforceErr
532534
case "truncate":
533535
newLeafBehavior = certutil.TruncateNotAfterBehavior
534536
case "permit":
@@ -797,6 +799,8 @@ func (b *backend) pathPatchIssuer(ctx context.Context, req *logical.Request, dat
797799
switch rawLeafBehavior {
798800
case "err":
799801
newLeafBehavior = certutil.ErrNotAfterBehavior
802+
case "always_enforce_err":
803+
newLeafBehavior = certutil.AlwaysEnforceErr
800804
case "truncate":
801805
newLeafBehavior = certutil.TruncateNotAfterBehavior
802806
case "permit":

builtin/logical/pki/path_root.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,10 @@ func (b *backend) pathIssuerSignIntermediate(ctx context.Context, req *logical.R
383383
// Since we are signing an intermediate, we will by default truncate the
384384
// signed intermediary in order to generate a valid intermediary chain. This
385385
// was changed in 1.17.x as the default prior was PermitNotAfterBehavior
386-
warnAboutTruncate = true
387-
signingBundle.LeafNotAfterBehavior = certutil.TruncateNotAfterBehavior
386+
if signingBundle.LeafNotAfterBehavior != certutil.AlwaysEnforceErr {
387+
warnAboutTruncate = true
388+
signingBundle.LeafNotAfterBehavior = certutil.TruncateNotAfterBehavior
389+
}
388390
}
389391

390392
useCSRValues := data.Get("use_csr_values").(bool)

changelog/28907.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
secret/pki: Introduce a new value `always_enforce_err` within `leaf_not_after_behavior` to force the error in all circumstances such as CA issuance and ACME requests if requested TTL values are beyond the issuer's NotAfter.
3+
```

sdk/helper/certutil/certutil_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,10 @@ func TestNotAfterValues(t *testing.T) {
916916
if PermitNotAfterBehavior != 2 {
917917
t.Fatalf("Expected PermitNotAfterBehavior=%v to have value 2", PermitNotAfterBehavior)
918918
}
919+
920+
if AlwaysEnforceErr != 3 {
921+
t.Fatalf("Expected AlwaysEnforceErr=%v to have value 3", AlwaysEnforceErr)
922+
}
919923
}
920924

921925
func TestSignatureAlgorithmRoundTripping(t *testing.T) {

sdk/helper/certutil/types.go

+2
Original file line numberDiff line numberDiff line change
@@ -708,9 +708,11 @@ const (
708708
ErrNotAfterBehavior NotAfterBehavior = iota
709709
TruncateNotAfterBehavior
710710
PermitNotAfterBehavior
711+
AlwaysEnforceErr
711712
)
712713

713714
var notAfterBehaviorNames = map[NotAfterBehavior]string{
715+
AlwaysEnforceErr: "always_enforce_err",
714716
ErrNotAfterBehavior: "err",
715717
TruncateNotAfterBehavior: "truncate",
716718
PermitNotAfterBehavior: "permit",

ui/app/models/pki/issuer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export default class PkiIssuerModel extends Model {
6363
'What happens when a leaf certificate is issued, but its NotAfter field (and therefore its expiry date) exceeds that of this issuer.',
6464
docLink: '/vault/api-docs/secret/pki#update-issuer',
6565
editType: 'yield',
66-
valueOptions: ['err', 'truncate', 'permit'],
66+
valueOptions: ['always_enforce_err', 'err', 'truncate', 'permit'],
6767
})
6868
leafNotAfterBehavior;
6969

ui/lib/pki/addon/components/page/pki-issuer-edit.hbs

+7-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@
3939
>
4040
{{#each field.options.valueOptions as |value|}}
4141
<option selected={{eq @model.leafNotAfterBehavior value}} value={{value}}>
42-
{{capitalize (if (eq value "err") "error" value)}}
43-
if the computed NotAfter exceeds that of this issuer
42+
{{#if (eq value "always_enforce_err")}}
43+
Error if the computed NotAfter exceeds that of this issuer in all circumstances (leaf, CA issuance and
44+
ACME)
45+
{{else}}
46+
{{capitalize (if (eq value "err") "error" value)}}
47+
if the computed NotAfter exceeds that of this issuer
48+
{{/if}}
4449
</option>
4550
{{/each}}
4651
</select>

ui/tests/integration/components/pki/page/pki-issuer-edit-test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ module('Integration | Component | pki | Page::PkiIssuerEditPage::PkiIssuerEdit',
7979
assert
8080
.dom(selectors.leafOption)
8181
.hasText(
82-
'Error if the computed NotAfter exceeds that of this issuer',
82+
'Error if the computed NotAfter exceeds that of this issuer in all circumstances (leaf, CA issuance and ACME)',
8383
'Correct text renders for leaf option'
8484
);
8585
assert.dom(selectors.usageCert).isChecked('Usage issuing certificates is checked');

website/content/api-docs/secret/pki/index.mdx

+8-2
Original file line numberDiff line numberDiff line change
@@ -1086,7 +1086,9 @@ have access.**
10861086
extension is missing from the CSR.
10871087

10881088
- `enforce_leaf_not_after_behavior` `(bool: false)` - If true, do not apply the default truncate
1089-
behavior to the issued CA certificate, instead defer to the issuer's configured `leaf_not_after_behavior`
1089+
behavior to the issued CA certificate, instead defer to the issuer's configured `leaf_not_after_behavior`.
1090+
If an issuer's `leaf_not_after_behavior` is set to `always_enforce_err`, this flag is not required if
1091+
the desired behavior is to error out on requests who's TTL extends beyond the issuer's NotAfter.
10901092

10911093
- `ttl` `(string: "")` - Specifies the requested Time To Live. Cannot be greater
10921094
than the engine's `max_ttl` value. If not provided, the engine's `ttl` value
@@ -2608,8 +2610,12 @@ do so, import a new issuer and a new `issuer_id` will be assigned.
26082610

26092611
- `leaf_not_after_behavior` `(string: "err")` - Behavior of a leaf's
26102612
`NotAfter` field during issuance. Valid options are:
2611-
2613+
- `always_enforce_err` overrides all hardcoded behaviors to enforce an
2614+
error if any requested TTL is beyond the issuer. This applies to CA issuance,
2615+
and ACME issuance, along with the normal err on leaf certificates through Vault's API. (Available from 1.18.2+)
26122616
- `err`, to error if the computed `NotAfter` exceeds that of this issuer;
2617+
- **Note** for CA issuance and ACME issuance this behavior is overridden
2618+
with truncate behavior, use `always_enforce_err` to disable these overrides
26132619
- `truncate` to silently truncate the requested `NotAfter` value to that
26142620
of this issuer; or
26152621
- `permit` to allow this issuance to succeed with a `NotAfter` value

0 commit comments

Comments
 (0)