Total Complexity | 91 |
Total Lines | 638 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like JWT often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use JWT, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
28 | class JWT |
||
29 | { |
||
30 | private const ASN1_INTEGER = 0x02; |
||
31 | private const ASN1_SEQUENCE = 0x10; |
||
32 | private const ASN1_BIT_STRING = 0x03; |
||
33 | |||
34 | /** |
||
35 | * When checking nbf, iat or expiration times, |
||
36 | * we want to provide some extra leeway time to |
||
37 | * account for clock skew. |
||
38 | * |
||
39 | * @var int |
||
40 | */ |
||
41 | public static $leeway = 0; |
||
42 | |||
43 | /** |
||
44 | * Allow the current timestamp to be specified. |
||
45 | * Useful for fixing a value within unit testing. |
||
46 | * Will default to PHP time() value if null. |
||
47 | * |
||
48 | * @var ?int |
||
49 | */ |
||
50 | public static $timestamp = null; |
||
51 | |||
52 | /** |
||
53 | * @var array<string, string[]> |
||
54 | */ |
||
55 | public static $supported_algs = [ |
||
56 | 'ES384' => ['openssl', 'SHA384'], |
||
57 | 'ES256' => ['openssl', 'SHA256'], |
||
58 | 'ES256K' => ['openssl', 'SHA256'], |
||
59 | 'HS256' => ['hash_hmac', 'SHA256'], |
||
60 | 'HS384' => ['hash_hmac', 'SHA384'], |
||
61 | 'HS512' => ['hash_hmac', 'SHA512'], |
||
62 | 'RS256' => ['openssl', 'SHA256'], |
||
63 | 'RS384' => ['openssl', 'SHA384'], |
||
64 | 'RS512' => ['openssl', 'SHA512'], |
||
65 | 'EdDSA' => ['sodium_crypto', 'EdDSA'], |
||
66 | ]; |
||
67 | |||
68 | /** |
||
69 | * Decodes a JWT string into a PHP object. |
||
70 | * |
||
71 | * @param string $jwt The JWT |
||
72 | * @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray The Key or associative array of key IDs |
||
73 | * (kid) to Key objects. |
||
74 | * If the algorithm used is asymmetric, this is |
||
75 | * the public key. |
||
76 | * Each Key object contains an algorithm and |
||
77 | * matching key. |
||
78 | * Supported algorithms are 'ES384','ES256', |
||
79 | * 'HS256', 'HS384', 'HS512', 'RS256', 'RS384' |
||
80 | * and 'RS512'. |
||
81 | * @param stdClass $headers Optional. Populates stdClass with headers. |
||
82 | * |
||
83 | * @return stdClass The JWT's payload as a PHP object |
||
84 | * |
||
85 | * @throws InvalidArgumentException Provided key/key-array was empty or malformed |
||
86 | * @throws DomainException Provided JWT is malformed |
||
87 | * @throws UnexpectedValueException Provided JWT was invalid |
||
88 | * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed |
||
89 | * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' |
||
90 | * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' |
||
91 | * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim |
||
92 | * |
||
93 | * @uses jsonDecode |
||
94 | * @uses urlsafeB64Decode |
||
95 | */ |
||
96 | public static function decode( |
||
97 | string $jwt, |
||
98 | $keyOrKeyArray, |
||
99 | ?stdClass &$headers = null |
||
100 | ): stdClass { |
||
101 | // Validate JWT |
||
102 | $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; |
||
103 | |||
104 | if (empty($keyOrKeyArray)) { |
||
105 | throw new InvalidArgumentException('Key may not be empty'); |
||
106 | } |
||
107 | $tks = \explode('.', $jwt); |
||
108 | if (\count($tks) !== 3) { |
||
109 | throw new UnexpectedValueException('Wrong number of segments'); |
||
110 | } |
||
111 | list($headb64, $bodyb64, $cryptob64) = $tks; |
||
112 | $headerRaw = static::urlsafeB64Decode($headb64); |
||
113 | if (null === ($header = static::jsonDecode($headerRaw))) { |
||
114 | throw new UnexpectedValueException('Invalid header encoding'); |
||
115 | } |
||
116 | if ($headers !== null) { |
||
117 | $headers = $header; |
||
118 | } |
||
119 | $payloadRaw = static::urlsafeB64Decode($bodyb64); |
||
120 | if (null === ($payload = static::jsonDecode($payloadRaw))) { |
||
121 | throw new UnexpectedValueException('Invalid claims encoding'); |
||
122 | } |
||
123 | if (\is_array($payload)) { |
||
124 | // prevent PHP Fatal Error in edge-cases when payload is empty array |
||
125 | $payload = (object) $payload; |
||
126 | } |
||
127 | if (!$payload instanceof stdClass) { |
||
128 | throw new UnexpectedValueException('Payload must be a JSON object'); |
||
129 | } |
||
130 | $sig = static::urlsafeB64Decode($cryptob64); |
||
131 | if (empty($header->alg)) { |
||
132 | throw new UnexpectedValueException('Empty algorithm'); |
||
133 | } |
||
134 | if (empty(static::$supported_algs[$header->alg])) { |
||
135 | throw new UnexpectedValueException('Algorithm not supported'); |
||
136 | } |
||
137 | |||
138 | $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null); |
||
139 | |||
140 | // Check the algorithm |
||
141 | if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { |
||
142 | // See issue #351 |
||
143 | throw new UnexpectedValueException('Incorrect key for this algorithm'); |
||
144 | } |
||
145 | if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) { |
||
146 | // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures |
||
147 | $sig = self::signatureToDER($sig); |
||
148 | } |
||
149 | if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { |
||
150 | throw new SignatureInvalidException('Signature verification failed'); |
||
151 | } |
||
152 | |||
153 | // Check the nbf if it is defined. This is the time that the |
||
154 | // token can actually be used. If it's not yet that time, abort. |
||
155 | if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) { |
||
156 | $ex = new BeforeValidException( |
||
157 | 'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf) |
||
158 | ); |
||
159 | $ex->setPayload($payload); |
||
160 | throw $ex; |
||
161 | } |
||
162 | |||
163 | // Check that this token has been created before 'now'. This prevents |
||
164 | // using tokens that have been created for later use (and haven't |
||
165 | // correctly used the nbf claim). |
||
166 | if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) { |
||
167 | $ex = new BeforeValidException( |
||
168 | 'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) $payload->iat) |
||
169 | ); |
||
170 | $ex->setPayload($payload); |
||
171 | throw $ex; |
||
172 | } |
||
173 | |||
174 | // Check if this token has expired. |
||
175 | if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { |
||
176 | $ex = new ExpiredException('Expired token'); |
||
177 | $ex->setPayload($payload); |
||
178 | throw $ex; |
||
179 | } |
||
180 | |||
181 | return $payload; |
||
182 | } |
||
183 | |||
184 | /** |
||
185 | * Converts and signs a PHP array into a JWT string. |
||
186 | * |
||
187 | * @param array<mixed> $payload PHP array |
||
188 | * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. |
||
189 | * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', |
||
190 | * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' |
||
191 | * @param string $keyId |
||
192 | * @param array<string, string> $head An array with header elements to attach |
||
193 | * |
||
194 | * @return string A signed JWT |
||
195 | * |
||
196 | * @uses jsonEncode |
||
197 | * @uses urlsafeB64Encode |
||
198 | */ |
||
199 | public static function encode( |
||
200 | array $payload, |
||
201 | $key, |
||
202 | string $alg, |
||
203 | ?string $keyId = null, |
||
204 | ?array $head = null |
||
205 | ): string { |
||
206 | $header = ['typ' => 'JWT']; |
||
207 | if (isset($head)) { |
||
208 | $header = \array_merge($header, $head); |
||
209 | } |
||
210 | $header['alg'] = $alg; |
||
211 | if ($keyId !== null) { |
||
212 | $header['kid'] = $keyId; |
||
213 | } |
||
214 | $segments = []; |
||
215 | $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); |
||
216 | $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); |
||
217 | $signing_input = \implode('.', $segments); |
||
218 | |||
219 | $signature = static::sign($signing_input, $key, $alg); |
||
220 | $segments[] = static::urlsafeB64Encode($signature); |
||
221 | |||
222 | return \implode('.', $segments); |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * Sign a string with a given key and algorithm. |
||
227 | * |
||
228 | * @param string $msg The message to sign |
||
229 | * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. |
||
230 | * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256', |
||
231 | * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' |
||
232 | * |
||
233 | * @return string An encrypted message |
||
234 | * |
||
235 | * @throws DomainException Unsupported algorithm or bad key was specified |
||
236 | */ |
||
237 | public static function sign( |
||
238 | string $msg, |
||
239 | $key, |
||
240 | string $alg |
||
241 | ): string { |
||
242 | if (empty(static::$supported_algs[$alg])) { |
||
243 | throw new DomainException('Algorithm not supported'); |
||
244 | } |
||
245 | list($function, $algorithm) = static::$supported_algs[$alg]; |
||
246 | switch ($function) { |
||
247 | case 'hash_hmac': |
||
248 | if (!\is_string($key)) { |
||
249 | throw new InvalidArgumentException('key must be a string when using hmac'); |
||
250 | } |
||
251 | return \hash_hmac($algorithm, $msg, $key, true); |
||
252 | case 'openssl': |
||
253 | $signature = ''; |
||
254 | if (!\is_resource($key) && !openssl_pkey_get_private($key)) { |
||
255 | throw new DomainException('OpenSSL unable to validate key'); |
||
256 | } |
||
257 | $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line |
||
258 | if (!$success) { |
||
259 | throw new DomainException('OpenSSL unable to sign data'); |
||
260 | } |
||
261 | if ($alg === 'ES256' || $alg === 'ES256K') { |
||
262 | $signature = self::signatureFromDER($signature, 256); |
||
263 | } elseif ($alg === 'ES384') { |
||
264 | $signature = self::signatureFromDER($signature, 384); |
||
265 | } |
||
266 | return $signature; |
||
267 | case 'sodium_crypto': |
||
268 | if (!\function_exists('sodium_crypto_sign_detached')) { |
||
269 | throw new DomainException('libsodium is not available'); |
||
270 | } |
||
271 | if (!\is_string($key)) { |
||
272 | throw new InvalidArgumentException('key must be a string when using EdDSA'); |
||
273 | } |
||
274 | try { |
||
275 | // The last non-empty line is used as the key. |
||
276 | $lines = array_filter(explode("\n", $key)); |
||
277 | $key = base64_decode((string) end($lines)); |
||
278 | if (\strlen($key) === 0) { |
||
279 | throw new DomainException('Key cannot be empty string'); |
||
280 | } |
||
281 | return sodium_crypto_sign_detached($msg, $key); |
||
282 | } catch (Exception $e) { |
||
283 | throw new DomainException($e->getMessage(), 0, $e); |
||
284 | } |
||
285 | } |
||
286 | |||
287 | throw new DomainException('Algorithm not supported'); |
||
288 | } |
||
289 | |||
290 | /** |
||
291 | * Verify a signature with the message, key and method. Not all methods |
||
292 | * are symmetric, so we must have a separate verify and sign method. |
||
293 | * |
||
294 | * @param string $msg The original message (header and body) |
||
295 | * @param string $signature The original signature |
||
296 | * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey |
||
297 | * @param string $alg The algorithm |
||
298 | * |
||
299 | * @return bool |
||
300 | * |
||
301 | * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure |
||
302 | */ |
||
303 | private static function verify( |
||
304 | string $msg, |
||
305 | string $signature, |
||
306 | $keyMaterial, |
||
307 | string $alg |
||
308 | ): bool { |
||
309 | if (empty(static::$supported_algs[$alg])) { |
||
310 | throw new DomainException('Algorithm not supported'); |
||
311 | } |
||
312 | |||
313 | list($function, $algorithm) = static::$supported_algs[$alg]; |
||
314 | switch ($function) { |
||
315 | case 'openssl': |
||
316 | $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line |
||
317 | if ($success === 1) { |
||
318 | return true; |
||
319 | } |
||
320 | if ($success === 0) { |
||
321 | return false; |
||
322 | } |
||
323 | // returns 1 on success, 0 on failure, -1 on error. |
||
324 | throw new DomainException( |
||
325 | 'OpenSSL error: ' . \openssl_error_string() |
||
326 | ); |
||
327 | case 'sodium_crypto': |
||
328 | if (!\function_exists('sodium_crypto_sign_verify_detached')) { |
||
329 | throw new DomainException('libsodium is not available'); |
||
330 | } |
||
331 | if (!\is_string($keyMaterial)) { |
||
332 | throw new InvalidArgumentException('key must be a string when using EdDSA'); |
||
333 | } |
||
334 | try { |
||
335 | // The last non-empty line is used as the key. |
||
336 | $lines = array_filter(explode("\n", $keyMaterial)); |
||
337 | $key = base64_decode((string) end($lines)); |
||
338 | if (\strlen($key) === 0) { |
||
339 | throw new DomainException('Key cannot be empty string'); |
||
340 | } |
||
341 | if (\strlen($signature) === 0) { |
||
342 | throw new DomainException('Signature cannot be empty string'); |
||
343 | } |
||
344 | return sodium_crypto_sign_verify_detached($signature, $msg, $key); |
||
345 | } catch (Exception $e) { |
||
346 | throw new DomainException($e->getMessage(), 0, $e); |
||
347 | } |
||
348 | case 'hash_hmac': |
||
349 | default: |
||
350 | if (!\is_string($keyMaterial)) { |
||
351 | throw new InvalidArgumentException('key must be a string when using hmac'); |
||
352 | } |
||
353 | $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); |
||
354 | return self::constantTimeEquals($hash, $signature); |
||
355 | } |
||
356 | } |
||
357 | |||
358 | /** |
||
359 | * Decode a JSON string into a PHP object. |
||
360 | * |
||
361 | * @param string $input JSON string |
||
362 | * |
||
363 | * @return mixed The decoded JSON string |
||
364 | * |
||
365 | * @throws DomainException Provided string was invalid JSON |
||
366 | */ |
||
367 | public static function jsonDecode(string $input) |
||
368 | { |
||
369 | $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); |
||
370 | |||
371 | if ($errno = \json_last_error()) { |
||
372 | self::handleJsonError($errno); |
||
373 | } elseif ($obj === null && $input !== 'null') { |
||
374 | throw new DomainException('Null result with non-null input'); |
||
375 | } |
||
376 | return $obj; |
||
377 | } |
||
378 | |||
379 | /** |
||
380 | * Encode a PHP array into a JSON string. |
||
381 | * |
||
382 | * @param array<mixed> $input A PHP array |
||
383 | * |
||
384 | * @return string JSON representation of the PHP array |
||
385 | * |
||
386 | * @throws DomainException Provided object could not be encoded to valid JSON |
||
387 | */ |
||
388 | public static function jsonEncode(array $input): string |
||
389 | { |
||
390 | $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); |
||
391 | if ($errno = \json_last_error()) { |
||
392 | self::handleJsonError($errno); |
||
393 | } elseif ($json === 'null') { |
||
394 | throw new DomainException('Null result with non-null input'); |
||
395 | } |
||
396 | if ($json === false) { |
||
397 | throw new DomainException('Provided object could not be encoded to valid JSON'); |
||
398 | } |
||
399 | return $json; |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * Decode a string with URL-safe Base64. |
||
404 | * |
||
405 | * @param string $input A Base64 encoded string |
||
406 | * |
||
407 | * @return string A decoded string |
||
408 | * |
||
409 | * @throws InvalidArgumentException invalid base64 characters |
||
410 | */ |
||
411 | public static function urlsafeB64Decode(string $input): string |
||
412 | { |
||
413 | return \base64_decode(self::convertBase64UrlToBase64($input)); |
||
414 | } |
||
415 | |||
416 | /** |
||
417 | * Convert a string in the base64url (URL-safe Base64) encoding to standard base64. |
||
418 | * |
||
419 | * @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding) |
||
420 | * |
||
421 | * @return string A Base64 encoded string with standard characters (+/) and padding (=), when |
||
422 | * needed. |
||
423 | * |
||
424 | * @see https://www.rfc-editor.org/rfc/rfc4648 |
||
425 | */ |
||
426 | public static function convertBase64UrlToBase64(string $input): string |
||
427 | { |
||
428 | $remainder = \strlen($input) % 4; |
||
429 | if ($remainder) { |
||
430 | $padlen = 4 - $remainder; |
||
431 | $input .= \str_repeat('=', $padlen); |
||
432 | } |
||
433 | return \strtr($input, '-_', '+/'); |
||
434 | } |
||
435 | |||
436 | /** |
||
437 | * Encode a string with URL-safe Base64. |
||
438 | * |
||
439 | * @param string $input The string you want encoded |
||
440 | * |
||
441 | * @return string The base64 encode of what you passed in |
||
442 | */ |
||
443 | public static function urlsafeB64Encode(string $input): string |
||
444 | { |
||
445 | return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); |
||
446 | } |
||
447 | |||
448 | |||
449 | /** |
||
450 | * Determine if an algorithm has been provided for each Key |
||
451 | * |
||
452 | * @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray |
||
453 | * @param string|null $kid |
||
454 | * |
||
455 | * @throws UnexpectedValueException |
||
456 | * |
||
457 | * @return Key |
||
458 | */ |
||
459 | private static function getKey( |
||
460 | $keyOrKeyArray, |
||
461 | ?string $kid |
||
462 | ): Key { |
||
463 | if ($keyOrKeyArray instanceof Key) { |
||
464 | return $keyOrKeyArray; |
||
465 | } |
||
466 | |||
467 | if (empty($kid) && $kid !== '0') { |
||
468 | throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); |
||
469 | } |
||
470 | |||
471 | if ($keyOrKeyArray instanceof CachedKeySet) { |
||
472 | // Skip "isset" check, as this will automatically refresh if not set |
||
473 | return $keyOrKeyArray[$kid]; |
||
474 | } |
||
475 | |||
476 | if (!isset($keyOrKeyArray[$kid])) { |
||
477 | throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); |
||
478 | } |
||
479 | |||
480 | return $keyOrKeyArray[$kid]; |
||
481 | } |
||
482 | |||
483 | /** |
||
484 | * @param string $left The string of known length to compare against |
||
485 | * @param string $right The user-supplied string |
||
486 | * @return bool |
||
487 | */ |
||
488 | public static function constantTimeEquals(string $left, string $right): bool |
||
489 | { |
||
490 | if (\function_exists('hash_equals')) { |
||
491 | return \hash_equals($left, $right); |
||
492 | } |
||
493 | $len = \min(self::safeStrlen($left), self::safeStrlen($right)); |
||
494 | |||
495 | $status = 0; |
||
496 | for ($i = 0; $i < $len; $i++) { |
||
497 | $status |= (\ord($left[$i]) ^ \ord($right[$i])); |
||
498 | } |
||
499 | $status |= (self::safeStrlen($left) ^ self::safeStrlen($right)); |
||
500 | |||
501 | return ($status === 0); |
||
502 | } |
||
503 | |||
504 | /** |
||
505 | * Helper method to create a JSON error. |
||
506 | * |
||
507 | * @param int $errno An error number from json_last_error() |
||
508 | * |
||
509 | * @throws DomainException |
||
510 | * |
||
511 | * @return void |
||
512 | */ |
||
513 | private static function handleJsonError(int $errno): void |
||
514 | { |
||
515 | $messages = [ |
||
516 | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', |
||
517 | JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', |
||
518 | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', |
||
519 | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', |
||
520 | JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 |
||
521 | ]; |
||
522 | throw new DomainException( |
||
523 | isset($messages[$errno]) |
||
524 | ? $messages[$errno] |
||
525 | : 'Unknown JSON error: ' . $errno |
||
526 | ); |
||
527 | } |
||
528 | |||
529 | /** |
||
530 | * Get the number of bytes in cryptographic strings. |
||
531 | * |
||
532 | * @param string $str |
||
533 | * |
||
534 | * @return int |
||
535 | */ |
||
536 | private static function safeStrlen(string $str): int |
||
542 | } |
||
543 | |||
544 | /** |
||
545 | * Convert an ECDSA signature to an ASN.1 DER sequence |
||
546 | * |
||
547 | * @param string $sig The ECDSA signature to convert |
||
548 | * @return string The encoded DER object |
||
549 | */ |
||
550 | private static function signatureToDER(string $sig): string |
||
551 | { |
||
552 | // Separate the signature into r-value and s-value |
||
553 | $length = max(1, (int) (\strlen($sig) / 2)); |
||
554 | list($r, $s) = \str_split($sig, $length); |
||
555 | |||
556 | // Trim leading zeros |
||
557 | $r = \ltrim($r, "\x00"); |
||
558 | $s = \ltrim($s, "\x00"); |
||
559 | |||
560 | // Convert r-value and s-value from unsigned big-endian integers to |
||
561 | // signed two's complement |
||
562 | if (\ord($r[0]) > 0x7f) { |
||
563 | $r = "\x00" . $r; |
||
564 | } |
||
565 | if (\ord($s[0]) > 0x7f) { |
||
566 | $s = "\x00" . $s; |
||
567 | } |
||
568 | |||
569 | return self::encodeDER( |
||
570 | self::ASN1_SEQUENCE, |
||
571 | self::encodeDER(self::ASN1_INTEGER, $r) . |
||
572 | self::encodeDER(self::ASN1_INTEGER, $s) |
||
573 | ); |
||
574 | } |
||
575 | |||
576 | /** |
||
577 | * Encodes a value into a DER object. |
||
578 | * |
||
579 | * @param int $type DER tag |
||
580 | * @param string $value the value to encode |
||
581 | * |
||
582 | * @return string the encoded object |
||
583 | */ |
||
584 | private static function encodeDER(int $type, string $value): string |
||
598 | } |
||
599 | |||
600 | /** |
||
601 | * Encodes signature from a DER object. |
||
602 | * |
||
603 | * @param string $der binary signature in DER format |
||
604 | * @param int $keySize the number of bits in the key |
||
605 | * |
||
606 | * @return string the signature |
||
607 | */ |
||
608 | private static function signatureFromDER(string $der, int $keySize): string |
||
609 | { |
||
610 | // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE |
||
611 | list($offset, $_) = self::readDER($der); |
||
612 | list($offset, $r) = self::readDER($der, $offset); |
||
613 | list($offset, $s) = self::readDER($der, $offset); |
||
614 | |||
615 | // Convert r-value and s-value from signed two's compliment to unsigned |
||
616 | // big-endian integers |
||
617 | $r = \ltrim($r, "\x00"); |
||
618 | $s = \ltrim($s, "\x00"); |
||
619 | |||
620 | // Pad out r and s so that they are $keySize bits long |
||
621 | $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); |
||
622 | $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); |
||
623 | |||
624 | return $r . $s; |
||
625 | } |
||
626 | |||
627 | /** |
||
628 | * Reads binary DER-encoded data and decodes into a single object |
||
629 | * |
||
630 | * @param string $der the binary data in DER format |
||
631 | * @param int $offset the offset of the data stream containing the object |
||
632 | * to decode |
||
633 | * |
||
634 | * @return array{int, string|null} the new offset and the decoded object |
||
|
|||
635 | */ |
||
636 | private static function readDER(string $der, int $offset = 0): array |
||
666 | } |
||
667 | } |
||
668 |