Version(s) affected
4.2.0 (and every previous 4.x)
Description
JWEBuilder::withSenderKey() cannot be used with a direct key agreement algorithm such as ECDH-SS.
Both possible call orders fail:
withSenderKey() before addRecipient() → InvalidArgumentException: Invalid content encryption algorithm,
because withSenderKey() calls checkKey(), which requires $contentEncryptionAlgorithm, and that
property is only populated by addRecipient().
withSenderKey() after addRecipient() → InvalidArgumentException: Foreign key management mode forbidden.,
because withSenderKey() re-derives the key management mode and
areKeyManagementModesCompatible('agree', 'agree') returns false.
On top of that, even if the mode check were bypassed, the sender key would still be dropped:
determineCEK() reads the sender key from a recipient entry that is never written anywhere:
ECDH-SS therefore always ends up in ECDHSS::getAgreementKey() with $senderKey === null, which
throws LogicException: The sender key shall be set.
ECDH-SS+A128KW / +A192KW / +A256KW are unaffected: they go through processRecipient(), whose
?? $this->senderKey fallback recovers the value.
How to reproduce
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHSS;
use Jose\Component\Encryption\JWEBuilder;
use Jose\Component\KeyManagement\JWKFactory;
$recipient = JWKFactory::createECKey('P-256');
$sender = JWKFactory::createECKey('P-256');
$builder = new JWEBuilder(new AlgorithmManager([new ECDHSS(), new A128CBCHS256()]));
// (a) sender key first
$builder->withPayload('hello')
->withSharedProtectedHeader(['alg' => 'ECDH-SS', 'enc' => 'A128CBC-HS256'])
->withSenderKey($sender)
->addRecipient($recipient)
->build();
// InvalidArgumentException: Invalid content encryption algorithm
// (b) recipient first
$builder->withPayload('hello')
->withSharedProtectedHeader(['alg' => 'ECDH-SS', 'enc' => 'A128CBC-HS256'])
->addRecipient($recipient)
->withSenderKey($sender)
->build();
// InvalidArgumentException: Foreign key management mode forbidden.
Possible Solution
- Do not re-derive/re-check the key management mode in
withSenderKey(): the sender key does not add a
recipient, so it must not go through the compatibility matrix. Defer the checkKey() call to
build(), where the content encryption algorithm is known whatever the call order is.
- Make
determineCEK() fall back to $this->senderKey like processRecipient() does, or remove the
sender_key recipient entry entirely if no per-recipient sender key is intended.
Additional Context
withSenderKey() currently has no test coverage at all (grep -r withSenderKey tests/ returns
nothing), and no test exercises ECDH-SS. Tests for both should come with the fix.
Version(s) affected
4.2.0 (and every previous 4.x)
Description
JWEBuilder::withSenderKey()cannot be used with a direct key agreement algorithm such asECDH-SS.Both possible call orders fail:
withSenderKey()beforeaddRecipient()→InvalidArgumentException: Invalid content encryption algorithm,because
withSenderKey()callscheckKey(), which requires$contentEncryptionAlgorithm, and thatproperty is only populated by
addRecipient().withSenderKey()afteraddRecipient()→InvalidArgumentException: Foreign key management mode forbidden.,because
withSenderKey()re-derives the key management mode andareKeyManagementModesCompatible('agree', 'agree')returnsfalse.On top of that, even if the mode check were bypassed, the sender key would still be dropped:
determineCEK()reads the sender key from a recipient entry that is never written anywhere:$senderKey = $this->recipients[0]['sender_key'] ?? null;(alwaysnull)$recipient['sender_key'] ?? $this->senderKey(the?? $this->senderKeyfallback is what makes thekey-wrapping variants work)
ECDH-SStherefore always ends up inECDHSS::getAgreementKey()with$senderKey === null, whichthrows
LogicException: The sender key shall be set.ECDH-SS+A128KW/+A192KW/+A256KWare unaffected: they go throughprocessRecipient(), whose?? $this->senderKeyfallback recovers the value.How to reproduce
Possible Solution
withSenderKey(): the sender key does not add arecipient, so it must not go through the compatibility matrix. Defer the
checkKey()call tobuild(), where the content encryption algorithm is known whatever the call order is.determineCEK()fall back to$this->senderKeylikeprocessRecipient()does, or remove thesender_keyrecipient entry entirely if no per-recipient sender key is intended.Additional Context
withSenderKey()currently has no test coverage at all (grep -r withSenderKey tests/returnsnothing), and no test exercises
ECDH-SS. Tests for both should come with the fix.