Skip to content

fix(jwe-decrypt): accept JWE tokens that authenticate the protected header - #13889

Open
AlinsRan wants to merge 3 commits into
apache:masterfrom
AlinsRan:feat/jwe-decrypt-rfc7516-aad
Open

fix(jwe-decrypt): accept JWE tokens that authenticate the protected header#13889
AlinsRan wants to merge 3 commits into
apache:masterfrom
AlinsRan:feat/jwe-decrypt-rfc7516-aad

Conversation

@AlinsRan

Copy link
Copy Markdown
Contributor

Description

jwe-decrypt decrypts the token with no additional authenticated data:

local decrypted, err = aes_default:decrypt(dec(o.ciphertext), dec(o.tag))

RFC 7516 §5.1 makes the encoded protected header the AES-GCM AAD for compact serialization, so every JWE library computes the tag over it. A token from a compliant producer therefore never authenticates and is rejected with 400 failed to decrypt JWE token, even though the key, IV and ciphertext are all correct.

Reproduced against master with a token generated by python cryptography (AESGCM(key).encrypt(iv, payload, aad=protected_header)): the identical token with the tag computed without AAD is accepted, the compliant one is rejected.

This PR tries the RFC 7516 form first and falls back to decrypting without AAD, so tokens generated the way APISIX itself used to generate them — including the one in the plugin documentation and everything produced by the /apisix/plugin/jwe/encrypt endpoint removed in #13464 — keep working. Authenticating the header also makes kid tamper-proof for compliant tokens: swapping it breaks the tag even when the two Consumers share a secret.

It also rejects a header that asks for an alg or enc the plugin does not implement (only dir / A256GCM are), instead of letting it fail later as a decryption error. Tokens that omit either field keep working, and a token that names another algorithm cannot decrypt today anyway, so no working configuration changes.

Trade-off worth naming: a legacy token now costs two GCM operations instead of one, since the AAD attempt runs first.

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible

Tests: TEST 26 accepts an RFC 7516 token produced by an independent library, TEST 27 pins the legacy no-AAD token, TEST 28 shows a kid swap on a compliant token is rejected across Consumers sharing a secret, TEST 29/30 cover the unsupported alg / enc. TEST 26, 29 and 30 fail on master.

Note: this touches jwe_decrypt_with_obj(), the same function as #13844; whichever lands first, I will rebase the other.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 27, 2026
@AlinsRan
AlinsRan force-pushed the feat/jwe-decrypt-rfc7516-aad branch from 6cee77e to 7011949 Compare August 27, 2026 09:34
nic-6443
nic-6443 previously approved these changes Aug 28, 2026
membphis
membphis previously approved these changes Aug 28, 2026

@membphis membphis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

…eader

RFC 7516 section 5.1 makes the encoded protected header the AES-GCM
additional authenticated data, so every JWE library computes the tag over
it. The plugin decrypts with no AAD, so a token produced by a compliant
library never authenticates and is rejected with 400.

Try the RFC 7516 form first and fall back to decrypting without AAD, so
tokens generated the way APISIX itself used to generate them keep working.
Authenticating the header also makes `kid` tamper-proof for compliant
tokens: replacing it now breaks the tag even when the two Consumers share
a secret.

Also reject a header that asks for an `alg` or `enc` the plugin does not
implement, instead of reporting a decryption failure for it.
Only a header naming an algorithm the plugin does not implement is
rejected; an absent alg or enc stays accepted, since such tokens
decrypt today and nothing in the plugin branches on either field.
@AlinsRan
AlinsRan dismissed stale reviews from membphis and nic-6443 via 005d148 August 31, 2026 03:55
@AlinsRan
AlinsRan force-pushed the feat/jwe-decrypt-rfc7516-aad branch from 7011949 to 005d148 Compare August 31, 2026 03:55
nic-6443
nic-6443 previously approved these changes Aug 31, 2026
membphis
membphis previously approved these changes Aug 31, 2026

@membphis membphis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds RFC 7516-compatible protected-header authentication while retaining legacy token support.

Changes:

  • Uses the protected header as AES-GCM AAD, with a legacy fallback.
  • Validates alg and enc.
  • Adds interoperability tests and updates documentation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
apisix/plugins/jwe-decrypt.lua Implements AAD decryption and algorithm validation.
t/plugin/jwe-decrypt.t Adds compatibility, tampering, and validation tests.
docs/en/latest/plugins/jwe-decrypt.md Documents standard and legacy JWE behavior.
docs/zh/latest/plugins/jwe-decrypt.md Updates Chinese JWE guidance.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apisix/plugins/jwe-decrypt.lua Outdated
Comment thread t/plugin/jwe-decrypt.t Outdated
Comment thread t/plugin/jwe-decrypt.t Outdated
A JSON false decodes to a Lua false, so the truthiness check let a header
carrying "alg": false through the algorithm validation. Compare against
nil so the backward compatible path only covers a genuinely absent field.

Also make the AAD tamper test point at the token it is derived from and
target a Consumer that really shares the secret, so the rejection can
only come from the header no longer being authenticated.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants