Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PKI: Change sign-intermediate to truncate notAfter by default (behavior change) #26796

Merged
merged 4 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 60 additions & 2 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2539,21 +2539,76 @@ func TestBackend_Root_Idempotency(t *testing.T) {
}
}

// TestBackend_SignIntermediate_EnforceLeafFlag verifies if the flag is true
// that we will leverage the issuer's configured behavior
func TestBackend_SignIntermediate_EnforceLeafFlag(t *testing.T) {
t.Parallel()
b, s := CreateBackendWithStorage(t)

resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "myvault.com",
})
require.NoError(t, err, "failed generating root cert")
rootCert := parseCert(t, resp.Data["certificate"].(string))

_, err = CBWrite(b, s, "issuer/default", map[string]interface{}{
"leaf_not_after_behavior": "err",
})
require.NoError(t, err, "failed updating root issuer cert behavior")

resp, err = CBWrite(b, s, "intermediate/generate/internal", map[string]interface{}{
"common_name": "myint.com",
})
require.NoError(t, err, "failed generating intermediary CSR")
csr := resp.Data["csr"]

_, err = CBWrite(b, s, "root/sign-intermediate", map[string]interface{}{
"common_name": "myint.com",
"other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:[email protected]",
"csr": csr,
"ttl": "60h",
"enforce_leaf_not_after_behavior": true,
})
require.Error(t, err, "sign-intermediate should have failed as root issuer leaf behavior is set to err")

// Now test with permit, the old default behavior
_, err = CBWrite(b, s, "issuer/default", map[string]interface{}{
"leaf_not_after_behavior": "permit",
})
require.NoError(t, err, "failed updating root issuer cert behavior to permit")

resp, err = CBWrite(b, s, "root/sign-intermediate", map[string]interface{}{
"common_name": "myint.com",
"other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:[email protected]",
"csr": csr,
"ttl": "60h",
"enforce_leaf_not_after_behavior": true,
})
require.NoError(t, err, "failed to sign intermediary CA with permit as issuer")
intCert := parseCert(t, resp.Data["certificate"].(string))

require.Truef(t, rootCert.NotAfter.Before(intCert.NotAfter),
"root cert notAfter %v was not before ca cert's notAfter %v", rootCert.NotAfter, intCert.NotAfter)
}

