]> BookStack Code Mirror - bookstack/blob - app/Auth/Access/Oidc/OidcJwtSigningKey.php
Fixed failing test after drawio default url change
[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         // 'alg' is optional for a JWK, but we will still attempt to validate if
64         // it exists otherwise presume it will be compatible.
65         $alg = $jwk['alg'] ?? null;
66         if ($jwk['kty'] !== 'RSA' || !(is_null($alg) || $alg === 'RS256')) {
67             throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$alg}");
68         }
69
70         if (empty($jwk['use'])) {
71             throw new OidcInvalidKeyException('A "use" parameter on the provided key is expected');
72         }
73
74         if ($jwk['use'] !== 'sig') {
75             throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
76         }
77
78         if (empty($jwk['e'])) {
79             throw new OidcInvalidKeyException('An "e" parameter on the provided key is expected');
80         }
81
82         if (empty($jwk['n'])) {
83             throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected');
84         }
85
86         $n = strtr($jwk['n'] ?? '', '-_', '+/');
87
88         try {
89             $key = PublicKeyLoader::load([
90                 'e' => new BigInteger(base64_decode($jwk['e']), 256),
91                 'n' => new BigInteger(base64_decode($n), 256),
92             ]);
93         } catch (\Exception $exception) {
94             throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
95         }
96
97         if (!$key instanceof RSA) {
98             throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
99         }
100
101         $this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
102     }
103
104     /**
105      * Use this key to sign the given content and return the signature.
106      */
107     public function verify(string $content, string $signature): bool
108     {
109         return $this->key->verify($content, $signature);
110     }
111
112     /**
113      * Convert the key to a PEM encoded key string.
114      */
115     public function toPem(): string
116     {
117         return $this->key->toString('PKCS8');
118     }
119 }