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 3 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.
```
8 changes: 8 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,11 @@ when signing an externally-owned intermediate.
Access to this endpoint should be restricted by policy to only trusted
operators.

~> **Note**: As of 1.17.x the default behavior has changed to truncate the
signed intermediary's notAfter field to the signing issuer's notAfter
if it is computed to go beyond. Prior versions permitted the notAfter
to go beyond.
stevendpclark marked this conversation as resolved.
Show resolved Hide resolved

| Method | Path | Issuer |
| :----- | :------------------------------------------ | :------- |
| `POST` | `/pki/root/sign-intermediate` | `default` |
Expand Down Expand Up @@ -1411,6 +1416,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
33 changes: 33 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,33 @@
---
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 the sign-intermediate API would permit a calculated notAfter field to go beyond
the signing issuer's notAfter. This would lead to a CA chain that would not validate properly. As
of 1.17.x the default behavior has changed to truncate the intermediary's notAfter value to
the signing issuer's notAfter if calculated to be greater.
stevendpclark marked this conversation as resolved.
Show resolved Hide resolved

#### How to opt out

A new flag has been introduced on the sign-intermediate API `enforce_leaf_not_after_behavior`. Setting
this flag to true, the sign-intermediate API will use the signing issuer's configured
`leaf_not_after_behavior` value to control the behavior. Configuring the issuer to a value of `permit`
will along with setting the `enforce_leaf_not_after_behavior` to true will restore the legacy behavior.
stevendpclark marked this conversation as resolved.
Show resolved Hide resolved

## 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