Skip to content
This repository has been archived by the owner on Nov 14, 2023. It is now read-only.

Commit

Permalink
Cleaner Key class and sundry names
Browse files Browse the repository at this point in the history
* Key now handles inputs as X.509, naked Public Key and
  Private Key (all PEM)
* Improved & clearer detection logic with explicit tests
  for asymmetric key material, falling back on shared secret
  for HMAC if asymmetric keys are not detected
  hasX509Certificate()
  hasPublicKey()
  hasPrivateKey()
* Clearer names for test constants (easier to expand to EC later)
  • Loading branch information
liamdennehy committed Jul 30, 2019
1 parent 0c9d9b8 commit fe91c52
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 85 deletions.
167 changes: 115 additions & 52 deletions src/Key.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ class Key
/** @var string */
private $secret;

/** @var string */
/** @var resource */
private $certificate;

/** @var string */
/** @var resource */
private $publicKey;

/** @var resource */
private $privateKey;

/** @var string */
Expand All @@ -26,76 +29,75 @@ class Key
public function __construct($id, $item)
{
$this->id = $id;
$certificate = $this->getX509Certificate($item);
$privateKey = $this->getRSAPrivateKey($item);
if (($certificate || $privateKey)) {
$this->type = 'rsa';
if ($privateKey) {
$this->privateKey = $privateKey;
}
if ($certificate) {
$this->certificate = openssl_x509_read($certificate);
}
if ($certificate && $privateKey) {
if (!openssl_x509_check_private_key(
$this->certificate,
$this->privateKey
)
) {
if (Key::hasX509Certificate($item) || Key::hasPublicKey($item)) {
$publicKey = Key::getPublicKey($item);
} else {
$publicKey = null;
}
if (Key::hasPrivateKey($item)) {
$privateKey = Key::getPrivateKey($item);
} else {
$privateKey = null;
}
if (($publicKey || $privateKey)) {
$this->type = 'asymmetric';
if ($publicKey && $privateKey) {
$publicKeyPEM = openssl_pkey_get_details($publicKey)['key'];
$privateKeyPublicPEM = openssl_pkey_get_details($privateKey)['key'];
if ($privateKeyPublicPEM != $publicKeyPEM) {
throw new KeyException('Supplied Certificate and Key are not related');
}
}
$this->privateKey = $privateKey;
$this->publicKey = $publicKey;
$this->secret = null;
} else {
$this->type = 'secret';
$this->secret = $item;
$publicKey = null;
$privateKey = null;
$this->publicKey = null;
$this->privateKey = null;
}
}

private function getRSAPrivateKey($object)
public static function getPrivateKey($object)
{
if (is_array($object)) {
foreach ($object as $item) {
$privateKey = Key::getRSAPrivateKey($item);
foreach ($object as $candidateKey) {
$privateKey = Key::getPrivateKey($candidateKey);
if ($privateKey) {
return $privateKey;
}
}
} else {
// OpenSSL libraries don't have detection methods, so try..catch
try {
$privateKey = openssl_get_privatekey($object);
} catch (\Exception $e) {
$privateKey = null;
}
if ($privateKey) {

return $privateKey;
} catch (\Exception $e) {
return null;
}
}
}

private function getX509Certificate($object)
public static function getPublicKey($object)
{
$key = null;
if (is_array($object)) {
foreach ($object as $item) {
$result = Key::getX509Certificate($item);
if ($result) {
$key = $result;
// If we implement key rotation in future, this should add to a collection
foreach ($object as $candidateKey) {
$publicKey = Key::getPublicKey($candidateKey);
if ($publicKey) {
return $publicKey;
}
}

return $key;
} else {
// OpenSSL libraries don't have detection methods, so try..catch
try {
$result = openssl_get_publickey($object);
} catch (\Exception $e) {
$result = null;
}
if ($result) {
openssl_x509_export($object, $out);
$publicKey = openssl_get_publickey($object);

return $object;
return $publicKey;
} catch (\Exception $e) {
return null;
}
}
}
Expand All @@ -108,29 +110,90 @@ public function getId()
public function getVerifyingKey()
{
switch ($this->type) {
case 'rsa':
return openssl_pkey_get_public($this->certificate);
case 'asymmetric':
if ($this->publicKey) {
return openssl_pkey_get_details($this->publicKey)['key'];
} else {
return null;
}
break;
case 'secret':
return $this->secret;
return $this->secret;
default:
throw new KeyException("Unknown key type $this->type");
}
throw new KeyException("Unknown key type $this->type");
}
}

public function getSigningKey()
{
switch ($this->type) {
case 'rsa':
return $this->privateKey;
case 'asymmetric':
if ($this->privateKey) {
openssl_pkey_export($this->privateKey, $pem);

return $pem;
} else {
return null;
}
break;
case 'secret':
return $this->secret;
return $this->secret;
default:
throw new KeyException("Unknown key type $this->type");
}
throw new KeyException("Unknown key type $this->type");
}
}

public function getType()
{
return $this->type;
}

public static function hasX509Certificate($object)
{
if (is_array($object)) {
foreach ($object as $candidateCertificate) {
$result = Key::hasX509Certificate($candidateCertificate);
if ($result) {
return $result;
}
}
} else {
// OpenSSL libraries don't have detection methods, so try..catch
try {
openssl_x509_export($object, $null);

return true;
} catch (\Exception $e) {
return false;
}
}
}

public static function hasPublicKey($object)
{
if (is_array($object)) {
foreach ($object as $candidatePublicKey) {
$result = Key::hasPublicKey($candidatePublicKey);
if ($result) {
return $result;
}
}
} else {
return false == !openssl_pkey_get_public($object);
}
}

public static function hasPrivateKey($object)
{
if (is_array($object)) {
foreach ($object as $candidatePrivateKey) {
$result = Key::hasPrivateKey($candidatePrivateKey);
if ($result) {
return $result;
}
}
} else {
return false != openssl_pkey_get_private($object);
}
}
}
6 changes: 3 additions & 3 deletions src/RsaAlgorithm.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ private function getRsaHashAlgo($digestName)
{
switch ($digestName) {
case 'sha256':
return OPENSSL_ALGO_SHA256;
return OPENSSL_ALGO_SHA256;
case 'sha1':
return OPENSSL_ALGO_SHA1;
return OPENSSL_ALGO_SHA1;
default:
throw new HttpSignatures\AlgorithmException($digestName.' is not a supported hash format');
throw new HttpSignatures\AlgorithmException($digestName.' is not a supported hash format');
}
}
}
4 changes: 2 additions & 2 deletions src/Verification.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ private function signatureMatches()
);

return $expectedResult === $providedResult;
case 'rsa':
case 'asymmetric':
$signedString = new SigningString(
$this->headerList(),
$this->message
Expand All @@ -69,7 +69,7 @@ private function signatureMatches()

return $result;
default:
throw new Exception("Unknown key type '$key->type', cannot verify");
throw new Exception("Unknown key type '".$key->getType()."', cannot verify");
}
} catch (SignatureParseException $e) {
return false;
Expand Down
83 changes: 60 additions & 23 deletions tests/KeyStoreRsaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,95 @@

class KeyStoreRsaTest extends \PHPUnit_Framework_TestCase
{
/** @var string */
public $testRsaPrivateKeyPEM;

/** @var string */
public $testRsaPublicKeyPEM;

public function setUp()
{
openssl_pkey_export(
openssl_pkey_get_private(TestKeys::rsaKey),
$this->testRsaPrivateKeyPEM
);
openssl_pkey_get_private(TestKeys::rsaPrivateKey),
$this->testRsaPrivateKeyPEM
);
$this->testRsaPublicKeyPEM = openssl_pkey_get_details(
openssl_get_publickey(TestKeys::rsaCert)
openssl_get_publickey(TestKeys::rsaPublicKey)
)['key'];
$this->testRsaCert = TestKeys::rsaCert;
}

public function testParseX509inObject()
{
$keySpec = ['rsaCert' => [TestKeys::rsaCert]];
$this->assertTrue(Key::hasX509Certificate($keySpec));

$ks = new KeyStore($keySpec);
$publicKey = $ks->fetch('rsaCert')->getVerifyingKey();
$this->assertEquals('asymmetric', $ks->fetch('rsaCert')->getType());
$this->assertEquals(TestKeys::rsaPublicKey, $publicKey);
}

public function testParseRsaPublicKeyinObject()
{
$keySpec = ['rsaPubKey' => [TestKeys::rsaPublicKey]];
$this->assertTrue(Key::hasPublicKey($keySpec));

$ks = new KeyStore($keySpec);
$publicKey = $ks->fetch('rsaPubKey')->getVerifyingKey();
$this->assertEquals('asymmetric', $ks->fetch('rsaPubKey')->getType());
$this->assertEquals(TestKeys::rsaPublicKey, $publicKey);
}

public function testParsePrivateKeyinObject()
{
$keySpec = ['rsaPrivKey' => [TestKeys::rsaPrivateKey]];
$this->assertTrue(Key::hasPrivateKey($keySpec));

$ks = new KeyStore($keySpec);
$publicKey = $ks->fetch('rsaPrivKey')->getSigningKey();
$this->assertEquals('asymmetric', $ks->fetch('rsaPrivKey')->getType());
$this->assertEquals($this->testRsaPrivateKeyPEM, $publicKey);
}

public function testFetchRsaSigningKeySuccess()
{
$ks = new KeyStore(['rsakey' => TestKeys::rsaKey]);
$ks = new KeyStore(['rsakey' => TestKeys::rsaPrivateKey]);
$key = $ks->fetch('rsakey');
openssl_pkey_export($key->getSigningKey(), $keyStoreSigningKey);
$this->assertEquals(['rsakey', $this->testRsaPrivateKeyPEM, null, 'rsa'], [
$this->assertEquals(['rsakey', $this->testRsaPrivateKeyPEM, null, 'asymmetric'], [
$key->getId(), $keyStoreSigningKey, $key->getVerifyingKey(), $key->getType(), ]);
}

public function testFetchRsaVerifyingKeySuccess()
public function testFetchRsaVerifyingKeyFromCertificateSuccess()
{
$ks = new KeyStore(['rsacert' => TestKeys::rsaCert]);
$key = $ks->fetch('rsacert');
$keyStoreVerifyingKey = openssl_pkey_get_details($key->getVerifyingKey())['key'];
$this->assertEquals(['rsacert', null, $this->testRsaPublicKeyPEM, 'rsa'], [
$keyStoreVerifyingKey = $key->getVerifyingKey();
$this->assertEquals(['rsacert', null, $this->testRsaPublicKeyPEM, 'asymmetric'], [
$key->getId(), $key->getSigningKey(), $keyStoreVerifyingKey, $key->getType(), ]);
}

public function testFetchRsaVerifyingKeyFromPublicKeySuccess()
{
$ks = new KeyStore(['rsapubkey' => TestKeys::rsaPublicKey]);
$key = $ks->fetch('rsapubkey');
$keyStoreVerifyingKey = $key->getVerifyingKey();
$this->assertEquals(['rsapubkey', null, $this->testRsaPublicKeyPEM, 'asymmetric'], [
$key->getId(), $key->getSigningKey(), $keyStoreVerifyingKey, $key->getType(), ]);
}

public function testFetchRsaBothSuccess()
{
$ks = new KeyStore(['rsaboth' => [TestKeys::rsaCert, TestKeys::rsaKey]]);
$ks = new KeyStore(['rsaboth' => [TestKeys::rsaCert, TestKeys::rsaPrivateKey]]);
$key = $ks->fetch('rsaboth');
$keyStoreVerifyingKey = openssl_pkey_get_details($key->getVerifyingKey())['key'];
openssl_pkey_export($key->getSigningKey(), $keyStoreSigningKey);
$this->assertEquals(['rsaboth', $this->testRsaPrivateKeyPEM, $this->testRsaPublicKeyPEM, 'rsa'], [
$keyStoreVerifyingKey = $key->getVerifyingKey();
$keyStoreSigningKey = $key->getSigningKey();
$this->assertEquals(['rsaboth', $this->testRsaPrivateKeyPEM, $this->testRsaPublicKeyPEM, 'asymmetric'], [
$key->getId(), $keyStoreSigningKey, $keyStoreVerifyingKey, $key->getType(), ]);
}

public function testFetchRsaBothSuccessSwitched()
{
$ks = new KeyStore(['rsabothswitch' => [TestKeys::rsaKey, TestKeys::rsaCert]]);
$ks = new KeyStore(['rsabothswitch' => [TestKeys::rsaPrivateKey, TestKeys::rsaCert]]);
$key = $ks->fetch('rsabothswitch');
$keyStoreVerifyingKey = openssl_pkey_get_details($key->getVerifyingKey())['key'];
openssl_pkey_export($key->getSigningKey(), $keyStoreSigningKey);
$this->assertEquals(['rsabothswitch', $this->testRsaPrivateKeyPEM, $this->testRsaPublicKeyPEM, 'rsa'], [
$keyStoreVerifyingKey = $key->getVerifyingKey();
$keyStoreSigningKey = $key->getSigningKey();
$this->assertEquals(['rsabothswitch', $this->testRsaPrivateKeyPEM, $this->testRsaPublicKeyPEM, 'asymmetric'], [
$key->getId(), $keyStoreSigningKey, $keyStoreVerifyingKey, $key->getType(), ]);
}

Expand Down
Loading

0 comments on commit fe91c52

Please sign in to comment.