]> BookStack Code Mirror - bookstack/blob - app/Auth/Access/Oidc/OidcJwtSigningKey.php
Merge branch 'oidc'
[bookstack] / app / Auth / Access / Oidc / OidcJwtSigningKey.php
1 <?php
2
3 namespace BookStack\Auth\Access\Oidc;
4
5 use phpseclib3\Crypt\Common\PublicKey;
6 use phpseclib3\Crypt\PublicKeyLoader;
7 use phpseclib3\Crypt\RSA;
8 use phpseclib3\Math\BigInteger;
9
10 class OidcJwtSigningKey
11 {
12     /**
13      * @var PublicKey
14      */
15     protected $key;
16
17     /**
18      * Can be created either from a JWK parameter array or local file path to load a certificate from.
19      * Examples:
20      * 'file:///var/www/cert.pem'
21      * ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...']
22      * @param array|string $jwkOrKeyPath
23      * @throws OidcInvalidKeyException
24      */
25     public function __construct($jwkOrKeyPath)
26     {
27         if (is_array($jwkOrKeyPath)) {
28             $this->loadFromJwkArray($jwkOrKeyPath);
29         } else if (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) {
30             $this->loadFromPath($jwkOrKeyPath);
31         } else {
32             throw new OidcInvalidKeyException('Unexpected type of key value provided');
33         }
34     }
35
36     /**
37      * @throws OidcInvalidKeyException
38      */
39     protected function loadFromPath(string $path)
40     {
41         try {
42             $this->key = PublicKeyLoader::load(
43                 file_get_contents($path)
44             )->withPadding(RSA::SIGNATURE_PKCS1);
45         } catch (\Exception $exception) {
46             throw new OidcInvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}");
47         }
48
49         if (!($this->key instanceof RSA)) {
50             throw new OidcInvalidKeyException("Key loaded from file path is not an RSA key as expected");
51         }
52     }
53
54     /**
55      * @throws OidcInvalidKeyException
56      */
57     protected function loadFromJwkArray(array $jwk)
58     {
59         if ($jwk['alg'] !== 'RS256') {
60             throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$jwk['alg']}");
61         }
62
63         if (empty($jwk['use'])) {
64             throw new OidcInvalidKeyException('A "use" parameter on the provided key is expected');
65         }
66
67         if ($jwk['use'] !== 'sig') {
68             throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
69         }
70
71         if (empty($jwk['e'])) {
72             throw new OidcInvalidKeyException('An "e" parameter on the provided key is expected');
73         }
74
75         if (empty($jwk['n'])) {
76             throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected');
77         }
78
79         $n = strtr($jwk['n'] ?? '', '-_', '+/');
80
81         try {
82             /** @var RSA $key */
83             $this->key = PublicKeyLoader::load([
84                 'e' => new BigInteger(base64_decode($jwk['e']), 256),
85                 'n' => new BigInteger(base64_decode($n), 256),
86             ])->withPadding(RSA::SIGNATURE_PKCS1);
87         } catch (\Exception $exception) {
88             throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
89         }
90     }
91
92     /**
93      * Use this key to sign the given content and return the signature.
94      */
95     public function verify(string $content, string $signature): bool
96     {
97         return $this->key->verify($content, $signature);
98     }
99
100     /**
101      * Convert the key to a PEM encoded key string.
102      */
103     public function toPem(): string
104     {
105         return $this->key->toString('PKCS8');
106     }
107
108 }