Skip to content

Commit 262ffbd

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 262ffbd

5 files changed

Lines changed: 399 additions & 54 deletions

File tree

src/Library/Encryption/JWEBuilder.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ private function processRecipient(array $recipient, string $cek, array &$additio
274274
);
275275
$recipientHeader = $recipient['header'];
276276
if ((is_countable($additionalHeader) ? count($additionalHeader) : 0) !== 0 && count($this->recipients) !== 1) {
277+
// The header parameter names of the three headers must be disjoint (RFC 7516 section 7.2.1). A
278+
// parameter already set in a shared header keeps its value, as it does with a single recipient.
279+
$additionalHeader = array_diff_key($additionalHeader, $this->sharedProtectedHeader, $this->sharedHeader);
277280
$recipientHeader = array_merge($recipientHeader, $additionalHeader);
278281
$additionalHeader = [];
279282
}

src/Library/Encryption/JWEDecrypter.php

Lines changed: 48 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;
@@ -117,15 +118,25 @@ private function decryptRecipientKey(
117118
?JWK $senderKey = null
118119
): ?string {
119120
$recipient = $jwe->getRecipient($i);
120-
$completeHeader = array_merge(
121-
$jwe->getSharedProtectedHeader(),
122-
$jwe->getSharedHeader(),
123-
$recipient->getHeader()
124-
);
121+
$sharedProtectedHeader = $jwe->getSharedProtectedHeader();
122+
$sharedHeader = $jwe->getSharedHeader();
123+
$recipientHeader = $recipient->getHeader();
124+
125+
// The three headers must be disjoint, as enforced by the JWEBuilder when the token is created.
126+
// Otherwise an unprotected parameter is able to redefine a protected one, "alg" and "enc" included.
127+
$this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $sharedHeader);
128+
$this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $recipientHeader);
129+
$this->checkDuplicatedHeaderParameters($sharedHeader, $recipientHeader);
130+
131+
// Same order as the JWEBuilder: the protected header always wins.
132+
$completeHeader = array_merge($sharedHeader, $recipientHeader, $sharedProtectedHeader);
125133
$this->checkCompleteHeader($completeHeader);
126134

127-
$key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($completeHeader);
128-
$content_encryption_algorithm = $this->getContentEncryptionAlgorithm($completeHeader);
135+
// The shared unprotected header is never a valid source for "alg" and "enc": it is not covered by the
136+
// AAD and, unlike the per-recipient header, nothing requires those parameters to be located there.
137+
$protectedAndRecipientHeader = array_merge($recipientHeader, $sharedProtectedHeader);
138+
$key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($protectedAndRecipientHeader);
139+
$content_encryption_algorithm = $this->getContentEncryptionAlgorithm($protectedAndRecipientHeader);
129140

130141
$this->checkIvSize($jwe->getIV(), $content_encryption_algorithm->getIVSize());
131142

@@ -253,29 +264,52 @@ private function checkCompleteHeader(array $completeHeaders): void
253264
}
254265
}
255266

256-
private function getKeyEncryptionAlgorithm(array $completeHeaders): KeyEncryptionAlgorithm
267+
private function getKeyEncryptionAlgorithm(array $header): KeyEncryptionAlgorithm
257268
{
258-
$key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($completeHeaders['alg']);
269+
$alg = $header['alg'] ?? null;
270+
if (! is_string($alg) || $alg === '') {
271+
throw new InvalidArgumentException(
272+
'The "alg" parameter must be a non-empty string set in the protected header or in the recipient header.'
273+
);
274+
}
275+
$key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($alg);
259276
if (! $key_encryption_algorithm instanceof KeyEncryptionAlgorithm) {
260277
throw new InvalidArgumentException(sprintf(
261278
'The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithm interface.',
262-
$completeHeaders['alg']
279+
$alg
263280
));
264281
}
265282

266283
return $key_encryption_algorithm;
267284
}
268285

269-
private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm
286+
private function getContentEncryptionAlgorithm(array $header): ContentEncryptionAlgorithm
270287
{
271-
$content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']);
288+
$enc = $header['enc'] ?? null;
289+
if (! is_string($enc) || $enc === '') {
290+
throw new InvalidArgumentException(
291+
'The "enc" parameter must be a non-empty string set in the protected header or in the recipient header.'
292+
);
293+
}
294+
$content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($enc);
272295
if (! $content_encryption_algorithm instanceof ContentEncryptionAlgorithm) {
273296
throw new InvalidArgumentException(sprintf(
274-
'The key encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.',
275-
$completeHeader['enc']
297+
'The content encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.',
298+
$enc
276299
));
277300
}
278301

279302
return $content_encryption_algorithm;
280303
}
304+
305+
private function checkDuplicatedHeaderParameters(array $header1, array $header2): void
306+
{
307+
$inter = array_intersect_key($header1, $header2);
308+
if (count($inter) !== 0) {
309+
throw new InvalidArgumentException(sprintf(
310+
'The header contains duplicated entries: %s.',
311+
implode(', ', array_keys($inter))
312+
));
313+
}
314+
}
281315
}

0 commit comments

Comments
 (0)