Skip to content

Commit 7e93ca0

Browse files
committed
fix(core): keep the cause of a load or verification failure
The loaders, the verifier, the decrypter and the serializer managers all swallowed the exception explaining why a token could not be used, and the caller was left with a bare "Unable to load and verify the token." with no previous exception. The last error met along the way is now chained as the previous exception. The per-key failures of JWSVerifier and JWEDecrypter, which cannot throw without changing the return semantics, are reported through a callable accepted as an additional argument; it will become part of the signature in 5.0.0. Closes #681
1 parent eadecb7 commit 7e93ca0

8 files changed

Lines changed: 322 additions & 27 deletions

File tree

src/Library/Encryption/JWEDecrypter.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
2020
use Throwable;
2121
use function count;
22+
use function func_num_args;
23+
use function is_callable;
2224
use function is_string;
2325
use function sprintf;
2426
use function strlen;
@@ -80,6 +82,12 @@ public function decryptUsingKey(JWE &$jwe, JWK $jwk, int $recipient, ?JWK $sende
8082
/**
8183
* This method will try to decrypt the given JWE and recipient using a JWKSet.
8284
*
85+
* A key that cannot be used, or that does not decrypt the recipient, does not abort the decryption: the next key of
86+
* the key set is tried and the reason of the failure is otherwise lost. A callable is accepted as an additional
87+
* argument to observe those failures; it is called with every discarded Throwable. That argument is not part of the
88+
* signature yet (it will be in 5.0.0) and is read with func_num_args()/func_get_arg(5), so that classes extending
89+
* this one remain compatible.
90+
*
8391
* @param JWE $jwe A JWE object to decrypt
8492
* @param JWKSet $jwkset The key set used to decrypt the input
8593
* @param JWK $jwk The key used to decrypt the token in case of success
@@ -92,6 +100,10 @@ public function decryptUsingKeySet(
92100
?JWK &$jwk = null,
93101
?JWK $senderKey = null
94102
): bool {
103+
$onError = func_num_args() >= 6 ? func_get_arg(5) : null;
104+
if (! is_callable($onError)) {
105+
$onError = null;
106+
}
95107
if ($jwkset->count() === 0) {
96108
throw new InvalidArgumentException('No key in the key set.');
97109
}
@@ -102,7 +114,7 @@ public function decryptUsingKeySet(
102114
throw new InvalidArgumentException('The JWE does not contain any recipient.');
103115
}
104116

105-
$plaintext = $this->decryptRecipientKey($jwe, $jwkset, $recipient, $jwk, $senderKey);
117+
$plaintext = $this->decryptRecipientKey($jwe, $jwkset, $recipient, $jwk, $senderKey, $onError);
106118
if ($plaintext !== null) {
107119
$jwe = $jwe->withPayload($plaintext);
108120

@@ -120,13 +132,16 @@ public function decryptUsingKeySet(
120132
*
121133
* The shared unprotected header is never a valid source for "alg" and "enc": it is not covered by the
122134
* AAD and, unlike the per-recipient header, nothing requires those parameters to be located there.
135+
*
136+
* @param callable(Throwable): void|null $onError
123137
*/
124138
private function decryptRecipientKey(
125139
JWE $jwe,
126140
JWKSet $jwkset,
127141
int $i,
128142
?JWK &$successJwk = null,
129-
?JWK $senderKey = null
143+
?JWK $senderKey = null,
144+
?callable $onError = null
130145
): ?string {
131146
$recipient = $jwe->getRecipient($i);
132147
$sharedProtectedHeader = $jwe->getSharedProtectedHeader();
@@ -167,8 +182,11 @@ private function decryptRecipientKey(
167182
$successJwk = $recipientKey;
168183

169184
return $payload;
170-
} catch (Throwable) {
171-
// We do nothing, we continue with other keys
185+
} catch (Throwable $throwable) {
186+
if ($onError !== null) {
187+
$onError($throwable);
188+
}
189+
172190
continue;
173191
}
174192
}

src/Library/Encryption/JWELoader.php

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,35 +61,52 @@ public function loadAndDecryptWithKey(string $token, JWK $key, ?int &$recipient)
6161
/**
6262
* This method will try to load and decrypt the given token using a JWKSet. If succeeded, the methods will populate
6363
* the $recipient variable and returns the JWE.
64+
*
65+
* The failure semantics are unchanged, but the last error met along the way - a serialization failure, a rejected
66+
* header or a key that could not decrypt the recipient - is chained as the previous exception, so that the reason
67+
* of the failure remains available to the caller.
6468
*/
6569
public function loadAndDecryptWithKeySet(string $token, JWKSet $keyset, ?int &$recipient): JWE
6670
{
71+
$lastError = null;
6772
try {
6873
$jwe = $this->serializerManager->unserialize($token);
6974
$nbRecipients = $jwe->countRecipients();
7075
for ($i = 0; $i < $nbRecipients; ++$i) {
71-
if ($this->processRecipient($jwe, $keyset, $i)) {
76+
if ($this->processRecipient($jwe, $keyset, $i, $lastError)) {
7277
$recipient = $i;
7378

7479
return $jwe;
7580
}
7681
}
77-
} catch (Throwable) {
78-
// Nothing to do. Exception thrown just after
82+
} catch (Throwable $throwable) {
83+
$lastError = $throwable;
7984
}
8085

81-
throw new RuntimeException('Unable to load and decrypt the token.');
86+
throw new RuntimeException('Unable to load and decrypt the token.', 0, $lastError);
8287
}
8388

84-
private function processRecipient(JWE &$jwe, JWKSet $keyset, int $recipient): bool
89+
private function processRecipient(JWE &$jwe, JWKSet $keyset, int $recipient, ?Throwable &$lastError): bool
8590
{
8691
try {
8792
if ($this->headerCheckerManager !== null) {
8893
$this->headerCheckerManager->check($jwe, $recipient);
8994
}
95+
$jwk = null;
96+
97+
return $this->jweDecrypter->decryptUsingKeySet(
98+
$jwe,
99+
$keyset,
100+
$recipient,
101+
$jwk,
102+
null,
103+
static function (Throwable $throwable) use (&$lastError): void {
104+
$lastError = $throwable;
105+
}
106+
);
107+
} catch (Throwable $throwable) {
108+
$lastError = $throwable;
90109

91-
return $this->jweDecrypter->decryptUsingKeySet($jwe, $keyset, $recipient);
92-
} catch (Throwable) {
93110
return false;
94111
}
95112
}

src/Library/Encryption/Serializer/JWESerializerManager.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,29 @@ public function serialize(string $name, JWE $jws, ?int $recipientIndex = null):
5050
/**
5151
* Loads data and return a JWE object. Throws an exception if none of the serializer was able to convert the input.
5252
*
53+
* When no serializer is able to convert the input, the exception thrown by the last one is chained as the previous
54+
* exception, so that the actual reason of the failure remains available to the caller.
55+
*
5356
* @param string $input A string that represents a JWE
5457
* @param string|null $name the name of the serializer if the input is unserialized
5558
*/
5659
public function unserialize(string $input, ?string &$name = null): JWE
5760
{
61+
$lastError = null;
5862
foreach ($this->serializers as $serializer) {
5963
try {
6064
$jws = $serializer->unserialize($input);
6165
$name = $serializer->name();
6266

6367
return $jws;
64-
} catch (InvalidArgumentException) {
68+
} catch (InvalidArgumentException $invalidArgumentException) {
69+
$lastError = $invalidArgumentException;
70+
6571
continue;
6672
}
6773
}
6874

69-
throw new InvalidArgumentException('Unsupported input.');
75+
throw new InvalidArgumentException('Unsupported input.', 0, $lastError);
7076
}
7177

7278
/**

src/Library/Signature/JWSLoader.php

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,39 +61,61 @@ public function loadAndVerifyWithKey(string $token, JWK $key, ?int &$signature,
6161
/**
6262
* This method will try to load and verify the token using the given key set. It returns a JWS and will populate the
6363
* $signature variable in case of success, otherwise an exception is thrown.
64+
*
65+
* The failure semantics are unchanged, but the last error met along the way - a serialization failure, a rejected
66+
* header or a key that could not verify the signature - is chained as the previous exception, so that the reason of
67+
* the failure remains available to the caller.
6468
*/
6569
public function loadAndVerifyWithKeySet(
6670
string $token,
6771
JWKSet $keyset,
6872
?int &$signature,
6973
?string $payload = null
7074
): JWS {
75+
$lastError = null;
7176
try {
7277
$jws = $this->serializerManager->unserialize($token);
7378
$nbSignatures = $jws->countSignatures();
7479
for ($i = 0; $i < $nbSignatures; ++$i) {
75-
if ($this->processSignature($jws, $keyset, $i, $payload)) {
80+
if ($this->processSignature($jws, $keyset, $i, $payload, $lastError)) {
7681
$signature = $i;
7782

7883
return $jws;
7984
}
8085
}
81-
} catch (Throwable) {
82-
// Nothing to do. Exception thrown just after
86+
} catch (Throwable $throwable) {
87+
$lastError = $throwable;
8388
}
8489

85-
throw new Exception('Unable to load and verify the token.');
90+
throw new Exception('Unable to load and verify the token.', 0, $lastError);
8691
}
8792

88-
private function processSignature(JWS $jws, JWKSet $keyset, int $signature, ?string $payload): bool
89-
{
93+
private function processSignature(
94+
JWS $jws,
95+
JWKSet $keyset,
96+
int $signature,
97+
?string $payload,
98+
?Throwable &$lastError
99+
): bool {
90100
try {
91101
if ($this->headerCheckerManager !== null) {
92102
$this->headerCheckerManager->check($jws, $signature);
93103
}
104+
$jwk = null;
105+
106+
return $this->jwsVerifier->verifyWithKeySet(
107+
$jws,
108+
$keyset,
109+
$signature,
110+
$payload,
111+
$jwk,
112+
static function (Throwable $throwable) use (&$lastError): void {
113+
$lastError = $throwable;
114+
}
115+
);
116+
} catch (Throwable $throwable) {
117+
$lastError = $throwable;
94118

95-
return $this->jwsVerifier->verifyWithKeySet($jws, $keyset, $signature, $payload);
96-
} catch (Throwable) {
97119
return false;
98120
}
99121
}

src/Library/Signature/JWSVerifier.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Jose\Component\Signature\Algorithm\MacAlgorithm;
1515
use Jose\Component\Signature\Algorithm\SignatureAlgorithm;
1616
use Throwable;
17+
use function func_num_args;
18+
use function is_callable;
1719
use function sprintf;
1820

1921
class JWSVerifier
@@ -48,6 +50,12 @@ public function verifyWithKey(JWS $jws, JWK $jwk, int $signature, ?string $detac
4850
* This method will try to verify the JWS object using the given key set and for the given signature. It returns
4951
* true if the signature is verified, otherwise false.
5052
*
53+
* A key that cannot be used, or that does not verify the signature, does not abort the verification: the next key
54+
* of the key set is tried and the reason of the failure is otherwise lost. A callable is accepted as an additional
55+
* argument to observe those failures; it is called with every discarded Throwable. That argument is not part of the
56+
* signature yet (it will be in 5.0.0) and is read with func_num_args()/func_get_arg(5), so that classes extending
57+
* this one remain compatible.
58+
*
5159
* @param JWS $jws A JWS object
5260
* @param JWKSet $jwkset The signature will be verified using keys in the key set
5361
* @param JWK $jwk The key used to verify the signature in case of success
@@ -62,6 +70,10 @@ public function verifyWithKeySet(
6270
?string $detachedPayload = null,
6371
?JWK &$jwk = null
6472
): bool {
73+
$onError = func_num_args() >= 6 ? func_get_arg(5) : null;
74+
if (! is_callable($onError)) {
75+
$onError = null;
76+
}
6577
if ($jwkset->count() === 0) {
6678
throw new InvalidArgumentException('There is no key in the key set.');
6779
}
@@ -71,15 +83,19 @@ public function verifyWithKeySet(
7183
$this->checkPayload($jws, $detachedPayload);
7284
$signature = $jws->getSignature($signatureIndex);
7385

74-
return $this->verifySignature($jws, $jwkset, $signature, $detachedPayload, $jwk);
86+
return $this->verifySignature($jws, $jwkset, $signature, $detachedPayload, $jwk, $onError);
7587
}
7688

89+
/**
90+
* @param callable(Throwable): void|null $onError
91+
*/
7792
private function verifySignature(
7893
JWS $jws,
7994
JWKSet $jwkset,
8095
Signature $signature,
8196
?string $detachedPayload = null,
82-
?JWK &$successJwk = null
97+
?JWK &$successJwk = null,
98+
?callable $onError = null
8399
): bool {
84100
$input = $this->getInputToVerify($jws, $signature, $detachedPayload);
85101
$algorithm = $this->getAlgorithm($signature);
@@ -92,8 +108,11 @@ private function verifySignature(
92108

93109
return true;
94110
}
95-
} catch (Throwable) {
96-
// We do nothing, we continue with other keys
111+
} catch (Throwable $throwable) {
112+
if ($onError !== null) {
113+
$onError($throwable);
114+
}
115+
97116
continue;
98117
}
99118
}

src/Library/Signature/Serializer/JWSSerializerManager.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,29 @@ public function serialize(string $name, JWS $jws, ?int $signatureIndex = null):
4848
/**
4949
* Loads data and return a JWS object.
5050
*
51+
* When no serializer is able to convert the input, the exception thrown by the last one is chained as the previous
52+
* exception, so that the actual reason of the failure remains available to the caller.
53+
*
5154
* @param string $input A string that represents a JWS
5255
* @param string|null $name the name of the serializer if the input is unserialized
5356
*/
5457
public function unserialize(string $input, ?string &$name = null): JWS
5558
{
59+
$lastError = null;
5660
foreach ($this->serializers as $serializer) {
5761
try {
5862
$jws = $serializer->unserialize($input);
5963
$name = $serializer->name();
6064

6165
return $jws;
62-
} catch (InvalidArgumentException) {
66+
} catch (InvalidArgumentException $invalidArgumentException) {
67+
$lastError = $invalidArgumentException;
68+
6369
continue;
6470
}
6571
}
6672

67-
throw new InvalidArgumentException('Unsupported input.');
73+
throw new InvalidArgumentException('Unsupported input.', 0, $lastError);
6874
}
6975

7076
private function add(JWSSerializer $serializer): void

0 commit comments

Comments
 (0)