diff --git a/src/Library/Encryption/JWEBuilder.php b/src/Library/Encryption/JWEBuilder.php index 7c5ac213..82aebe30 100644 --- a/src/Library/Encryption/JWEBuilder.php +++ b/src/Library/Encryption/JWEBuilder.php @@ -257,6 +257,12 @@ private function checkAndSetContentEncryptionAlgorithm(array $completeHeader): v } } + /** + * The header parameters computed by the key encryption algorithm are added to the per-recipient header + * when there is more than one recipient. Those already set in a shared header are filtered out: the + * header parameter names of the three headers must be disjoint (RFC 7516 section 7.2.1), and a shared + * value takes precedence, as it does with a single recipient. + */ private function processRecipient(array $recipient, string $cek, array &$additionalHeader): Recipient { $completeHeader = array_merge($this->sharedHeader, $recipient['header'], $this->sharedProtectedHeader); @@ -274,6 +280,7 @@ private function processRecipient(array $recipient, string $cek, array &$additio ); $recipientHeader = $recipient['header']; if ((is_countable($additionalHeader) ? count($additionalHeader) : 0) !== 0 && count($this->recipients) !== 1) { + $additionalHeader = array_diff_key($additionalHeader, $this->sharedProtectedHeader, $this->sharedHeader); $recipientHeader = array_merge($recipientHeader, $additionalHeader); $additionalHeader = []; } diff --git a/src/Library/Encryption/JWEDecrypter.php b/src/Library/Encryption/JWEDecrypter.php index b70b3296..f298fab6 100644 --- a/src/Library/Encryption/JWEDecrypter.php +++ b/src/Library/Encryption/JWEDecrypter.php @@ -18,6 +18,7 @@ use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyWrapping; use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm; use Throwable; +use function count; use function is_string; use function sprintf; use function strlen; @@ -109,6 +110,15 @@ public function decryptUsingKeySet( return false; } + /** + * The header parameter names of the shared protected header, the shared unprotected header and the + * per-recipient header must be disjoint (RFC 7516 section 7.2.1), as enforced by the JWEBuilder when the + * token is created. Otherwise an unprotected parameter is able to redefine a protected one. The headers + * are then merged in the same order as the JWEBuilder does, so that the protected header always wins. + * + * The shared unprotected header is never a valid source for "alg" and "enc": it is not covered by the + * AAD and, unlike the per-recipient header, nothing requires those parameters to be located there. + */ private function decryptRecipientKey( JWE $jwe, JWKSet $jwkset, @@ -117,15 +127,20 @@ private function decryptRecipientKey( ?JWK $senderKey = null ): ?string { $recipient = $jwe->getRecipient($i); - $completeHeader = array_merge( - $jwe->getSharedProtectedHeader(), - $jwe->getSharedHeader(), - $recipient->getHeader() - ); + $sharedProtectedHeader = $jwe->getSharedProtectedHeader(); + $sharedHeader = $jwe->getSharedHeader(); + $recipientHeader = $recipient->getHeader(); + + $this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $sharedHeader); + $this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $recipientHeader); + $this->checkDuplicatedHeaderParameters($sharedHeader, $recipientHeader); + + $completeHeader = array_merge($sharedHeader, $recipientHeader, $sharedProtectedHeader); $this->checkCompleteHeader($completeHeader); - $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($completeHeader); - $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($completeHeader); + $protectedAndRecipientHeader = array_merge($recipientHeader, $sharedProtectedHeader); + $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($protectedAndRecipientHeader); + $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($protectedAndRecipientHeader); $this->checkIvSize($jwe->getIV(), $content_encryption_algorithm->getIVSize()); @@ -253,29 +268,52 @@ private function checkCompleteHeader(array $completeHeaders): void } } - private function getKeyEncryptionAlgorithm(array $completeHeaders): KeyEncryptionAlgorithm + private function getKeyEncryptionAlgorithm(array $header): KeyEncryptionAlgorithm { - $key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($completeHeaders['alg']); + $alg = $header['alg'] ?? null; + if (! is_string($alg) || $alg === '') { + throw new InvalidArgumentException( + 'The "alg" parameter must be a non-empty string set in the protected header or in the recipient header.' + ); + } + $key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($alg); if (! $key_encryption_algorithm instanceof KeyEncryptionAlgorithm) { throw new InvalidArgumentException(sprintf( 'The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithm interface.', - $completeHeaders['alg'] + $alg )); } return $key_encryption_algorithm; } - private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm + private function getContentEncryptionAlgorithm(array $header): ContentEncryptionAlgorithm { - $content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']); + $enc = $header['enc'] ?? null; + if (! is_string($enc) || $enc === '') { + throw new InvalidArgumentException( + 'The "enc" parameter must be a non-empty string set in the protected header or in the recipient header.' + ); + } + $content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($enc); if (! $content_encryption_algorithm instanceof ContentEncryptionAlgorithm) { throw new InvalidArgumentException(sprintf( - 'The key encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.', - $completeHeader['enc'] + 'The content encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.', + $enc )); } return $content_encryption_algorithm; } + + private function checkDuplicatedHeaderParameters(array $header1, array $header2): void + { + $inter = array_intersect_key($header1, $header2); + if (count($inter) !== 0) { + throw new InvalidArgumentException(sprintf( + 'The header contains duplicated entries: %s.', + implode(', ', array_keys($inter)) + )); + } + } } diff --git a/tests/Component/Encryption/HeaderParameterConfusionTest.php b/tests/Component/Encryption/HeaderParameterConfusionTest.php new file mode 100644 index 00000000..fef475e4 --- /dev/null +++ b/tests/Component/Encryption/HeaderParameterConfusionTest.php @@ -0,0 +1,294 @@ +createFlattenedToken(); + $token['header'] = [ + 'alg' => 'dir', + ]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The header contains duplicated entries: alg.'); + + $this->decrypt($token); + } + + #[Test] + public function theKeyEncryptionAlgorithmCannotBeOverriddenByTheSharedUnprotectedHeader(): void + { + $token = $this->createFlattenedToken(); + $token['unprotected'] = [ + 'alg' => 'dir', + ]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The header contains duplicated entries: alg.'); + + $this->decrypt($token); + } + + #[Test] + public function theContentEncryptionAlgorithmCannotBeOverriddenByAnUnprotectedHeader(): void + { + $token = $this->createFlattenedToken(); + $token['unprotected'] = [ + 'enc' => 'A256GCM', + ]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The header contains duplicated entries: enc.'); + + $this->decrypt($token); + } + + #[Test] + public function theContentEncryptionAlgorithmCannotBeOverriddenByARecipientHeader(): void + { + $token = $this->createFlattenedToken(); + $token['header'] = [ + 'enc' => 'A256GCM', + ]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The header contains duplicated entries: enc.'); + + $this->decrypt($token); + } + + #[Test] + public function aNonStringAlgorithmIsRejected(): void + { + $token = $this->createFlattenedToken(); + $protectedHeader = JsonConverter::decode(Base64UrlSafe::decodeNoPadding($token['protected'])); + $protectedHeader['alg'] = ['A128KW']; + $token['protected'] = Base64UrlSafe::encodeUnpadded(JsonConverter::encode($protectedHeader)); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The "alg" parameter must be a non-empty string set in the protected header or in the recipient header.' + ); + + $this->decrypt($token); + } + + #[Test] + public function theKeyEncryptionAlgorithmCannotComeFromTheSharedUnprotectedHeader(): void + { + $token = $this->createFlattenedToken(); + $alg = $this->moveOutOfTheProtectedHeader($token, 'alg'); + $token['unprotected'] = [ + 'alg' => $alg, + ]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The "alg" parameter must be a non-empty string set in the protected header or in the recipient header.' + ); + + $this->decrypt($token); + } + + #[Test] + public function theContentEncryptionAlgorithmCannotComeFromTheSharedUnprotectedHeader(): void + { + $token = $this->createFlattenedToken(); + $enc = $this->moveOutOfTheProtectedHeader($token, 'enc'); + $token['unprotected'] = [ + 'enc' => $enc, + ]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The "enc" parameter must be a non-empty string set in the protected header or in the recipient header.' + ); + + $this->decrypt($token); + } + + /** + * RFC 7516 puts no location constraint on "enc": only "zip" must occur within the protected header + * (section 4.1.3). A per-recipient "enc" is therefore unusual, but it is not invalid. + */ + #[Test] + public function theContentEncryptionAlgorithmMayComeFromTheRecipientHeader(): void + { + $key = $this->createSharedKey(); + $jwe = $this->getJWEBuilderFactory() + ->create(['A128KW', 'A128GCM']) + ->create() + ->withPayload('Live long and prosper.') + ->withSharedProtectedHeader([ + 'alg' => 'A128KW', + ]) + ->addRecipient($key, [ + 'enc' => 'A128GCM', + ]) + ->build(); + $token = $this->getJWESerializerManager() + ->serialize('jwe_json_flattened', $jwe, 0); + + $jwe = $this->getJWESerializerManager() + ->unserialize($token); + $decrypter = $this->getJWEDecrypterFactory() + ->create(['A128KW', 'A128GCM']); + + static::assertTrue($decrypter->decryptUsingKey($jwe, $key, 0)); + static::assertSame('Live long and prosper.', $jwe->getPayload()); + } + + /** + * In the JSON General Serialization, the key management algorithm legitimately lives in the per-recipient + * header: it is not part of the AAD and may differ from one recipient to the other (RFC 7516 section 7.2.1). + */ + #[Test] + public function theKeyEncryptionAlgorithmMayComeFromTheRecipientHeader(): void + { + $key = $this->createSharedKey(); + $jwe = $this->getJWEBuilderFactory() + ->create(['A128KW', 'A256KW', 'A128GCM']) + ->create() + ->withPayload('Live long and prosper.') + ->withSharedProtectedHeader([ + 'enc' => 'A128GCM', + ]) + ->addRecipient($key, [ + 'alg' => 'A128KW', + ]) + ->addRecipient($this->createOtherSharedKey(), [ + 'alg' => 'A256KW', + ]) + ->build(); + $token = $this->getJWESerializerManager() + ->serialize('jwe_json_general', $jwe); + + $jwe = $this->getJWESerializerManager() + ->unserialize($token); + $decrypter = $this->getJWEDecrypterFactory() + ->create(['A128KW', 'A256KW', 'A128GCM']); + + static::assertTrue($decrypter->decryptUsingKey($jwe, $key, 0)); + static::assertSame('Live long and prosper.', $jwe->getPayload()); + } + + /** + * The header parameters computed by the key encryption algorithm are added to the per-recipient header + * when there are several recipients. They must not collide with a shared header, otherwise the builder + * produces a token that the decrypter, the header checkers and RFC 7516 section 7.2.1 all reject. + */ + #[Test] + public function theBuilderNeverProducesDuplicatedHeaderParameters(): void + { + $key = $this->createSharedKey(); + $jwe = $this->getJWEBuilderFactory() + ->create(['PBES2-HS256+A128KW', 'A128GCM']) + ->create() + ->withPayload('Live long and prosper.') + ->withSharedProtectedHeader([ + 'alg' => 'PBES2-HS256+A128KW', + 'enc' => 'A128GCM', + 'p2c' => 4096, + ]) + ->addRecipient($key) + ->addRecipient($this->createOtherSharedKey()) + ->build(); + $token = $this->getJWESerializerManager() + ->serialize('jwe_json_general', $jwe); + + static::assertArrayNotHasKey('p2c', $jwe->getRecipient(0)->getHeader()); + + $jwe = $this->getJWESerializerManager() + ->unserialize($token); + $decrypter = $this->getJWEDecrypterFactory() + ->create(['PBES2-HS256+A128KW', 'A128GCM']); + + static::assertTrue($decrypter->decryptUsingKey($jwe, $key, 0)); + static::assertSame('Live long and prosper.', $jwe->getPayload()); + } + + /** + * @return array + */ + private function createFlattenedToken(): array + { + $jwe = $this->getJWEBuilderFactory() + ->create(['A128KW', 'A128GCM']) + ->create() + ->withPayload('Live long and prosper.') + ->withSharedProtectedHeader([ + 'alg' => 'A128KW', + 'enc' => 'A128GCM', + ]) + ->addRecipient($this->createSharedKey()) + ->build(); + + return JsonConverter::decode( + $this->getJWESerializerManager() + ->serialize('jwe_json_flattened', $jwe) + ); + } + + /** + * @param array $token + */ + private function moveOutOfTheProtectedHeader(array &$token, string $parameter): mixed + { + $protectedHeader = JsonConverter::decode(Base64UrlSafe::decodeNoPadding($token['protected'])); + $value = $protectedHeader[$parameter]; + unset($protectedHeader[$parameter]); + $token['protected'] = Base64UrlSafe::encodeUnpadded(JsonConverter::encode($protectedHeader)); + + return $value; + } + + /** + * @param array $token + */ + private function decrypt(array $token): void + { + $jwe = $this->getJWESerializerManager() + ->unserialize(JsonConverter::encode($token)); + $this->getJWEDecrypterFactory() + ->create(['A128KW', 'dir', 'A128GCM', 'A256GCM']) + ->decryptUsingKey($jwe, $this->createSharedKey(), 0); + } + + private function createSharedKey(): JWK + { + return new JWK([ + 'kty' => 'oct', + 'k' => 'GawgguFyGrWKav7AX4VKUg', + ]); + } + + private function createOtherSharedKey(): JWK + { + return new JWK([ + 'kty' => 'oct', + 'k' => 'iSjSbTfB5aumWmT9v65p2mCVvbLBmrTPLPMhFEHCBJs', + ]); + } +} diff --git a/tests/Component/Encryption/RFC7520/A128KWAndA128GCMEncryptionProtectedContentOnlyTest.php b/tests/Component/Encryption/RFC7520/A128KWAndA128GCMEncryptionProtectedContentOnlyTest.php index 9ec432be..b61c900d 100644 --- a/tests/Component/Encryption/RFC7520/A128KWAndA128GCMEncryptionProtectedContentOnlyTest.php +++ b/tests/Component/Encryption/RFC7520/A128KWAndA128GCMEncryptionProtectedContentOnlyTest.php @@ -4,27 +4,28 @@ namespace Jose\Tests\Component\Encryption\RFC7520; +use InvalidArgumentException; use Jose\Component\Core\JWK; use Jose\Component\Core\Util\Base64UrlSafe; +use Jose\Component\Encryption\JWE; use Jose\Tests\Component\Encryption\EncryptionTestCase; use PHPUnit\Framework\Attributes\Test; /** * @see https://tools.ietf.org/html/rfc7520#section-5.12 * + * In this example, the JWE Protected Header is empty: "alg" and "enc" are carried by the shared unprotected + * header and are therefore not covered by the AAD. Such a token is parsed, but its decryption is refused: + * the key management and content encryption algorithms are only read from the protected header (or, for + * "alg", from the per-recipient header, see RFC 7516 section 7.2.1). + * * @internal */ final class A128KWAndA128GCMEncryptionProtectedContentOnlyTest extends EncryptionTestCase { - /** - * Please note that we cannot the encryption and get the same result as the example (IV, TAG and other data are - * always different). The output given in the RFC is used and only decrypted. - */ #[Test] public function a128KWAndA128GCMEncryptionProtectedContentOnly(): void { - $expected_payload = "You can trust us to stick with you through thick and thin\xe2\x80\x93to the bitter end. And you can trust us to keep any secret of yours\xe2\x80\x93closer than you keep it yourself. But you cannot trust us to let you face trouble alone, and go off without a word. We are your friends, Frodo."; - $private_key = new JWK([ 'kty' => 'oct', 'kid' => '81b20965-8332-43d9-a468-82160ad91ac8', @@ -48,16 +49,10 @@ public function a128KWAndA128GCMEncryptionProtectedContentOnly(): void $expected_ciphertext = 'qtPIMMaOBRgASL10dNQhOa7Gqrk7Eal1vwht7R4TT1uq-arsVCPaIeFwQfzrSS6oEUWbBtxEasE0vC6r7sphyVziMCVJEuRJyoAHFSP3eqQPb4Ic1SDSqyXjw_L3svybhHYUGyQuTmUQEDjgjJfBOifwHIsDsRPeBz1NomqeifVPq5GTCWFo5k_MNIQURR2Wj0AHC2k7JZfu2iWjUHLF8ExFZLZ4nlmsvJu_mvifMYiikfNfsZAudISOa6O73yPZtL04k_1FI7WDfrb2w7OqKLWDXzlpcxohPVOLQwpA3mFNRKdY-bQz4Z4KX9lfz1cne31N4-8BKmojpw-OdQjKdLOGkC445Fb_K1tlDQXw2sBF'; $expected_tag = 'e2m0Vm7JvjK2VpCKXS-kyg'; - $jweDecrypter = $this->getJWEDecrypterFactory() - ->create(['A128KW', 'A128GCM']); - $loaded_flattened_json = $this->getJWESerializerManager() ->unserialize($expected_flattened_json); - static::assertTrue($jweDecrypter->decryptUsingKey($loaded_flattened_json, $private_key, 0)); - $loaded_json = $this->getJWESerializerManager() ->unserialize($expected_json); - static::assertTrue($jweDecrypter->decryptUsingKey($loaded_json, $private_key, 0)); static::assertSame( $expected_ciphertext, @@ -82,8 +77,8 @@ public function a128KWAndA128GCMEncryptionProtectedContentOnly(): void static::assertEqualsCanonicalizing($header, $loaded_json->getSharedHeader()); static::assertSame($expected_tag, Base64UrlSafe::encodeUnpadded($loaded_json->getTag())); - static::assertSame($expected_payload, $loaded_flattened_json->getPayload()); - static::assertSame($expected_payload, $loaded_json->getPayload()); + $this->assertDecryptionIsRefused($loaded_flattened_json, $private_key); + $this->assertDecryptionIsRefused($loaded_json, $private_key); } /** @@ -112,8 +107,6 @@ public function a128KWAndA128GCMEncryptionProtectedContentOnlyBis(): void $jweBuilder = $this->getJWEBuilderFactory() ->create(['A128KW', 'A128GCM']); - $jweDecrypter = $this->getJWEDecrypterFactory() - ->create(['A128KW', 'A128GCM']); $jwe = $jweBuilder ->create() @@ -125,11 +118,8 @@ public function a128KWAndA128GCMEncryptionProtectedContentOnlyBis(): void $loaded_flattened_json = $this->getJWESerializerManager() ->unserialize($this->getJWESerializerManager()->serialize('jwe_json_flattened', $jwe, 0)); - static::assertTrue($jweDecrypter->decryptUsingKey($loaded_flattened_json, $private_key, 0)); - $loaded_json = $this->getJWESerializerManager() ->unserialize($this->getJWESerializerManager()->serialize('jwe_json_general', $jwe)); - static::assertTrue($jweDecrypter->decryptUsingKey($loaded_json, $private_key, 0)); static::assertSame($protectedHeader, $loaded_flattened_json->getSharedProtectedHeader()); static::assertSame($header, $loaded_flattened_json->getSharedHeader()); @@ -137,7 +127,23 @@ public function a128KWAndA128GCMEncryptionProtectedContentOnlyBis(): void static::assertSame($protectedHeader, $loaded_json->getSharedProtectedHeader()); static::assertSame($header, $loaded_json->getSharedHeader()); - static::assertSame($expected_payload, $loaded_flattened_json->getPayload()); - static::assertSame($expected_payload, $loaded_json->getPayload()); + $this->assertDecryptionIsRefused($loaded_flattened_json, $private_key); + $this->assertDecryptionIsRefused($loaded_json, $private_key); + } + + private function assertDecryptionIsRefused(JWE $jwe, JWK $key): void + { + $jweDecrypter = $this->getJWEDecrypterFactory() + ->create(['A128KW', 'A128GCM']); + + try { + $jweDecrypter->decryptUsingKey($jwe, $key, 0); + static::fail('The token should not be decrypted: "alg" and "enc" are not protected.'); + } catch (InvalidArgumentException $exception) { + static::assertSame( + 'The "alg" parameter must be a non-empty string set in the protected header or in the recipient header.', + $exception->getMessage() + ); + } } } diff --git a/tests/Component/Encryption/RFC7520/A128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValuesTest.php b/tests/Component/Encryption/RFC7520/A128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValuesTest.php index 83d3495d..c1cd2557 100644 --- a/tests/Component/Encryption/RFC7520/A128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValuesTest.php +++ b/tests/Component/Encryption/RFC7520/A128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValuesTest.php @@ -4,27 +4,30 @@ namespace Jose\Tests\Component\Encryption\RFC7520; +use InvalidArgumentException; use Jose\Component\Core\JWK; use Jose\Component\Core\Util\Base64UrlSafe; +use Jose\Component\Encryption\JWE; use Jose\Tests\Component\Encryption\EncryptionTestCase; use PHPUnit\Framework\Attributes\Test; /** * @see https://tools.ietf.org/html/rfc7520#section-5.11 * + * In this example, only "enc" is protected: "alg" is carried by the shared unprotected header and is + * therefore not covered by the AAD. Such a token is parsed, but its decryption is refused: the key management + * algorithm is only read from the protected header or from the per-recipient header (RFC 7516 section 7.2.1). + * * @internal */ final class A128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValuesTest extends EncryptionTestCase { /** - * Please note that we cannot the encryption and get the same result as the example (IV, TAG and other data are - * always different). The output given in the RFC is used and only decrypted. + * The output given in the RFC is used and only parsed: the token cannot be decrypted anymore. */ #[Test] public function a128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValues(): void { - $expected_payload = "You can trust us to stick with you through thick and thin\xe2\x80\x93to the bitter end. And you can trust us to keep any secret of yours\xe2\x80\x93closer than you keep it yourself. But you cannot trust us to let you face trouble alone, and go off without a word. We are your friends, Frodo."; - $private_key = new JWK([ 'kty' => 'oct', 'kid' => '81b20965-8332-43d9-a468-82160ad91ac8', @@ -49,16 +52,10 @@ public function a128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValues(): v $expected_ciphertext = 'lIbCyRmRJxnB2yLQOTqjCDKV3H30ossOw3uD9DPsqLL2DM3swKkjOwQyZtWsFLYMj5YeLht_StAn21tHmQJuuNt64T8D4t6C7kC9OCCJ1IHAolUv4MyOt80MoPb8fZYbNKqplzYJgIL58g8N2v46OgyG637d6uuKPwhAnTGm_zWhqc_srOvgiLkzyFXPq1hBAURbc3-8BqeRb48iR1-_5g5UjWVD3lgiLCN_P7AW8mIiFvUNXBPJK3nOWL4teUPS8yHLbWeL83olU4UAgL48x-8dDkH23JykibVSQju-f7e-1xreHWXzWLHs1NqBbre0dEwK3HX_xM0LjUz77Krppgegoutpf5qaKg3l-_xMINmf'; $expected_tag = 'fNYLqpUe84KD45lvDiaBAQ'; - $jweDecrypter = $this->getJWEDecrypterFactory() - ->create(['A128KW', 'A128GCM']); - $loaded_flattened_json = $this->getJWESerializerManager() ->unserialize($expected_flattened_json); - static::assertTrue($jweDecrypter->decryptUsingKey($loaded_flattened_json, $private_key, 0)); - $loaded_json = $this->getJWESerializerManager() ->unserialize($expected_json); - static::assertTrue($jweDecrypter->decryptUsingKey($loaded_json, $private_key, 0)); static::assertSame( $expected_ciphertext, @@ -83,8 +80,8 @@ public function a128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValues(): v static::assertSame($header, $loaded_json->getSharedHeader()); static::assertSame($expected_tag, Base64UrlSafe::encodeUnpadded($loaded_json->getTag())); - static::assertSame($expected_payload, $loaded_flattened_json->getPayload()); - static::assertSame($expected_payload, $loaded_json->getPayload()); + $this->assertDecryptionIsRefused($loaded_flattened_json, $private_key); + $this->assertDecryptionIsRefused($loaded_json, $private_key); } /** @@ -114,8 +111,6 @@ public function a128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValuesBis() $jweBuilder = $this->getJWEBuilderFactory() ->create(['A128KW', 'A128GCM']); - $jweDecrypter = $this->getJWEDecrypterFactory() - ->create(['A128KW', 'A128GCM']); $jwe = $jweBuilder ->create() @@ -127,11 +122,8 @@ public function a128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValuesBis() $loaded_flattened_json = $this->getJWESerializerManager() ->unserialize($this->getJWESerializerManager()->serialize('jwe_json_flattened', $jwe, 0)); - static::assertTrue($jweDecrypter->decryptUsingKey($loaded_flattened_json, $private_key, 0)); - $loaded_json = $this->getJWESerializerManager() ->unserialize($this->getJWESerializerManager()->serialize('jwe_json_general', $jwe)); - static::assertTrue($jweDecrypter->decryptUsingKey($loaded_json, $private_key, 0)); static::assertSame($protectedHeader, $loaded_flattened_json->getSharedProtectedHeader()); static::assertSame($header, $loaded_flattened_json->getSharedHeader()); @@ -139,7 +131,23 @@ public function a128KWAndA128GCMEncryptionWithSpecificProtectedHeaderValuesBis() static::assertSame($protectedHeader, $loaded_json->getSharedProtectedHeader()); static::assertSame($header, $loaded_json->getSharedHeader()); - static::assertSame($expected_payload, $loaded_flattened_json->getPayload()); - static::assertSame($expected_payload, $loaded_json->getPayload()); + $this->assertDecryptionIsRefused($loaded_flattened_json, $private_key); + $this->assertDecryptionIsRefused($loaded_json, $private_key); + } + + private function assertDecryptionIsRefused(JWE $jwe, JWK $key): void + { + $jweDecrypter = $this->getJWEDecrypterFactory() + ->create(['A128KW', 'A128GCM']); + + try { + $jweDecrypter->decryptUsingKey($jwe, $key, 0); + static::fail('The token should not be decrypted: "alg" is not protected.'); + } catch (InvalidArgumentException $exception) { + static::assertSame( + 'The "alg" parameter must be a non-empty string set in the protected header or in the recipient header.', + $exception->getMessage() + ); + } } }