Skip to content

RFC: Add support for ES256 algorithm #256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 137 additions & 3 deletions src/JWT.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

namespace Firebase\JWT;

use \DomainException;
use \InvalidArgumentException;
use \UnexpectedValueException;
Expand All @@ -21,6 +22,9 @@
*/
class JWT
{
const ASN1_INTEGER = 0x02;
const ASN1_SEQUENCE = 0x10;
const ASN1_BIT_STRING = 0x03;

/**
* When checking nbf, iat or expiration times,
Expand Down Expand Up @@ -97,6 +101,11 @@ public static function decode($jwt, $key, array $allowed_algs = array())
if (!in_array($header->alg, $allowed_algs)) {
throw new UnexpectedValueException('Algorithm not allowed');
}
if ($header->alg === 'ES256') {
// OpenSSL expects an ASN.1 DER sequence for ES256 signatures
$sig = self::signatureToDER($sig);
}

if (is_array($key) || $key instanceof \ArrayAccess) {
if (isset($header->kid)) {
if (!isset($key[$header->kid])) {
Expand Down Expand Up @@ -160,7 +169,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he
if ($keyId !== null) {
$header['kid'] = $keyId;
}
if ( isset($head) && is_array($head) ) {
if (isset($head) && is_array($head)) {
$header = array_merge($head, $header);
}
$segments = array();
Expand Down Expand Up @@ -192,7 +201,7 @@ public static function sign($msg, $key, $alg = 'HS256')
throw new DomainException('Algorithm not supported');
}
list($function, $algorithm) = static::$supported_algs[$alg];
switch($function) {
switch ($function) {
case 'hash_hmac':
return hash_hmac($algorithm, $msg, $key, true);
case 'openssl':
Expand All @@ -201,6 +210,9 @@ public static function sign($msg, $key, $alg = 'HS256')
if (!$success) {
throw new DomainException("OpenSSL unable to sign data");
} else {
if ($alg === 'ES256') {
$signature = self::signatureFromDER($signature, 256);
}
return $signature;
}
}
Expand All @@ -226,7 +238,7 @@ private static function verify($msg, $signature, $key, $alg)
}

list($function, $algorithm) = static::$supported_algs[$alg];
switch($function) {
switch ($function) {
case 'openssl':
$success = openssl_verify($msg, $signature, $key, $algorithm);
if ($success === 1) {
Expand Down Expand Up @@ -377,4 +389,126 @@ private static function safeStrlen($str)
}
return strlen($str);
}

/**
* Convert an ECDSA signature to an ASN.1 DER sequence
*
* @param string $sig The ECDSA signature to convert
* @return string The encoded DER object
*/
private static function signatureToDER($sig)
{
// Separate the signature into r-value and s-value
list($r, $s) = str_split($sig, (int) (strlen($sig) / 2));

// Trim leading zeros
$r = ltrim($r, "\x00");
$s = ltrim($s, "\x00");

// Convert r-value and s-value from unsigned big-endian integers to
// signed two's complement
if (ord($r[0]) > 0x7f) {
$r = "\x00" . $r;
}
if (ord($s[0]) > 0x7f) {
$s = "\x00" . $s;
}

return self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(self::ASN1_INTEGER, $r) .
self::encodeDER(self::ASN1_INTEGER, $s)
);
}

/**
* Encodes a value into a DER object.
*
* @param int $type DER tag
* @param string $value the value to encode
* @return string the encoded object
*/
private static function encodeDER($type, $value)
{
$tag_header = 0;
if ($type === self::ASN1_SEQUENCE) {
$tag_header |= 0x20;
}

// Type
$der = chr($tag_header | $type);

// Length
$der .= chr(strlen($value));

return $der . $value;
}

/**
* Encodes signature from a DER object.
*
* @param string $der binary signature in DER format
* @param int $keySize the nubmer of bits in the key
* @return string the signature
*/
private static function signatureFromDER($der, $keySize)
{
// OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
list($offset, $_) = self::readDER($der);
list($offset, $r) = self::readDER($der, $offset);
list($offset, $s) = self::readDER($der, $offset);

// Convert r-value and s-value from signed two's compliment to unsigned
// big-endian integers
$r = ltrim($r, "\x00");
$s = ltrim($s, "\x00");

// Pad out r and s so that they are $keySize bits long
$r = str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
$s = str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);

return $r . $s;
}

/**
* Reads binary DER-encoded data and decodes into a single object
*
* @param string $der the binary data in DER format
* @param int $offset the offset of the data stream containing the object
* to decode
* @return array [$offset, $data] the new offset and the decoded object
*/
private static function readDER($der, $offset = 0)
{
$pos = $offset;
$size = strlen($der);
$constructed = (ord($der[$pos]) >> 5) & 0x01;
$type = ord($der[$pos++]) & 0x1f;

// Length
$len = ord($der[$pos++]);
if ($len & 0x80) {
$n = $len & 0x1f;
$len = 0;
while ($n-- && $pos < $size) {
$len = ($len << 8) | ord($der[$pos++]);
}
}

// Value
if ($type == self::ASN1_BIT_STRING) {
$pos++; // Skip the first contents octet (padding indicator)
$data = substr($der, $pos, $len - 1);
if (!$ignore_bit_strings) {
$pos += $len - 1;
}
} elseif (!$constructed) {
$data = substr($der, $pos, $len);
$pos += $len;
} else {
$data = null;
}

return array($pos, $data);
}
}
16 changes: 16 additions & 0 deletions tests/JWTTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,22 @@ public function testVerifyError()
self::$opensslVerifyReturnValue = -1;
JWT::decode($msg, $pkey, array('RS256'));
}

/**
* @runInSeparateProcess
*/
public function testEncodeAndDecodeEcdsaToken()
{
$privateKey = file_get_contents(__DIR__ . '/ecdsa-private.pem');
$payload = array('foo' => 'bar');
$encoded = JWT::encode($payload, $privateKey, 'ES256');

// Verify decoding succeeds
$publicKey = file_get_contents(__DIR__ . '/ecdsa-public.pem');
$decoded = JWT::decode($encoded, $publicKey, array('ES256'));

$this->assertEquals('bar', $decoded->foo);
}
}

/*
Expand Down
18 changes: 18 additions & 0 deletions tests/ecdsa-private.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-----BEGIN EC PARAMETERS-----
MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////
/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6
k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+
kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK
fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz
ucrC/GMlUQIBAQ==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIIBaAIBAQQgyP9e7yS1tjpXa0l6o+80dbSxuMcqx3lUg0n2OT9AmiuggfowgfcC
AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE
axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W
K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8
YyVRAgEBoUQDQgAE2klp6aX6y5kAir3EWQt0QAeapTW+db/9fD65KAoDzVajtThx
PVLEf1CufcfTxMQAQPM3wkZhu0NjlWFetcMdcQ==
-----END EC PRIVATE KEY-----
9 changes: 9 additions & 0 deletions tests/ecdsa-public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA
AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA////
///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd
NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5
RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA
//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABNpJaeml+suZAIq9xFkLdEAH
mqU1vnW//Xw+uSgKA81Wo7U4cT1SxH9Qrn3H08TEAEDzN8JGYbtDY5VhXrXDHXE=
-----END PUBLIC KEY-----