]> BookStack Code Mirror - bookstack/blob - app/Auth/Access/Oidc/OidcJwtSigningKey.php
Fixed failing webhook test cases
[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      *
23      * @param array|string $jwkOrKeyPath
24      *
25      * @throws OidcInvalidKeyException
26      */
27     public function __construct($jwkOrKeyPath)
28     {
29         if (is_array($jwkOrKeyPath)) {
30             $this->loadFromJwkArray($jwkOrKeyPath);
31         } elseif (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) {
32             $this->loadFromPath($jwkOrKeyPath);
33         } else {
34             throw new OidcInvalidKeyException('Unexpected type of key value provided');
35         }
36     }
37
38     /**
39      * @throws OidcInvalidKeyException
40      */
41     protected function loadFromPath(string $path)
42     {
43         try {
44             $key = PublicKeyLoader::load(
45                 file_get_contents($path)
46             );
47         } catch (\Exception $exception) {
48             throw new OidcInvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}");
49         }
50
51         if (!$key instanceof RSA) {
52             throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
53         }
54
55         $this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
56     }
57
58     /**
59      * @throws OidcInvalidKeyException
60      */
61     protected function loadFromJwkArray(array $jwk)
62     {
63         if ($jwk['alg'] !== 'RS256') {
64             throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$jwk['alg']}");
65         }
66
67         if (empty($jwk['use'])) {
68             throw new OidcInvalidKeyException('A "use" parameter on the provided key is expected');
69         }
70
71         if ($jwk['use'] !== 'sig') {
72             throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
73         }
74
75         if (empty($jwk['e'])) {
76             throw new OidcInvalidKeyException('An "e" parameter on the provided key is expected');
77         }
78
79         if (empty($jwk['n'])) {
80             throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected');
81         }
82
83         $n = strtr($jwk['n'] ?? '', '-_', '+/');
84
85         try {
86             $key = PublicKeyLoader::load([
87                 'e' => new BigInteger(base64_decode($jwk['e']), 256),
88                 'n' => new BigInteger(base64_decode($n), 256),
89             ]);
90         } catch (\Exception $exception) {
91             throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
92         }
93
94         if (!$key instanceof RSA) {
95             throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
96         }
97
98         $this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
99     }
100
101     /**
102      * Use this key to sign the given content and return the signature.
103      */
104     public function verify(string $content, string $signature): bool
105     {
106         return $this->key->verify($content, $signature);
107     }
108
109     /**
110      * Convert the key to a PEM encoded key string.
111      */
112     public function toPem(): string
113     {
114         return $this->key->toString('PKCS8');
115     }
116 }