2021-10-11 14:05:16 -04:00
|
|
|
<?php
|
|
|
|
|
2021-10-12 18:04:28 -04:00
|
|
|
namespace BookStack\Auth\Access\Oidc;
|
2021-10-11 14:05:16 -04:00
|
|
|
|
|
|
|
use phpseclib3\Crypt\Common\PublicKey;
|
|
|
|
use phpseclib3\Crypt\PublicKeyLoader;
|
|
|
|
use phpseclib3\Crypt\RSA;
|
|
|
|
use phpseclib3\Math\BigInteger;
|
|
|
|
|
2021-10-12 18:04:28 -04:00
|
|
|
class OidcJwtSigningKey
|
2021-10-11 14:05:16 -04:00
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var PublicKey
|
|
|
|
*/
|
|
|
|
protected $key;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Can be created either from a JWK parameter array or local file path to load a certificate from.
|
|
|
|
* Examples:
|
|
|
|
* 'file:///var/www/cert.pem'
|
2021-10-16 11:01:59 -04:00
|
|
|
* ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...'].
|
|
|
|
*
|
2021-10-11 14:05:16 -04:00
|
|
|
* @param array|string $jwkOrKeyPath
|
2021-10-16 11:01:59 -04:00
|
|
|
*
|
2021-10-12 18:04:28 -04:00
|
|
|
* @throws OidcInvalidKeyException
|
2021-10-11 14:05:16 -04:00
|
|
|
*/
|
|
|
|
public function __construct($jwkOrKeyPath)
|
|
|
|
{
|
|
|
|
if (is_array($jwkOrKeyPath)) {
|
|
|
|
$this->loadFromJwkArray($jwkOrKeyPath);
|
2021-10-16 11:01:59 -04:00
|
|
|
} elseif (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) {
|
2021-10-11 18:00:45 -04:00
|
|
|
$this->loadFromPath($jwkOrKeyPath);
|
|
|
|
} else {
|
2021-10-12 18:04:28 -04:00
|
|
|
throw new OidcInvalidKeyException('Unexpected type of key value provided');
|
2021-10-11 18:00:45 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-10-12 18:04:28 -04:00
|
|
|
* @throws OidcInvalidKeyException
|
2021-10-11 18:00:45 -04:00
|
|
|
*/
|
|
|
|
protected function loadFromPath(string $path)
|
|
|
|
{
|
|
|
|
try {
|
2021-11-22 18:33:55 -05:00
|
|
|
$key = PublicKeyLoader::load(
|
2021-10-11 18:00:45 -04:00
|
|
|
file_get_contents($path)
|
2021-11-22 18:33:55 -05:00
|
|
|
);
|
2021-10-11 18:00:45 -04:00
|
|
|
} catch (\Exception $exception) {
|
2021-10-12 18:04:28 -04:00
|
|
|
throw new OidcInvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}");
|
2021-10-11 18:00:45 -04:00
|
|
|
}
|
|
|
|
|
2021-11-22 18:33:55 -05:00
|
|
|
if (!$key instanceof RSA) {
|
2021-10-16 11:01:59 -04:00
|
|
|
throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
|
2021-10-11 14:05:16 -04:00
|
|
|
}
|
2021-11-22 18:33:55 -05:00
|
|
|
|
|
|
|
$this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
|
2021-10-11 14:05:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-10-12 18:04:28 -04:00
|
|
|
* @throws OidcInvalidKeyException
|
2021-10-11 14:05:16 -04:00
|
|
|
*/
|
|
|
|
protected function loadFromJwkArray(array $jwk)
|
|
|
|
{
|
2022-01-28 09:00:55 -05:00
|
|
|
// 'alg' is optional for a JWK, but we will still attempt to validate if
|
|
|
|
// it exists otherwise presume it will be compatible.
|
|
|
|
$alg = $jwk['alg'] ?? null;
|
|
|
|
if ($jwk['kty'] !== 'RSA' || !(is_null($alg) || $alg === 'RS256')) {
|
|
|
|
throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$alg}");
|
2021-10-11 14:05:16 -04:00
|
|
|
}
|
|
|
|
|
2022-11-23 06:50:59 -05:00
|
|
|
// 'use' is optional for a JWK but we assume 'sig' where no value exists since that's what
|
|
|
|
// the OIDC discovery spec infers since 'sig' MUST be set if encryption keys come into play.
|
|
|
|
$use = $jwk['use'] ?? 'sig';
|
|
|
|
if ($use !== 'sig') {
|
2021-10-12 18:04:28 -04:00
|
|
|
throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
|
2021-10-11 14:05:16 -04:00
|
|
|
}
|
|
|
|
|
2021-10-11 19:01:51 -04:00
|
|
|
if (empty($jwk['e'])) {
|
2021-10-12 18:04:28 -04:00
|
|
|
throw new OidcInvalidKeyException('An "e" parameter on the provided key is expected');
|
2021-10-11 14:05:16 -04:00
|
|
|
}
|
|
|
|
|
2021-10-11 19:01:51 -04:00
|
|
|
if (empty($jwk['n'])) {
|
2021-10-12 18:04:28 -04:00
|
|
|
throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected');
|
2021-10-11 14:05:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
$n = strtr($jwk['n'] ?? '', '-_', '+/');
|
|
|
|
|
|
|
|
try {
|
2021-11-22 18:33:55 -05:00
|
|
|
$key = PublicKeyLoader::load([
|
2021-10-11 14:05:16 -04:00
|
|
|
'e' => new BigInteger(base64_decode($jwk['e']), 256),
|
|
|
|
'n' => new BigInteger(base64_decode($n), 256),
|
2021-11-22 18:33:55 -05:00
|
|
|
]);
|
2021-10-11 14:05:16 -04:00
|
|
|
} catch (\Exception $exception) {
|
2021-10-12 18:04:28 -04:00
|
|
|
throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
|
2021-10-11 14:05:16 -04:00
|
|
|
}
|
2021-11-22 18:33:55 -05:00
|
|
|
|
|
|
|
if (!$key instanceof RSA) {
|
|
|
|
throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
|
2021-10-11 14:05:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Use this key to sign the given content and return the signature.
|
|
|
|
*/
|
|
|
|
public function verify(string $content, string $signature): bool
|
|
|
|
{
|
|
|
|
return $this->key->verify($content, $signature);
|
|
|
|
}
|
|
|
|
|
2021-10-11 18:00:45 -04:00
|
|
|
/**
|
|
|
|
* Convert the key to a PEM encoded key string.
|
|
|
|
*/
|
|
|
|
public function toPem(): string
|
|
|
|
{
|
|
|
|
return $this->key->toString('PKCS8');
|
|
|
|
}
|
2021-10-16 11:01:59 -04:00
|
|
|
}
|