diff --git a/src/Experimental/KeyEncryption/Chacha20Poly1305.php b/src/Experimental/KeyEncryption/Chacha20Poly1305.php index 5e9126a3..6345609a 100644 --- a/src/Experimental/KeyEncryption/Chacha20Poly1305.php +++ b/src/Experimental/KeyEncryption/Chacha20Poly1305.php @@ -47,15 +47,15 @@ public function encryptKey(JWK $key, string $cek, array $completeHeader, array & $k = $this->getKey($key); $nonce = random_bytes(12); - // We set header parameters - $additionalHeader['nonce'] = Base64UrlSafe::encodeUnpadded($nonce); - $tag = null; $result = openssl_encrypt($cek, 'chacha20-poly1305', $k, OPENSSL_RAW_DATA, $nonce, $tag); - if ($result === false || ! is_string($tag)) { + if ($result === false || ! is_string($tag) || strlen($tag) !== 16) { throw new RuntimeException('Unable to encrypt the CEK'); } + $additionalHeader['nonce'] = Base64UrlSafe::encodeUnpadded($nonce); + $additionalHeader['tag'] = Base64UrlSafe::encodeUnpadded($tag); + return $result; } @@ -72,8 +72,14 @@ public function decryptKey(JWK $key, string $encrypted_cek, array $header): stri if (strlen($nonce) !== 12) { throw new InvalidArgumentException('The header parameter "nonce" is not valid.'); } + isset($header['tag']) || throw new InvalidArgumentException('The header parameter "tag" is missing.'); + is_string($header['tag']) || throw new InvalidArgumentException('The header parameter "tag" is not valid.'); + $tag = Base64UrlSafe::decodeNoPadding($header['tag']); + if (strlen($tag) !== 16) { + throw new InvalidArgumentException('The header parameter "tag" is not valid.'); + } - $result = openssl_decrypt($encrypted_cek, 'chacha20-poly1305', $k, OPENSSL_RAW_DATA, $nonce); + $result = openssl_decrypt($encrypted_cek, 'chacha20-poly1305', $k, OPENSSL_RAW_DATA, $nonce, $tag); if ($result === false) { throw new RuntimeException('Unable to decrypt the CEK'); } diff --git a/src/Library/Encryption/Algorithm/KeyEncryption/PBES2AESKW.php b/src/Library/Encryption/Algorithm/KeyEncryption/PBES2AESKW.php index eb9c0ad3..1faaac91 100644 --- a/src/Library/Encryption/Algorithm/KeyEncryption/PBES2AESKW.php +++ b/src/Library/Encryption/Algorithm/KeyEncryption/PBES2AESKW.php @@ -16,12 +16,16 @@ use function in_array; use function is_int; use function is_string; +use function sprintf; abstract readonly class PBES2AESKW implements KeyWrapping { + public const DEFAULT_MAX_COUNT = 1_000_000; + public function __construct( private readonly int $salt_size = 64, - private readonly int $nb_count = 4096 + private readonly int $nb_count = 4096, + private readonly int $max_count = self::DEFAULT_MAX_COUNT ) { if (! interface_exists(WrapperInterface::class)) { throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms'); @@ -139,6 +143,12 @@ protected function checkHeaderAdditionalParameters(array $header): void if (! is_int($header['p2c']) || $header['p2c'] <= 0) { throw new InvalidArgumentException('The header parameter "p2c" is not valid.'); } + if ($header['p2c'] > $this->max_count) { + throw new InvalidArgumentException(sprintf( + 'The header parameter "p2c" is too large. The maximum allowed value is %d.', + $this->max_count + )); + } } abstract protected function getWrapper(): A256KW|A128KW|A192KW; diff --git a/src/Library/Encryption/Algorithm/KeyEncryption/RSA15.php b/src/Library/Encryption/Algorithm/KeyEncryption/RSA15.php index 8e7ad55c..5f65b2b7 100644 --- a/src/Library/Encryption/Algorithm/KeyEncryption/RSA15.php +++ b/src/Library/Encryption/Algorithm/KeyEncryption/RSA15.php @@ -4,17 +4,54 @@ namespace Jose\Component\Encryption\Algorithm\KeyEncryption; +use InvalidArgumentException; +use Jose\Component\Core\JWK; +use Jose\Component\Core\Util\RSAKey; use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\RSACrypt; use Override; +use function is_string; final readonly class RSA15 extends RSA { + /** + * @var array + */ + private const CEK_LENGTHS = [ + 'A128GCM' => 16, + 'A192GCM' => 24, + 'A256GCM' => 32, + 'A128CBC-HS256' => 32, + 'A192CBC-HS384' => 48, + 'A256CBC-HS512' => 64, + ]; + #[Override] public function name(): string { return 'RSA1_5'; } + /** + * @param array $header + */ + #[Override] + public function decryptKey(JWK $key, string $encrypted_cek, array $header): string + { + $this->checkKey($key); + if (! $key->has('d')) { + throw new InvalidArgumentException('The key is not a private key'); + } + $priv = RSAKey::createFromJWK($key); + + return RSACrypt::decrypt( + $priv, + $encrypted_cek, + RSACrypt::ENCRYPTION_PKCS1, + null, + $this->getExpectedCekLength($header) + ); + } + #[Override] protected function getEncryptionMode(): int { @@ -26,4 +63,17 @@ protected function getHashAlgorithm(): ?string { return null; } + + /** + * @param array $header + */ + private function getExpectedCekLength(array $header): ?int + { + $enc = $header['enc'] ?? null; + if (! is_string($enc)) { + return null; + } + + return self::CEK_LENGTHS[$enc] ?? null; + } } diff --git a/src/Library/Encryption/Algorithm/KeyEncryption/Util/RSACrypt.php b/src/Library/Encryption/Algorithm/KeyEncryption/Util/RSACrypt.php index b36d1738..453630e3 100644 --- a/src/Library/Encryption/Algorithm/KeyEncryption/Util/RSACrypt.php +++ b/src/Library/Encryption/Algorithm/KeyEncryption/Util/RSACrypt.php @@ -47,8 +47,13 @@ public static function encrypt(RSAKey $key, string $data, int $mode, ?string $ha } } - public static function decrypt(RSAKey $key, string $plaintext, int $mode, ?string $hash = null): string - { + public static function decrypt( + RSAKey $key, + string $plaintext, + int $mode, + ?string $hash = null, + ?int $expectedKeyLength = null + ): string { switch ($mode) { case self::ENCRYPTION_OAEP: if ($hash === null) { @@ -57,7 +62,7 @@ public static function decrypt(RSAKey $key, string $plaintext, int $mode, ?strin return self::decryptWithRSAOAEP($key, $plaintext, $hash); case self::ENCRYPTION_PKCS1: - return self::decryptWithRSA15($key, $plaintext); + return self::decryptWithRSA15($key, $plaintext, $expectedKeyLength); default: throw new InvalidArgumentException('Unsupported mode.'); } @@ -86,7 +91,7 @@ public static function encryptWithRSA15(RSAKey $key, string $data): string return self::convertIntegerToOctetString($c, $key->getModulusLength()); } - public static function decryptWithRSA15(RSAKey $key, string $c): string + public static function decryptWithRSA15(RSAKey $key, string $c, ?int $expectedKeyLength = null): string { if (strlen($c) !== $key->getModulusLength()) { throw new InvalidArgumentException('Unable to decrypt'); @@ -94,16 +99,71 @@ public static function decryptWithRSA15(RSAKey $key, string $c): string $c = BigInteger::createFromBinaryString($c); $m = self::getRSADP($key, $c); $em = self::convertIntegerToOctetString($m, $key->getModulusLength()); - if (ord($em[0]) !== 0 || ord($em[1]) > 2) { - throw new InvalidArgumentException('Unable to decrypt'); + if ($expectedKeyLength === null) { + if (ord($em[0]) !== 0 || ord($em[1]) > 2) { + throw new InvalidArgumentException('Unable to decrypt'); + } + $ps = substr($em, 2, (int) strpos($em, chr(0), 2) - 2); + $m = substr($em, strlen($ps) + 3); + if (strlen($ps) < 8) { + throw new InvalidArgumentException('Unable to decrypt'); + } + + return $m; } - $ps = substr($em, 2, (int) strpos($em, chr(0), 2) - 2); - $m = substr($em, strlen($ps) + 3); - if (strlen($ps) < 8) { - throw new InvalidArgumentException('Unable to decrypt'); + + return self::extractRSA15KeyOrRandom($em, $expectedKeyLength); + } + + private static function extractRSA15KeyOrRandom(string $em, int $expectedKeyLength): string + { + $k = strlen($em); + $random = random_bytes($expectedKeyLength); + + if ($k < $expectedKeyLength + 11) { + return $random; + } + $candidate = substr($em, $k - $expectedKeyLength); + + $valid = self::ctEq(ord($em[0]), 0x00) & self::ctEq(ord($em[1]), 0x02); + + $seenSeparator = 0; + $separatorIndex = 0; + $psLength = 0; + for ($i = 2; $i < $k; ++$i) { + $isZero = self::ctEq(ord($em[$i]), 0x00); + $firstZero = $isZero & (1 - $seenSeparator); + $separatorIndex |= $firstZero * $i; + $psLength += (1 - $seenSeparator) & (1 - $isZero); + $seenSeparator |= $isZero; } - return $m; + $valid &= $seenSeparator; + $valid &= self::ctGe($psLength, 8); + + $messageLength = $k - $separatorIndex - 1; + $valid &= self::ctEq($messageLength, $expectedKeyLength); + + return self::ctSelect($valid, $candidate, $random); + } + + private static function ctEq(int $a, int $b): int + { + $diff = $a ^ $b; + + return (($diff - 1) >> 63) & 1; + } + + private static function ctGe(int $a, int $b): int + { + return (($b - $a - 1) >> 63) & 1; + } + + private static function ctSelect(int $condition, string $a, string $b): string + { + $mask = str_repeat(chr(($condition * 0xFF) & 0xFF), strlen($a)); + + return ($a & $mask) | ($b & ~$mask); } /** diff --git a/src/Library/Signature/JWSVerifier.php b/src/Library/Signature/JWSVerifier.php index 6be08e72..97349e61 100644 --- a/src/Library/Signature/JWSVerifier.php +++ b/src/Library/Signature/JWSVerifier.php @@ -141,16 +141,17 @@ private function checkPayload(JWS $jws, ?string $detachedPayload = null): void */ private function getAlgorithm(Signature $signature): Algorithm { - $completeHeader = [...$signature->getProtectedHeader(), ...$signature->getHeader()]; - if (! isset($completeHeader['alg'])) { - throw new InvalidArgumentException('No "alg" parameter set in the header.'); + $protectedHeader = $signature->getProtectedHeader(); + if (! isset($protectedHeader['alg'])) { + throw new InvalidArgumentException('No "alg" parameter set in the protected header.'); } + $alg = $protectedHeader['alg']; - $algorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']); + $algorithm = $this->signatureAlgorithmManager->get($alg); if (! $algorithm instanceof SignatureAlgorithm && ! $algorithm instanceof MacAlgorithm) { throw new InvalidArgumentException(sprintf( 'The algorithm "%s" is not supported or is not a signature or MAC algorithm.', - $completeHeader['alg'] + $alg )); } diff --git a/tests/Component/Encryption/RSA15ImplicitRejectionTest.php b/tests/Component/Encryption/RSA15ImplicitRejectionTest.php new file mode 100644 index 00000000..99951e04 --- /dev/null +++ b/tests/Component/Encryption/RSA15ImplicitRejectionTest.php @@ -0,0 +1,102 @@ + 'RSA1_5', + 'use' => 'enc', + ]); + $algorithm = new RSA15(); + $cek = random_bytes(16); // A128GCM CEK + $header = [ + 'alg' => 'RSA1_5', + 'enc' => 'A128GCM', + ]; + + $additionalHeader = []; + $encrypted = $algorithm->encryptKey($jwk, $cek, $header, $additionalHeader); + $decrypted = $algorithm->decryptKey($jwk, $encrypted, $header); + + static::assertSame($cek, $decrypted); + } + + #[Test] + public function malformedCiphertextDoesNotThrowAndReturnsExpectedLength(): void + { + $jwk = JWKFactory::createRSAKey(2048, [ + 'alg' => 'RSA1_5', + 'use' => 'enc', + ]); + $algorithm = new RSA15(); + $key = RSAKey::createFromJWK($jwk); + $garbage = "\x00" . random_bytes($key->getModulusLength() - 1); + $header = [ + 'alg' => 'RSA1_5', + 'enc' => 'A128GCM', + ]; + + $result = $algorithm->decryptKey($jwk, $garbage, $header); + + // 16 bytes for A128GCM, and (overwhelmingly) not a valid recovery. + static::assertSame(16, mb_strlen($result, '8bit')); + } + + #[Test] + public function implicitRejectionRespectsEncCekLength(): void + { + $jwk = JWKFactory::createRSAKey(2048, [ + 'alg' => 'RSA1_5', + 'use' => 'enc', + ]); + $algorithm = new RSA15(); + $key = RSAKey::createFromJWK($jwk); + $garbage = "\x00" . random_bytes($key->getModulusLength() - 1); + + $lengths = [ + 'A128GCM' => 16, + 'A192GCM' => 24, + 'A256GCM' => 32, + 'A128CBC-HS256' => 32, + 'A192CBC-HS384' => 48, + 'A256CBC-HS512' => 64, + ]; + foreach ($lengths as $enc => $expected) { + $result = $algorithm->decryptKey($jwk, $garbage, ['alg' => 'RSA1_5', 'enc' => $enc]); + static::assertSame($expected, mb_strlen($result, '8bit'), $enc); + } + } + + #[Test] + public function legacyDirectDecryptStillThrowsOnGarbage(): void + { + $jwk = JWKFactory::createRSAKey(2048, [ + 'alg' => 'RSA1_5', + 'use' => 'enc', + ]); + $key = RSAKey::createFromJWK($jwk); + $garbage = "\x00" . random_bytes($key->getModulusLength() - 1); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to decrypt'); + RSACrypt::decrypt($key, $garbage, RSACrypt::ENCRYPTION_PKCS1); + } +} diff --git a/tests/Component/Signature/AlgorithmConfusionTest.php b/tests/Component/Signature/AlgorithmConfusionTest.php new file mode 100644 index 00000000..9ffdf475 --- /dev/null +++ b/tests/Component/Signature/AlgorithmConfusionTest.php @@ -0,0 +1,89 @@ +key(); + $algorithmManager = new AlgorithmManager([new HS256(), new HS512()]); + $jws = (new JWSBuilder($algorithmManager)) + ->create() + ->withPayload('payload') + ->addSignature($key, ['alg' => 'HS256']) + ->build(); + + $verifier = new JWSVerifier($algorithmManager); + static::assertTrue($verifier->verifyWithKey($jws, $key, 0)); + } + + #[Test] + public function unprotectedAlgDoesNotOverrideProtected(): void + { + $key = $this->key(); + $algorithmManager = new AlgorithmManager([new HS256(), new HS512()]); + + $jws = (new JWSBuilder($algorithmManager)) + ->create() + ->withPayload('payload') + ->addSignature($key, ['alg' => 'HS256']) + ->build(); + $serializer = new JSONFlattenedSerializer(); + $data = json_decode($serializer->serialize($jws, 0), true); + $data['header'] = ['alg' => 'HS512']; + $tamperedJws = $serializer->unserialize(json_encode($data)); + + $verifier = new JWSVerifier($algorithmManager); + + static::assertTrue($verifier->verifyWithKey($tamperedJws, $key, 0)); + } + + #[Test] + public function algOnlyInUnprotectedHeaderIsRejected(): void + { + $key = $this->key(); + $algorithmManager = new AlgorithmManager([new HS256(), new HS512()]); + + $jws = (new JWSBuilder($algorithmManager)) + ->create() + ->withPayload('payload') + ->addSignature($key, ['alg' => 'HS256']) + ->build(); + $serializer = new JSONFlattenedSerializer(); + $data = json_decode($serializer->serialize($jws, 0), true); + unset($data['protected']); + $data['header'] = ['alg' => 'HS256']; + $tamperedJws = $serializer->unserialize(json_encode($data)); + + $verifier = new JWSVerifier($algorithmManager); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No "alg" parameter set in the protected header.'); + $verifier->verifyWithKey($tamperedJws, $key, 0); + } + + private function key(): JWK + { + return new JWK([ + 'kty' => 'oct', + 'k' => 'dKaQUMb3qDMRdfg6qLqsGgG-aWdh6cd1F6tXrXddzpc', + ]); + } +} diff --git a/tests/Component/Signature/RFC7520/MultipleSignaturesTest.php b/tests/Component/Signature/RFC7520/MultipleSignaturesTest.php index 9f3e5f1d..cd7e3f40 100644 --- a/tests/Component/Signature/RFC7520/MultipleSignaturesTest.php +++ b/tests/Component/Signature/RFC7520/MultipleSignaturesTest.php @@ -4,7 +4,10 @@ namespace Jose\Tests\Component\Signature\RFC7520; +use InvalidArgumentException; use Jose\Component\Core\JWK; +use Jose\Component\Signature\JWS; +use Jose\Component\Signature\JWSVerifier; use Jose\Tests\Component\Signature\SignatureTestCase; use PHPUnit\Framework\Attributes\Test; @@ -81,9 +84,9 @@ public function multipleSignatures(): void static::assertSame(3, $jws->countSignatures()); - static::assertTrue($jwsVerifier->verifyWithKey($jws, $ecdsa_private_key, 0)); static::assertTrue($jwsVerifier->verifyWithKey($jws, $rsa_private_key, 1)); static::assertTrue($jwsVerifier->verifyWithKey($jws, $symmetric_key, 2)); + $this->assertUnprotectedAlgIsRejected($jwsVerifier, $jws, $ecdsa_private_key, 0); /** @see https://tools.ietf.org/html/rfc7520#section-4.8.5 */ $expected_json = '{"payload":"SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4","signatures":[{"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"bilbo.baggins@hobbiton.example"},"signature":"MIsjqtVlOpa71KE-Mss8_Nq2YH4FGhiocsqrgi5NvyG53uoimic1tcMdSg-qptrzZc7CG6Svw2Y13TDIqHzTUrL_lR2ZFcryNFiHkSw129EghGpwkpxaTn_THJTCglNbADko1MZBCdwzJxwqZc-1RlpO2HibUYyXSwO97BSe0_evZKdjvvKSgsIqjytKSeAMbhMBdMma622_BG5t4sdbuCHtFjp9iJmkio47AIwqkZV1aIZsv33uPUqBBCXbYoQJwt7mxPftHmNlGoOSMxR_3thmXTCm4US-xiNOyhbm8afKK64jU6_TPtQHiJeQJxz9G3Tx-083B745_AfYOnlC9w"},{"header":{"alg":"ES512","kid":"bilbo.baggins@hobbiton.example"},"signature":"ARcVLnaJJaUWG8fG-8t5BREVAuTY8n8YHjwDO1muhcdCoFZFFjfISu0Cdkn9Ybdlmi54ho0x924DUz8sK7ZXkhc7AFM8ObLfTvNCrqcI3Jkl2U5IX3utNhODH6v7xgy1Qahsn0fyb4zSAkje8bAWz4vIfj5pCMYxxm4fgV3q7ZYhm5eD"},{"protected":"eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LWVlZjMxNGJjNzAzNyJ9","signature":"s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0"}]}'; @@ -94,7 +97,21 @@ public function multipleSignatures(): void static::assertSame($payload, $loaded_json->getPayload()); static::assertTrue($jwsVerifier->verifyWithKey($loaded_json, $rsa_private_key, 0)); - static::assertTrue($jwsVerifier->verifyWithKey($loaded_json, $ecdsa_private_key, 1)); static::assertTrue($jwsVerifier->verifyWithKey($loaded_json, $symmetric_key, 2)); + $this->assertUnprotectedAlgIsRejected($jwsVerifier, $loaded_json, $ecdsa_private_key, 1); + } + + private function assertUnprotectedAlgIsRejected( + JWSVerifier $jwsVerifier, + JWS $jws, + JWK $key, + int $signature + ): void { + try { + $jwsVerifier->verifyWithKey($jws, $key, $signature); + static::fail('A signature whose "alg" is not in the protected header must be rejected.'); + } catch (InvalidArgumentException $e) { + static::assertSame('No "alg" parameter set in the protected header.', $e->getMessage()); + } } } diff --git a/tests/Component/Signature/SignerTest.php b/tests/Component/Signature/SignerTest.php index e24e7a38..34a0a318 100644 --- a/tests/Component/Signature/SignerTest.php +++ b/tests/Component/Signature/SignerTest.php @@ -930,7 +930,7 @@ public function flattenedJSONWithUnencodedDetachedPayload(): void public function signAndLoadWithoutAlgParameterInTheHeader(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No "alg" parameter set in the header.'); + $this->expectExceptionMessage('No "alg" parameter set in the protected header.'); $payload = "It\xe2\x80\x99s a dangerous business, Frodo, going out your door. You step onto the road, and if you don't keep your feet, there\xe2\x80\x99s no knowing where you might be swept off to."; $jws = 'eyJraWQiOiJiaWxiby5iYWdnaW5zQGhvYmJpdG9uLmV4YW1wbGUifQ.SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4.MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmKZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4JIwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_fcIe8u9ipH84ogoree7vjbU5y18kDquDg'; diff --git a/tests/EncryptionAlgorithm/Experimental/Chacha20Poly1305KeyEncryptionTest.php b/tests/EncryptionAlgorithm/Experimental/Chacha20Poly1305KeyEncryptionTest.php new file mode 100644 index 00000000..c8691f53 --- /dev/null +++ b/tests/EncryptionAlgorithm/Experimental/Chacha20Poly1305KeyEncryptionTest.php @@ -0,0 +1,104 @@ +key(); + $cek = random_bytes(32); + $algorithm = new Chacha20Poly1305(); + + $additionalHeader = []; + $encrypted = $algorithm->encryptKey($key, $cek, [], $additionalHeader); + + static::assertArrayHasKey('nonce', $additionalHeader); + static::assertArrayHasKey('tag', $additionalHeader); + + $decrypted = $algorithm->decryptKey($key, $encrypted, $additionalHeader); + static::assertSame($cek, $decrypted); + } + + #[Test] + public function tamperedCiphertextIsRejected(): void + { + $key = $this->key(); + $cek = random_bytes(32); + $algorithm = new Chacha20Poly1305(); + + $additionalHeader = []; + $encrypted = $algorithm->encryptKey($key, $cek, [], $additionalHeader); + + $encrypted[0] = $encrypted[0] ^ "\xff"; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unable to decrypt the CEK'); + $algorithm->decryptKey($key, $encrypted, $additionalHeader); + } + + #[Test] + public function tamperedTagIsRejected(): void + { + $key = $this->key(); + $cek = random_bytes(32); + $algorithm = new Chacha20Poly1305(); + + $additionalHeader = []; + $encrypted = $algorithm->encryptKey($key, $cek, [], $additionalHeader); + + $tag = Base64UrlSafe::decodeNoPadding($additionalHeader['tag']); + $tag[0] = $tag[0] ^ "\xff"; + $additionalHeader['tag'] = Base64UrlSafe::encodeUnpadded($tag); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unable to decrypt the CEK'); + $algorithm->decryptKey($key, $encrypted, $additionalHeader); + } + + #[Test] + public function missingTagIsRejected(): void + { + $key = $this->key(); + $cek = random_bytes(32); + $algorithm = new Chacha20Poly1305(); + + $additionalHeader = []; + $encrypted = $algorithm->encryptKey($key, $cek, [], $additionalHeader); + unset($additionalHeader['tag']); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The header parameter "tag" is missing.'); + $algorithm->decryptKey($key, $encrypted, $additionalHeader); + } + + private function key(): JWK + { + return new JWK([ + 'kty' => 'oct', + 'k' => Base64UrlSafe::encodeUnpadded(random_bytes(32)), + ]); + } +} diff --git a/tests/SignatureAlgorithm/HMAC/HMACFromRFC7520Test.php b/tests/SignatureAlgorithm/HMAC/HMACFromRFC7520Test.php index 1c010166..071593f7 100644 --- a/tests/SignatureAlgorithm/HMAC/HMACFromRFC7520Test.php +++ b/tests/SignatureAlgorithm/HMAC/HMACFromRFC7520Test.php @@ -4,6 +4,7 @@ namespace Jose\Tests\SignatureAlgorithm\HMAC; +use InvalidArgumentException; use Jose\Component\Core\AlgorithmManager; use Jose\Component\Core\JWK; use Jose\Component\Signature\Algorithm\HS256; @@ -302,9 +303,10 @@ public function hS256WithoutProtectedHeader(): void ); $loaded_flattened_json = $jsonFlattenedSerializer->unserialize($expected_flattened_json); - static::assertTrue($jwsVerifier->verifyWithKey($loaded_flattened_json, $key, 0)); - $loaded_json = $jsonGeneralSerializer->unserialize($expected_json); - static::assertTrue($jwsVerifier->verifyWithKey($loaded_json, $key, 0)); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No "alg" parameter set in the protected header.'); + $jwsVerifier->verifyWithKey($loaded_flattened_json, $key, 0); } }