func TestBackend_SignIntermediate_AllowedPastCAValidity(t *testing.T) {
t.Parallel()
b_root, s_root := CreateBackendWithStorage(t)
b_int, s_int := CreateBackendWithStorage(t)
var err error

// Direct issuing from root
_, err = CBWrite(b_root, s_root, "root/generate/internal", map[string]interface{}{
resp, err := CBWrite(b_root, s_root, "root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "myvault.com",
})
if err != nil {
t.Fatal(err)
}

rootCert := parseCert(t, resp.Data["certificate"].(string))

_, err = CBWrite(b_root, s_root, "roles/test", map[string]interface{}{
"allow_bare_domains": true,
"allow_subdomains": true,
Expand All @@ -2563,7 +2618,7 @@ func TestBackend_SignIntermediate_AllowedPastCAValidity(t *testing.T) {
t.Fatal(err)
}

resp, err := CBWrite(b_int, s_int, "intermediate/generate/internal", map[string]interface{}{
resp, err = CBWrite(b_int, s_int, "intermediate/generate/internal", map[string]interface{}{
"common_name": "myint.com",
})
schema.ValidateResponse(t, schema.GetResponseSchema(t, b_root.Route("intermediate/generate/internal"), logical.UpdateOperation), resp, true)
Expand Down Expand Up @@ -2614,6 +2669,9 @@ func TestBackend_SignIntermediate_AllowedPastCAValidity(t *testing.T) {
cert := parseCert(t, resp.Data["certificate"].(string))
certSkid := certutil.GetHexFormatted(cert.SubjectKeyId, ":")
require.Equal(t, intSkid, certSkid)

require.Equal(t, rootCert.NotAfter, cert.NotAfter, "intermediary cert's NotAfter did not match root cert's NotAfter")
require.Contains(t, resp.Warnings, intCaTruncatationWarning, "missing warning about intermediary CA notAfter truncation")
}

func TestBackend_ConsulSignLeafWithLegacyRole(t *testing.T) {
Expand Down
19 changes: 15 additions & 4 deletions builtin/logical/pki/path_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
"golang.org/x/crypto/ed25519"
)

const intCaTruncatationWarning = "the signed intermediary CA certificate's notAfter was truncated to the issuer's notAfter"

func pathGenerateRoot(b *backend) *framework.Path {
pattern := "root/generate/" + framework.GenericNameRegex("exported")

Expand Down Expand Up @@ -375,10 +377,14 @@ func (b *backend) pathIssuerSignIntermediate(ctx context.Context, req *logical.R
}
}

// Since we are signing an intermediate, we explicitly want to override
// the leaf NotAfterBehavior to permit issuing intermediates longer than
// the life of this issuer.
signingBundle.LeafNotAfterBehavior = certutil.PermitNotAfterBehavior
warnAboutTruncate := false
if enforceLeafNotAfter := data.Get("enforce_leaf_not_after_behavior").(bool); !enforceLeafNotAfter {
// Since we are signing an intermediate, we will by default truncate the
// signed intermediary in order to generate a valid intermediary chain. This
// was changed in 1.17.x as the default prior was PermitNotAfterBehavior
warnAboutTruncate = true
signingBundle.LeafNotAfterBehavior = certutil.TruncateNotAfterBehavior
}

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

Expand Down Expand Up @@ -418,6 +424,11 @@ func (b *backend) pathIssuerSignIntermediate(ctx context.Context, req *logical.R
return nil, err
}

if warnAboutTruncate &&
signingBundle.Certificate.NotAfter.Equal(parsedBundle.Certificate.NotAfter) {
resp.AddWarning(intCaTruncatationWarning)
}

return resp, nil
}

Expand Down
5 changes: 5 additions & 0 deletions builtin/logical/pki/path_sign_issuers.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ func pathSignIntermediate(b *backend) *framework.Path {

func buildPathIssuerSignIntermediateRaw(b *backend, pattern string, displayAttrs *framework.DisplayAttributes) *framework.Path {
fields := addIssuerRefField(map[string]*framework.FieldSchema{})
fields["enforce_leaf_not_after_behavior"] = &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: "Do not truncate the NotAfter field, use the issuer's configured leaf_not_after_behavior",
}
path := &framework.Path{
Pattern: pattern,
DisplayAttrs: displayAttrs,
Expand Down
3 changes: 3 additions & 0 deletions changelog/26796.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:change
secrets/pki: sign-intermediate API will truncate notAfter if calculated to go beyond the signing issuer's notAfter. Previously the notAfter was permitted to go beyond leading to invalid chains.
```
7 changes: 7 additions & 0 deletions website/content/api-docs/secret/pki.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,10 @@ when signing an externally-owned intermediate.
Access to this endpoint should be restricted by policy to only trusted
operators.

By default, Vault truncates the `notAfter` field of the signed intermediary
to use the `notAfter` field of the signing issuer, it the computed field for the
intermediary goes beyond the prescribed length.

| Method | Path | Issuer |
| :----- | :------------------------------------------ | :------- |
| `POST` | `/pki/root/sign-intermediate` | `default` |
Expand Down Expand Up @@ -1411,6 +1415,9 @@ have access.**
~> Note: This value is only used as a default when the `ExtendedKeyUsage`
extension is missing from the CSR.

- `enforce_leaf_not_after_behavior` `(bool: false)` - If true, do not apply the default truncate
behavior to the issued CA certificate, instead defer to the issuer's configured `leaf_not_after_behavior`

- `ttl` `(string: "")` - Specifies the requested Time To Live. Cannot be greater
than the engine's `max_ttl` value. If not provided, the engine's `ttl` value
will be used, which defaults to system values if not explicitly set. See
Expand Down
39 changes: 39 additions & 0 deletions website/content/docs/upgrading/upgrade-to-1.17.x.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
layout: docs
page_title: Upgrade to Vault 1.17.x - Guides
description: |-
Deprecations, important or breaking changes, and remediation recommendations
for anyone upgrading to 1.17.x from Vault 1.16.x.
---

# Overview

The Vault 1.17.x upgrade guide contains information on deprecations, important
or breaking changes, and remediation recommendations for anyone upgrading from
Vault 1.16. **Please read carefully**.

## Important changes

### PKI sign-intermediate now truncates notAfter field to signing issuer

Prior to 1.17.x, Vault allowed the calculated sign-intermediate `notAfter` field
to go beyond the signing issuer `notAfter` field. The extended value lead to a
CA chain that would not validate properly. As of 1.17.x, Vault truncates the
intermediary `notAfter` value to the signing issuer `notAfter` if the calculated
field is greater.

#### How to opt out

You can use the new `enforce_leaf_not_after_behavior` flag on the
sign-intermediate API along with the `leaf_not_after_behavior` flag for the
signing issuer to opt out of the truncating behavior.

When you set `enforce_leaf_not_after_behavior` to true, the sign-intermediate
API uses the `leaf_not_after_behavior` value configured for the signing issuer
to control truncation the behavior. Setting the issuer `leaf_not_after_behavior`
field to `permit` and `enforce_leaf_not_after_behavior` to true restores the
legacy behavior.

## Known issues and workarounds

@include 'known-issues/ocsp-redirect.mdx'
4 changes: 4 additions & 0 deletions website/data/docs-nav-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -2282,6 +2282,10 @@
"title": "Upgrade to Raft WAL",
"path": "upgrading/raft-wal"
},
{
"title": "Upgrade to 1.17.x",
"path": "upgrading/upgrade-to-1.17.x"
},
{
"title": "Upgrade to 1.16.x",
"path": "upgrading/upgrade-to-1.16.x"
Expand Down
Loading