Skip to content

Commit

Permalink
Merge pull request #207 from /issues/206-rsa-pkcs8
Browse files Browse the repository at this point in the history
Add PKCS#8 parsing code to RSAKey
  • Loading branch information
kelvinmo authored Sep 21, 2024
2 parents cc9ed17 + d6c98ab commit bcd952f
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 56 deletions.
91 changes: 47 additions & 44 deletions src/SimpleJWT/Keys/ECKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,30 +163,7 @@ public function __construct($data, string $format, ?string $password = null, ?st

$seq = $der->decode($binary);

$version = $seq->getChildAt(0)->getValue();
if ($version != 1) throw new KeyException('Invalid private key version: ' . $version);

$d = $seq->getChildAt(1)->getValue();

$curve_oid = $seq->getChildWithTag(0)->getValue();
$curve = self::getCurveNameFromOID($curve_oid);
if ($curve == null) throw new KeyException('Unrecognised EC parameter: ' . $curve_oid);

$len = self::$curves[$curve]['len'];

$point = $seq->getChildWithTag(1)->getValue();
if (strlen($point) != $len + 1) throw new KeyException('Incorrect private key length: ' . strlen($point));

if (ord($point[0]) != 0x04) throw new KeyException('Invalid private key'); // W

$x = substr($point, 1, $len / 2);
$y = substr($point, 1 + $len / 2);

$jwk['kty'] = self::KTY;
$jwk['crv'] = $curve;
$jwk['d'] = Util::base64url_encode($d);
$jwk['x'] = Util::base64url_encode($x);
$jwk['y'] = Util::base64url_encode($y);
$jwk = self::parseASN1PrivateKey($seq);
} elseif (preg_match(Key::PEM_PKCS8_PRIVATE, $data, $matches)) {
/** @var string $binary */
$binary = base64_decode($matches[1]);
Expand All @@ -204,29 +181,10 @@ public function __construct($data, string $format, ?string $password = null, ?st
$curve = self::getCurveNameFromOID($curve_oid);
if ($curve == null) throw new KeyException('Unrecognised EC parameter: ' . $curve_oid);

$len = self::$curves[$curve]['len'];

$private_octet_string = $seq->getChildAt(2)->getValue();
$private_seq = $der->decode($private_octet_string);

$version = $private_seq->getChildAt(0)->getValue();
if ($version != 1) throw new KeyException('Invalid private key version: ' . $version);

$d = $private_seq->getChildAt(1)->getValue();

$point = $private_seq->getChildWithTag(1)->getValue();
if (strlen($point) != $len + 1) throw new KeyException('Incorrect private key length: ' . strlen($point));

if (ord($point[0]) != 0x04) throw new KeyException('Invalid private key'); // W

$x = substr($point, 1, $len / 2);
$y = substr($point, 1 + $len / 2);

$jwk['kty'] = self::KTY;
$jwk['crv'] = $curve;
$jwk['d'] = Util::base64url_encode($d);
$jwk['x'] = Util::base64url_encode($x);
$jwk['y'] = Util::base64url_encode($y);
$jwk = self::parseASN1PrivateKey($private_seq, $curve);
} else {
throw new KeyException('Unrecognised key format');
}
Expand Down Expand Up @@ -395,6 +353,51 @@ protected function getThumbnailMembers(): array {
return ['crv', 'kty', 'x', 'y'];
}

/**
* Parses an EC private key in DER form.
*
* An EC private key is encoded using the ECPrivateKey type as per SEC 1.
*
* @param ASN1Value $seq the ASN.1 sequence to parse
* @param string $curve the name of the elliptic curve. If null, this will be
* read from the sequence
* @return array<string, mixed> the parsed private key data
* @throws KeyException if an error occurs in parsing the key
*/
protected static function parseASN1PrivateKey(ASN1Value $seq, $curve = null): array {
$version = $seq->getChildAt(0)->getValue();
if ($version != 1) throw new KeyException('Invalid private key version: ' . $version);

$d = $seq->getChildAt(1)->getValue();

if ($curve == null) {
$curve_oid_param = $seq->getChildWithTag(0);
if ($curve_oid_param == null) throw new KeyException('Missing EC curve parameter');
$curve_oid = $curve_oid_param->getValue();
$curve = self::getCurveNameFromOID($curve_oid);
if ($curve == null) throw new KeyException('Unrecognised EC parameter: ' . $curve_oid);
}

if (!isset(self::$curves[$curve])) throw new KeyException('Curve not found');
$len = self::$curves[$curve]['len'];

$point = $seq->getChildWithTag(1)->getValue();
if (strlen($point) != $len + 1) throw new KeyException('Incorrect private key length: ' . strlen($point));

if (ord($point[0]) != 0x04) throw new KeyException('Invalid private key'); // W

$x = substr($point, 1, $len / 2);
$y = substr($point, 1 + $len / 2);

return [
'kty' => self::KTY,
'crv' => $curve,
'd' => Util::base64url_encode($d),
'x' => Util::base64url_encode($x),
'y' => Util::base64url_encode($y)
];
}

private static function getCurveNameFromOID(string $curve_oid): ?string {
foreach (self::$curves as $crv => $params) {
if ($params['oid'] == $curve_oid) return $crv;
Expand Down
52 changes: 42 additions & 10 deletions src/SimpleJWT/Keys/RSAKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,24 @@ public function __construct($data, string $format, ?string $password = null, ?st

$seq = $der->decode($binary);

$jwk = self::parseASN1PrivateKey($seq);
} elseif (preg_match(Key::PEM_PKCS8_PRIVATE, $data, $matches)) {
/** @var string $binary */
$binary = base64_decode($matches[1]);
if ($binary == FALSE) throw new KeyException('Cannot read PEM key');

$seq = $der->decode($binary);

$version = $seq->getChildAt(0)->getValue();
if ($version != 0) throw new KeyException('Unsupported RSA private key version');
if ($version != 0) throw new KeyException('Invalid private key version: ' . $version);

$key_oid = $seq->getChildAt(1)->getChildAt(0)->getValue();
if ($key_oid != self::OID) throw new KeyException('Invalid key type: ' . $key_oid);

$jwk['kty'] = self::KTY;
$jwk['n'] = Util::base64url_encode($seq->getChildAt(1)->getValueAsUIntOctets());
$jwk['e'] = Util::base64url_encode($seq->getChildAt(2)->getValueAsUIntOctets());
$jwk['d'] = Util::base64url_encode($seq->getChildAt(3)->getValueAsUIntOctets());
$jwk['p'] = Util::base64url_encode($seq->getChildAt(4)->getValueAsUIntOctets());
$jwk['q'] = Util::base64url_encode($seq->getChildAt(5)->getValueAsUIntOctets());
$jwk['dp'] = Util::base64url_encode($seq->getChildAt(6)->getValueAsUIntOctets());
$jwk['dq'] = Util::base64url_encode($seq->getChildAt(7)->getValueAsUIntOctets());
$jwk['qi'] = Util::base64url_encode($seq->getChildAt(8)->getValueAsUIntOctets());
$private_octet_string = $seq->getChildAt(2)->getValue();
$private_seq = $der->decode($private_octet_string);

$jwk = self::parseASN1PrivateKey($private_seq);
} else {
throw new KeyException('Unrecognised key format');
}
Expand Down Expand Up @@ -193,6 +199,32 @@ protected function getThumbnailMembers(): array {
// https://tools.ietf.org/html/rfc7638#section-3.2
return ['e', 'kty', 'n'];
}

/**
* Parses an RSA private key in DER form.
*
* An RSA private key is encoded using the RSAPrivateKey type as per PKCS#1
*
* @param ASN1Value $seq the ASN.1 sequence to parse
* @return array<string, mixed> the parsed private key data
* @throws KeyException if an error occurs in parsing the key
*/
protected static function parseASN1PrivateKey(ASN1Value $seq): array {
$version = $seq->getChildAt(0)->getValue();
if ($version != 0) throw new KeyException('Unsupported RSA private key version');

return [
'kty' => self::KTY,
'n' => Util::base64url_encode($seq->getChildAt(1)->getValueAsUIntOctets()),
'e' => Util::base64url_encode($seq->getChildAt(2)->getValueAsUIntOctets()),
'd' => Util::base64url_encode($seq->getChildAt(3)->getValueAsUIntOctets()),
'p' => Util::base64url_encode($seq->getChildAt(4)->getValueAsUIntOctets()),
'q' => Util::base64url_encode($seq->getChildAt(5)->getValueAsUIntOctets()),
'dp' => Util::base64url_encode($seq->getChildAt(6)->getValueAsUIntOctets()),
'dq' => Util::base64url_encode($seq->getChildAt(7)->getValueAsUIntOctets()),
'qi' => Util::base64url_encode($seq->getChildAt(8)->getValueAsUIntOctets())
];
}
}

?>
15 changes: 13 additions & 2 deletions tests/KeyFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
use PHPUnit\Framework\TestCase;

class KeyFactoryTest extends TestCase {
public function testPEMRSA() {
$pem = file_get_contents('rsa_private.pem');
public function testPEMRSA_PKCS1() {
$pem = file_get_contents('rsa_private_pkcs1.pem');
$key = KeyFactory::create($pem, 'pem');
$this->assertInstanceOf(RSAKey::class, $key);
$key_data = $key->getKeyData();
Expand All @@ -26,6 +26,17 @@ public function testPEMRSA() {
$this->assertEquals("p8wHcPeYsIbQBwFg-mUXIFjZI-b1gQJuNoGboa3ub7KMVjmob9c4mOqc8j2u9cMS6PLnqGMIiM2H1HVDZSwZs6kS7Kq942uNBsut2cHy-PZd5Jq3cWIoQZwnhrjg_OfwbJugYeGe0Orub8J42qyT8HuhLX65Q6iSSf_3bo1Rr2M", $key_data['n']);
}

public function testPEMRSA_PKCS8() {
$pem = file_get_contents('rsa_private_pkcs8.pem');
$key = KeyFactory::create($pem, 'pem');
$this->assertInstanceOf(RSAKey::class, $key);
$key_data = $key->getKeyData();
$this->assertFalse($key->isPublic());
$this->assertEquals("p8wHcPeYsIbQBwFg-mUXIFjZI-b1gQJuNoGboa3ub7KMVjmob9c4mOqc8j2u9cMS6PLnqGMIiM2H1HVDZSwZs6kS7Kq942uNBsut2cHy-PZd5Jq3cWIoQZwnhrjg_OfwbJugYeGe0Orub8J42qyT8HuhLX65Q6iSSf_3bo1Rr2M", $key_data['n']);
$this->assertEquals("Wxts7umA_lg0m5kkDtDUvbuAKv48TtADB5VX63GFBSDtEeQ8kH1LPbwle2ICnW5N1i4NmmArQhxWpAUHkudfDExa9fZf5wUtsDlw8zNhzoDKqtw50D1BhWCfYO19IobTL-x3RJmPAepK5IH8ZYfGbQD2ZEwN0WF2I8sBg4Pu6Q", $key_data['d']);
$this->assertEquals("2sUA6n24v51XMCk_H3WKTELYfEyWzdr6yI3a8xNol8RlLNcjr9NxthPWkOGY6uT1JAbDgBNsiPQXpAiOdRD5FQ", $key_data['p']);
$this->assertEquals("xFpV_GjvAlkzeElY_fb5AWgV9_APIfyuo3NxqmvaRTIUyatsHdrUNE0jsOTJF-2uZZ818sbHltDOb-x3_3y0lw", $key_data['q']);
}

public function testPEMEC() {
$pem = file_get_contents('ec_private.pem');
Expand Down
File renamed without changes.
16 changes: 16 additions & 0 deletions tests/rsa_private_pkcs8.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdAIBADANBgkqhkiG9w0BAQEFAASCAl4wggJaAgEAAoGBAKfMB3D3mLCG0AcB
YPplFyBY2SPm9YECbjaBm6Gt7m+yjFY5qG/XOJjqnPI9rvXDEujy56hjCIjNh9R1
Q2UsGbOpEuyqveNrjQbLrdnB8vj2XeSat3FiKEGcJ4a44Pzn8GyboGHhntDq7m/C
eNqsk/B7oS1+uUOokkn/926NUa9jAgMBAAECf1sbbO7pgP5YNJuZJA7Q1L27gCr+
PE7QAweVV+txhQUg7RHkPJB9Sz28JXtiAp1uTdYuDZpgK0IcVqQFB5LnXwxMWvX2
X+cFLbA5cPMzYc6AyqrcOdA9QYVgn2DtfSKG0y/sd0SZjwHqSuSB/GWHxm0A9mRM
DdFhdiPLAYOD7ukCQQDaxQDqfbi/nVcwKT8fdYpMQth8TJbN2vrIjdrzE2iXxGUs
1yOv03G2E9aQ4Zjq5PUkBsOAE2yI9BekCI51EPkVAkEAxFpV/GjvAlkzeElY/fb5
AWgV9/APIfyuo3NxqmvaRTIUyatsHdrUNE0jsOTJF+2uZZ818sbHltDOb+x3/3y0
lwJAJKcE9ESEA3Z0+Riv/rFOrmA0rP6X9X1OrvM1T6xcxbCd0tlonTirwHmqZTOm
zxP1Dkgj0P1wHbHnlA/q7iod0QJBAKcmd7ht47lOEkC9v+JCkmseHkV4uIkoP8qp
BgQb2C27mnKrWVh45ti3KkD2IjIahbHAvP41Nccvbe1dkjGOCXUCQGbnRafT+O5B
YNjKRUNVDDyTlrdrAfG/C3v6REH3Q+6Z7kCYBi1ZyryUUwH+2iLpO9+6w1geGFPT
siByv/VbT1I=
-----END PRIVATE KEY-----

0 comments on commit bcd952f

Please sign in to comment.