Skip to content

Commit 108400a

Browse files
Spomkyrossaddison
andcommitted
fix(encryption): enforce the disjoint header requirement of RFC 7516
JWEDecrypter built the complete header with array_merge($sharedProtectedHeader, $sharedHeader, $recipientHeader). Because of the last-wins behaviour of array_merge, an unprotected header parameter was able to override an integrity protected one, "alg" and "enc" included: the HeaderCheckerManager validated the protected values while the decryption used the unprotected ones. RFC 7516 section 7.2.1 requires that "the Header Parameter names in the three locations MUST be disjoint". That requirement is now enforced when the token is read, as it already was when it is written, and the merge order is reversed so that the protected header wins. "alg" and "enc" are also no longer read from the shared unprotected header. The RFC puts no location constraint on them, but that header is not covered by the AAD and, unlike the per-recipient header, nothing requires those parameters to be located there. As a consequence the RFC 7520 sections 5.11 and 5.12 vectors are still parsed but are no longer decrypted; the corresponding tests now assert the refusal. JWEBuilder could produce a token violating the disjoint requirement: with several recipients, the header parameters computed by the key encryption algorithm are added to the per-recipient header without looking at the shared headers. A PBES2 token carrying "p2c" in the shared protected header ended up with "p2c" in both places, and was already rejected by the HeaderCheckerManager. Those parameters are now filtered, which is the behaviour of the single recipient path. Also adds non-empty string guards on "alg" and "enc" and fixes the error message of getContentEncryptionAlgorithm which mentioned the key encryption algorithm. Co-authored-by: Ross Addison <rossaddison@users.noreply.github.com>
1 parent 8a33a6e commit 108400a

5 files changed

Lines changed: 407 additions & 54 deletions

File tree

src/Library/Encryption/JWEBuilder.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ private function checkAndSetContentEncryptionAlgorithm(array $completeHeader): v
257257
}
258258
}
259259

260+
/**
261+
* The header parameters computed by the key encryption algorithm are added to the per-recipient header
262+
* when there is more than one recipient. Those already set in a shared header are filtered out: the
263+
* header parameter names of the three headers must be disjoint (RFC 7516 section 7.2.1), and a shared
264+
* value takes precedence, as it does with a single recipient.
265+
*/
260266
private function processRecipient(array $recipient, string $cek, array &$additionalHeader): Recipient
261267
{
262268
$completeHeader = array_merge($this->sharedHeader, $recipient['header'], $this->sharedProtectedHeader);
@@ -274,6 +280,7 @@ private function processRecipient(array $recipient, string $cek, array &$additio
274280
);
275281
$recipientHeader = $recipient['header'];
276282
if ((is_countable($additionalHeader) ? count($additionalHeader) : 0) !== 0 && count($this->recipients) !== 1) {
283+
$additionalHeader = array_diff_key($additionalHeader, $this->sharedProtectedHeader, $this->sharedHeader);
277284
$recipientHeader = array_merge($recipientHeader, $additionalHeader);
278285
$additionalHeader = [];
279286
}

src/Library/Encryption/JWEDecrypter.php

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyWrapping;
1919
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
2020
use Throwable;
21+
use function count;
2122
use function is_string;
2223
use function sprintf;
2324
use function strlen;
@@ -109,6 +110,15 @@ public function decryptUsingKeySet(
109110
return false;
110111
}
111112

113+
/**
114+
* The header parameter names of the shared protected header, the shared unprotected header and the
115+
* per-recipient header must be disjoint (RFC 7516 section 7.2.1), as enforced by the JWEBuilder when the
116+
* token is created. Otherwise an unprotected parameter is able to redefine a protected one. The headers
117+
* are then merged in the same order as the JWEBuilder does, so that the protected header always wins.
118+
*
119+
* The shared unprotected header is never a valid source for "alg" and "enc": it is not covered by the
120+
* AAD and, unlike the per-recipient header, nothing requires those parameters to be located there.
121+
*/
112122
private function decryptRecipientKey(
113123
JWE $jwe,
114124
JWKSet $jwkset,
@@ -117,15 +127,20 @@ private function decryptRecipientKey(
117127
?JWK $senderKey = null
118128
): ?string {
119129
$recipient = $jwe->getRecipient($i);
120-
$completeHeader = array_merge(
121-
$jwe->getSharedProtectedHeader(),
122-
$jwe->getSharedHeader(),
123-
$recipient->getHeader()
124-
);
130+
$sharedProtectedHeader = $jwe->getSharedProtectedHeader();
131+
$sharedHeader = $jwe->getSharedHeader();
132+
$recipientHeader = $recipient->getHeader();
133+
134+
$this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $sharedHeader);
135+
$this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $recipientHeader);
136+
$this->checkDuplicatedHeaderParameters($sharedHeader, $recipientHeader);
137+
138+
$completeHeader = array_merge($sharedHeader, $recipientHeader, $sharedProtectedHeader);
125139
$this->checkCompleteHeader($completeHeader);
126140

127-
$key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($completeHeader);
128-
$content_encryption_algorithm = $this->getContentEncryptionAlgorithm($completeHeader);
141+
$protectedAndRecipientHeader = array_merge($recipientHeader, $sharedProtectedHeader);
142+
$key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($protectedAndRecipientHeader);
143+
$content_encryption_algorithm = $this->getContentEncryptionAlgorithm($protectedAndRecipientHeader);
129144

130145
$this->checkIvSize($jwe->getIV(), $content_encryption_algorithm->getIVSize());
131146

@@ -253,29 +268,52 @@ private function checkCompleteHeader(array $completeHeaders): void
253268
}
254269
}
255270

