Skip to content

Commit 1633fa8

Browse files
authored
Merge pull request #666 from web-token/fix/jwe-header-parameter-confusion
fix(encryption): enforce the disjoint header requirement of RFC 7516
2 parents 8a33a6e + 108400a commit 1633fa8

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)