]> BookStack Code Mirror - bookstack/blob - app/Access/Oidc/OidcJwtSigningKey.php
System CLI: Updated to 126de5599c state
[bookstack] / app / Access / Oidc / OidcJwtSigningKey.php
1 <?php
2
3 namespace BookStack\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         // 'use' is optional for a JWK but we assume 'sig' where no value exists since that's what
71         // the OIDC discovery spec infers since 'sig' MUST be set if encryption keys come into play.
72         $use = $jwk['use'] ?? 'sig';
73         if ($use !== 'sig') {
74             throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
75         }
76
77         if (empty($jwk['e'])) {
78             throw new OidcInvalidKeyException('An "e" parameter on the provided key is expected');
79         }
80
81         if (empty($jwk['n'])) {
82             throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected');
83         }
84
85         $n = strtr($jwk['n'] ?? '', '-_', '+/');
86
87         try {
88             $key = PublicKeyLoader::load([
89                 'e' => new BigInteger(base64_decode($jwk['e']), 256),
90                 'n' => new BigInteger(base64_decode($n), 256),
91             ]);
92         } catch (\Exception $exception) {
93             throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}");
94         }
95
96         if (!$key instanceof RSA) {
97             throw new OidcInvalidKeyException('Key loaded from file path is not an RSA key as expected');
98         }
99
100         $this->key = $key->withPadding(RSA::SIGNATURE_PKCS1);
101     }
102
103     /**
104      * Use this key to sign the given content and return the signature.
105      */
106     public function verify(string $content, string $signature): bool
107     {
108         return $this->key->verify($content, $signature);
109     }
110
111     /**
112      * Convert the key to a PEM encoded key string.
113      */
114     public function toPem(): string
115     {
116         return $this->key->toString('PKCS8');
117     }
118 }