Skip to content

fix(encryption): allow a sender key with a static key agreement - #696

Merged
Spomky merged 1 commit into
4.2.xfrom
fix/jwe-builder-sender-key
Aug 28, 2026
Merged

fix(encryption): allow a sender key with a static key agreement#696
Spomky merged 1 commit into
4.2.xfrom
fix/jwe-builder-sender-key

Conversation

@Spomky

@Spomky Spomky commented Aug 28, 2026

Copy link
Copy Markdown
Member

Closes #680.

JWEBuilder::withSenderKey() could not be used with a direct static key agreement such as ECDH-SS, and
the sender key was dropped even when the mode check was passed.

What was wrong

  • withSenderKey() derived the key management mode from the shared headers and checked the key against it.
    Called before addRecipient() it threw Invalid content encryption algorithm, because checkKey()
    needs the content encryption algorithm and only addRecipient() sets it. Called after it threw
    Foreign key management mode forbidden., because agree + agree is not a supported combination.
  • determineCEK() read the sender key from $this->recipients[0]['sender_key'], an entry that is never
    written anywhere, so a direct key agreement always reached ECDHSS::getAgreementKey() with no sender key
    and threw The sender key shall be set. processRecipient() did not have the problem thanks to its
    ?? $this->senderKey fallback, which is why ECDH-SS+A128KW and friends worked.
  • JWEDecrypter::decryptUsingKey() passed $senderKey as the fourth argument of decryptUsingKeySet(),
    which is the &$jwk output parameter. The sender key never reached the algorithm, so a token built
    with a static key agreement could not be decrypted back through that method. Found while writing the
    round-trip test: without this one, the builder fix produces tokens nothing can read.

What changed

  • withSenderKey() only stores the key. The sender key does not add a recipient, so it must not go through
    the key management mode compatibility matrix.
  • The key is verified by build() (new checkSenderKey()), against the key encryption algorithm of each
    recipient, where the content encryption algorithm is known whatever the call order is. This also settles
    the TODO that sat on top of the method.
  • determineCEK() falls back to $this->senderKey like processRecipient() does.
  • decryptUsingKey() passes the sender key in the right position.

Tests

tests/Component/Encryption/JWEBuilderSenderKeyTest.phpwithSenderKey() had no coverage at all and no
test exercised ECDH-SS. Six cases: sender key set before and after addRecipient() with an ECDH-SS
round trip, missing sender key, ECDH-SS+A128KW, ECDH-ES where the sender key replaces the ephemeral key
(the published epk is the given key), and the deferred key check at build time. Five of the six fail on
4.2.x.

Note for the reviewer: on the decryption side of a static key agreement the roles are swapped — the public
key of the sender is the key of the key set and the private key of the recipient is the sender key, since
epk is not part of the token. That is the existing convention of ECDHSSAESKW and its tests, not
something introduced here.

Checks

Full test suite (874 tests), ECS and PHPStan are green. One PHPStan baseline entry goes from count: 1 to
2 (Cannot access offset 'key_encryption_algorithm' on mixed): $recipients is an untyped array and the
new loop adds one more access already covered by that rule.

`JWEBuilder::withSenderKey()` derived the key management mode and checked the
key against it, so it could not be used with ECDH-SS: called before
`addRecipient()` the content encryption algorithm was still unknown, and called
after it the "agree" mode was rejected as a foreign key management mode. The
sender key does not add a recipient: it no longer takes part in the
compatibility check and is verified by `build()`, once the recipients and the
content encryption algorithm are known whatever the call order is.

`determineCEK()` read the sender key from a per-recipient entry that is never
written, so a direct key agreement always ended up with no sender key at all. It
now falls back to the sender key of the builder, as `processRecipient()` does.

`JWEDecrypter::decryptUsingKey()` passed the sender key as the fourth argument
of `decryptUsingKeySet()`, which is the output key parameter: the sender key
never reached the algorithm and such a token could not be decrypted back.

Closes #680
@Spomky Spomky added this to the 4.2.1 milestone Aug 28, 2026
@Spomky Spomky self-assigned this Aug 28, 2026
@Spomky Spomky added the bug label Aug 28, 2026
@Spomky
Spomky merged commit eb2308d into 4.2.x Aug 28, 2026
17 checks passed
@Spomky
Spomky deleted the fix/jwe-builder-sender-key branch August 28, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JWEBuilder::withSenderKey() cannot be used with direct key agreement (ECDH-SS)

1 participant