Skip to content

Commit

Permalink
feat: add support for ES256 algorithm (#256)
Browse files Browse the repository at this point in the history
  • Loading branch information
bshaffer committed Feb 24, 2020
1 parent 78ec50c commit 4566062
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 2 deletions.
138 changes: 136 additions & 2 deletions src/JWT.php
@@ -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 @@ -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
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
@@ -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
@@ -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-----

0 comments on commit 4566062

Please sign in to comment.