-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
JWSVerifier.php
163 lines (141 loc) · 5.98 KB
/
JWSVerifier.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
<?php
declare(strict_types=1);
namespace Jose\Component\Signature;
use InvalidArgumentException;
use Jose\Component\Core\Algorithm;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\KeyChecker;
use Jose\Component\Signature\Algorithm\MacAlgorithm;
use Jose\Component\Signature\Algorithm\SignatureAlgorithm;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Throwable;
class JWSVerifier
{
public function __construct(
private readonly AlgorithmManager $signatureAlgorithmManager
) {
}
/**
* Returns the algorithm manager associated to the JWSVerifier.
*/
public function getSignatureAlgorithmManager(): AlgorithmManager
{
return $this->signatureAlgorithmManager;
}
/**
* This method will try to verify the JWS object using the given key and for the given signature. It returns true if
* the signature is verified, otherwise false.
*
* @return bool true if the verification of the signature succeeded, else false
*/
public function verifyWithKey(JWS $jws, JWK $jwk, int $signature, ?string $detachedPayload = null): bool
{
$jwkset = new JWKSet([$jwk]);
return $this->verifyWithKeySet($jws, $jwkset, $signature, $detachedPayload);
}
/**
* This method will try to verify the JWS object using the given key set and for the given signature. It returns
* true if the signature is verified, otherwise false.
*
* @param JWS $jws A JWS object
* @param JWKSet $jwkset The signature will be verified using keys in the key set
* @param JWK $jwk The key used to verify the signature in case of success
* @param string|null $detachedPayload If not null, the value must be the detached payload encoded in Base64 URL safe. If the input contains a payload, throws an exception.
*
* @return bool true if the verification of the signature succeeded, else false
*/
public function verifyWithKeySet(
JWS $jws,
JWKSet $jwkset,
int $signatureIndex,
?string $detachedPayload = null,
JWK &$jwk = null
): bool {
if ($jwkset->count() === 0) {
throw new InvalidArgumentException('There is no key in the key set.');
}
if ($jws->countSignatures() === 0) {
throw new InvalidArgumentException('The JWS does not contain any signature.');
}
$this->checkPayload($jws, $detachedPayload);
$signature = $jws->getSignature($signatureIndex);
return $this->verifySignature($jws, $jwkset, $signature, $detachedPayload, $jwk);
}
private function verifySignature(
JWS $jws,
JWKSet $jwkset,
Signature $signature,
?string $detachedPayload = null,
JWK &$successJwk = null
): bool {
$input = $this->getInputToVerify($jws, $signature, $detachedPayload);
$algorithm = $this->getAlgorithm($signature);
foreach ($jwkset->all() as $jwk) {
try {
KeyChecker::checkKeyUsage($jwk, 'verification');
KeyChecker::checkKeyAlgorithm($jwk, $algorithm->name());
if ($algorithm->verify($jwk, $input, $signature->getSignature()) === true) {
$successJwk = $jwk;
return true;
}
} catch (Throwable) {
//We do nothing, we continue with other keys
continue;
}
}
return false;
}
private function getInputToVerify(JWS $jws, Signature $signature, ?string $detachedPayload): string
{
$payload = $jws->getPayload();
$isPayloadEmpty = $payload === null || $payload === '';
$encodedProtectedHeader = $signature->getEncodedProtectedHeader() ?? '';
$isPayloadBase64Encoded = ! $signature->hasProtectedHeaderParameter(
'b64'
) || $signature->getProtectedHeaderParameter('b64') === true;
$encodedPayload = $jws->getEncodedPayload();
if ($isPayloadBase64Encoded && $encodedPayload !== null) {
return sprintf('%s.%s', $encodedProtectedHeader, $encodedPayload);
}
$callable = $isPayloadBase64Encoded === true ? static fn (?string $p): string => Base64UrlSafe::encodeUnpadded(
$p ?? ''
)
: static fn (?string $p): string => $p ?? '';
$payloadToUse = $callable($isPayloadEmpty ? $detachedPayload : $payload);
return sprintf('%s.%s', $encodedProtectedHeader, $payloadToUse);
}
private function checkPayload(JWS $jws, ?string $detachedPayload = null): void
{
$isPayloadEmpty = $this->isPayloadEmpty($jws->getPayload());
if ($detachedPayload !== null && ! $isPayloadEmpty) {
throw new InvalidArgumentException('A detached payload is set, but the JWS already has a payload.');
}
if ($isPayloadEmpty && $detachedPayload === null) {
throw new InvalidArgumentException('The JWS has a detached payload, but no payload is provided.');
}
}
/**
* @return MacAlgorithm|SignatureAlgorithm
*/
private function getAlgorithm(Signature $signature): Algorithm
{
$completeHeader = array_merge($signature->getProtectedHeader(), $signature->getHeader());
if (! isset($completeHeader['alg'])) {
throw new InvalidArgumentException('No "alg" parameter set in the header.');
}
$algorithm = $this->signatureAlgorithmManager->get($completeHeader['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']
));
}
return $algorithm;
}
private function isPayloadEmpty(?string $payload): bool
{
return $payload === null || $payload === '';
}
}