Skip to content

Commit 4566062

Browse files
authored
feat: add support for ES256 algorithm (#256)
1 parent 78ec50c commit 4566062

File tree

4 files changed

+179
-2
lines changed

4 files changed

+179
-2
lines changed

src/JWT.php

+136-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
namespace Firebase\JWT;
4+
45
use \DomainException;
56
use \InvalidArgumentException;
67
use \UnexpectedValueException;
@@ -21,6 +22,9 @@
2122
*/
2223
class JWT
2324
{
25+
const ASN1_INTEGER = 0x02;
26+
const ASN1_SEQUENCE = 0x10;
27+
const ASN1_BIT_STRING = 0x03;
2428

2529
/**
2630
* When checking nbf, iat or expiration times,
@@ -97,6 +101,11 @@ public static function decode($jwt, $key, array $allowed_algs = array())
97101
if (!in_array($header->alg, $allowed_algs)) {
98102
throw new UnexpectedValueException('Algorithm not allowed');
99103
}
104+
if ($header->alg === 'ES256') {
105+
// OpenSSL expects an ASN.1 DER sequence for ES256 signatures
106+
$sig = self::signatureToDER($sig);
107+
}
108+
100109
if (is_array($key) || $key instanceof \ArrayAccess) {
101110
if (isset($header->kid)) {
102111
if (!isset($key[$header->kid])) {
@@ -192,7 +201,7 @@ public static function sign($msg, $key, $alg = 'HS256')
192201
throw new DomainException('Algorithm not supported');
193202
}
194203
list($function, $algorithm) = static::$supported_algs[$alg];
195-
switch($function) {
204+
switch ($function) {
196205
case 'hash_hmac':
197206
return hash_hmac($algorithm, $msg, $key, true);
198207
case 'openssl':
@@ -201,6 +210,9 @@ public static function sign($msg, $key, $alg = 'HS256')
201210
if (!$success) {
202211
throw new DomainException("OpenSSL unable to sign data");
203212
} else {
213+
if ($alg === 'ES256') {
214+
$signature = self::signatureFromDER($signature, 256);
215+
}
204216
return $signature;
205217
}
206218
}
@@ -226,7 +238,7 @@ private static function verify($msg, $signature, $key, $alg)
226238
}
227239

228240
list($function, $algorithm) = static::$supported_algs[$alg];
229-
switch($function) {
241+
switch ($function) {
230242
case 'openssl':
231243
$success = openssl_verify($msg, $signature, $key, $algorithm);
232244
if ($success === 1) {
@@ -377,4 +389,126 @@ private static function safeStrlen($str)
377389
}
378390
return strlen($str);
379391
}
392+
393+
/**
394+
* Convert an ECDSA signature to an ASN.1 DER sequence
395+
*
396+
* @param string $sig The ECDSA signature to convert
397+
* @return string The encoded DER object
398+
*/
399+
private static function signatureToDER($sig)
400+
{
401+
// Separate the signature into r-value and s-value
402+
list($r, $s) = str_split($sig, (int) (strlen($sig) / 2));
403+
404+
// Trim leading zeros
405+
$r = ltrim($r, "\x00");
406+
$s = ltrim($s, "\x00");
407+
408+
// Convert r-value and s-value from unsigned big-endian integers to
409+
// signed two's complement
410+
if (ord($r[0]) > 0x7f) {
411+
$r = "\x00" . $r;
412+
}
413+
if (ord($s[0]) > 0x7f) {
414+
$s = "\x00" . $s;
415+
}
416+
417+
return self::encodeDER(
418+
self::ASN1_SEQUENCE,
419+
self::encodeDER(self::ASN1_INTEGER, $r) .
420+
self::encodeDER(self::ASN1_INTEGER, $s)
421+
);
422+
}
423+
424+
/**
425+
* Encodes a value into a DER object.
426+
*
427+
* @param int $type DER tag
428+
* @param string $value the value to encode
429+
* @return string the encoded object
430+
*/
431+
private static function encodeDER($type, $value)
432+
{
433+
$tag_header = 0;
434+
if ($type === self::ASN1_SEQUENCE) {
435+
$tag_header |= 0x20;
436+
}
437+
438+
// Type
439+
$der = chr($tag_header | $type);
440+
441+
// Length
442+
$der .= chr(strlen($value));
443+
444+
return $der . $value;
445+
}
446+
447+
/**
448+
* Encodes signature from a DER object.
449+
*
450+
* @param string $der binary signature in DER format
451+
* @param int $keySize the nubmer of bits in the key
452+
* @return string the signature
453+
*/
454+
private static function signatureFromDER($der, $keySize)
455+
{
456+
// OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
457+
list($offset, $_) = self::readDER($der);
458+
list($offset, $r) = self::readDER($der, $offset);
459+
list($offset, $s) = self::readDER($der, $offset);
460+
461+
// Convert r-value and s-value from signed two's compliment to unsigned
462+
// big-endian integers
463+
$r = ltrim($r, "\x00");
464+
$s = ltrim($s, "\x00");
465+
466+
// Pad out r and s so that they are $keySize bits long
467+
$r = str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
468+
$s = str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
469+
470+
return $r . $s;
471+
}
472+
473+
/**
474+
* Reads binary DER-encoded data and decodes into a single object
475+
*
476+
* @param string $der the binary data in DER format
477+
* @param int $offset the offset of the data stream containing the object
478+
* to decode
479+
* @return array [$offset, $data] the new offset and the decoded object
480+
*/
481+
private static function readDER($der, $offset = 0)
482+
{
483+
$pos = $offset;
484+
$size = strlen($der);
485+
$constructed = (ord($der[$pos]) >> 5) & 0x01;
486+
$type = ord($der[$pos++]) & 0x1f;
487+
488+
// Length
489+
$len = ord($der[$pos++]);
490+
if ($len & 0x80) {
491+
$n = $len & 0x1f;
492+
$len = 0;
493+
while ($n-- && $pos < $size) {
494+
$len = ($len << 8) | ord($der[$pos++]);
495+
}
496+
}
497+
498+
// Value
499+
if ($type == self::ASN1_BIT_STRING) {
500+
$pos++; // Skip the first contents octet (padding indicator)
501+
$data = substr($der, $pos, $len - 1);
502+
if (!$ignore_bit_strings) {
503+
$pos += $len - 1;
504+
}
505+
} elseif (!$constructed) {
506+
$data = substr($der, $pos, $len);
507+
$pos += $len;
508+
} else {
509+
$data = null;
510+
}
511+
512+
return array($pos, $data);
513+
}
380514
}

tests/JWTTest.php

+16
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,22 @@ public function testVerifyError()
282282
self::$opensslVerifyReturnValue = -1;
283283
JWT::decode($msg, $pkey, array('RS256'));
284284
}
285+
286+
/**
287+
* @runInSeparateProcess
288+
*/
289+
public function testEncodeAndDecodeEcdsaToken()
290+
{
291+
$privateKey = file_get_contents(__DIR__ . '/ecdsa-private.pem');
292+
$payload = array('foo' => 'bar');
293+
$encoded = JWT::encode($payload, $privateKey, 'ES256');
294+
295+
// Verify decoding succeeds
296+
$publicKey = file_get_contents(__DIR__ . '/ecdsa-public.pem');
297+
$decoded = JWT::decode($encoded, $publicKey, array('ES256'));
298+
299+
$this->assertEquals('bar', $decoded->foo);
300+
}
285301
}
286302