256-
private function getKeyEncryptionAlgorithm(array $completeHeaders): KeyEncryptionAlgorithm
271+
private function getKeyEncryptionAlgorithm(array $header): KeyEncryptionAlgorithm
257272
{
258-
$key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($completeHeaders['alg']);
273+
$alg = $header['alg'] ?? null;
274+
if (! is_string($alg) || $alg === '') {
275+
throw new InvalidArgumentException(
276+
'The "alg" parameter must be a non-empty string set in the protected header or in the recipient header.'
277+
);
278+
}
279+
$key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($alg);
259280
if (! $key_encryption_algorithm instanceof KeyEncryptionAlgorithm) {
260281
throw new InvalidArgumentException(sprintf(
261282
'The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithm interface.',
262-
$completeHeaders['alg']
283+
$alg
263284
));
264285
}
265286

266287
return $key_encryption_algorithm;
267288
}
268289

269-
private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm
290+
private function getContentEncryptionAlgorithm(array $header): ContentEncryptionAlgorithm
270291
{
271-
$content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']);
292+
$enc = $header['enc'] ?? null;
293+
if (! is_string($enc) || $enc === '') {
294+
throw new InvalidArgumentException(
295+
'The "enc" parameter must be a non-empty string set in the protected header or in the recipient header.'
296+
);
297+
}
298+
$content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($enc);
272299
if (! $content_encryption_algorithm instanceof ContentEncryptionAlgorithm) {
273300
throw new InvalidArgumentException(sprintf(
274-
'The key encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.',
275-
$completeHeader['enc']
301+
'The content encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.',
302+
$enc
276303
));
277304
}
278305

279306
return $content_encryption_algorithm;
280307
}
308+
309+
private function checkDuplicatedHeaderParameters(array $header1, array $header2): void
310+
{
311+
$inter = array_intersect_key($header1, $header2);
312+
if (count($inter) !== 0) {
313+
throw new InvalidArgumentException(sprintf(
314+
'The header contains duplicated entries: %s.',
315+
implode(', ', array_keys($inter))
316+
));
317+
}
318+
}
281319
}

0 commit comments

Comments
 (0)