287303
/*

tests/ecdsa-private.pem

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-----BEGIN EC PARAMETERS-----
2+
MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////
3+
/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6
4+
k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+
5+
kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK
6+
fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz
7+
ucrC/GMlUQIBAQ==
8+
-----END EC PARAMETERS-----
9+
-----BEGIN EC PRIVATE KEY-----
10+
MIIBaAIBAQQgyP9e7yS1tjpXa0l6o+80dbSxuMcqx3lUg0n2OT9AmiuggfowgfcC
11+
AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
12+
MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
13+
vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE
14+
axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W
15+
K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8
16+
YyVRAgEBoUQDQgAE2klp6aX6y5kAir3EWQt0QAeapTW+db/9fD65KAoDzVajtThx
17+
PVLEf1CufcfTxMQAQPM3wkZhu0NjlWFetcMdcQ==
18+
-----END EC PRIVATE KEY-----

tests/ecdsa-public.pem

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA
3+
AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA////
4+
///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd
5+
NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5
6+
RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA
7+
//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABNpJaeml+suZAIq9xFkLdEAH
8+
mqU1vnW//Xw+uSgKA81Wo7U4cT1SxH9Qrn3H08TEAEDzN8JGYbtDY5VhXrXDHXE=
9+
-----END PUBLIC KEY-----

0 commit comments

Comments
 (0)