Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
 
3
namespace Firebase\JWT;
4
 
5
use DomainException;
6
use InvalidArgumentException;
7
use UnexpectedValueException;
8
 
9
/**
10
 * JSON Web Key implementation, based on this spec:
11
 * https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
12
 *
13
 * PHP version 5
14
 *
15
 * @category Authentication
16
 * @package  Authentication_JWT
17
 * @author   Bui Sy Nguyen <nguyenbs@gmail.com>
18
 * @license  http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
19
 * @link     https://github.com/firebase/php-jwt
20
 */
21
class JWK
22
{
23
    private const OID = '1.2.840.10045.2.1';
24
    private const ASN1_OBJECT_IDENTIFIER = 0x06;
25
    private const ASN1_SEQUENCE = 0x10; // also defined in JWT
26
    private const ASN1_BIT_STRING = 0x03;
27
    private const EC_CURVES = [
28
        'P-256' => '1.2.840.10045.3.1.7', // Len: 64
29
        'secp256k1' => '1.3.132.0.10', // Len: 64
30
        'P-384' => '1.3.132.0.34', // Len: 96
31
        // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
32
    ];
33
 
34
    // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype.
35
    // This library supports the following subtypes:
36
    private const OKP_SUBTYPES = [
37
        'Ed25519' => true, // RFC 8037
38
    ];
39
 
40
    /**
41
     * Parse a set of JWK keys
42
     *
43
     * @param array<mixed> $jwks The JSON Web Key Set as an associative array
44
     * @param string       $defaultAlg The algorithm for the Key object if "alg" is not set in the
45
     *                                 JSON Web Key Set
46
     *
47
     * @return array<string, Key> An associative array of key IDs (kid) to Key objects
48
     *
49
     * @throws InvalidArgumentException     Provided JWK Set is empty
50
     * @throws UnexpectedValueException     Provided JWK Set was invalid
51
     * @throws DomainException              OpenSSL failure
52
     *
53
     * @uses parseKey
54
     */
1441 ariadna 55
    public static function parseKeySet(array $jwks, ?string $defaultAlg = null): array
1 efrain 56
    {
57
        $keys = [];
58
 
59
        if (!isset($jwks['keys'])) {
60
            throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
61
        }
62
 
63
        if (empty($jwks['keys'])) {
64
            throw new InvalidArgumentException('JWK Set did not contain any keys');
65
        }
66
 
67
        foreach ($jwks['keys'] as $k => $v) {
68
            $kid = isset($v['kid']) ? $v['kid'] : $k;
69
            if ($key = self::parseKey($v, $defaultAlg)) {
70
                $keys[(string) $kid] = $key;
71
            }
72
        }
73
 
74
        if (0 === \count($keys)) {
75
            throw new UnexpectedValueException('No supported algorithms found in JWK Set');
76
        }
77
 
78
        return $keys;
79
    }
80
 
81
    /**
82
     * Parse a JWK key
83
     *
84
     * @param array<mixed> $jwk An individual JWK
85
     * @param string       $defaultAlg The algorithm for the Key object if "alg" is not set in the
86
     *                                 JSON Web Key Set
87
     *
88
     * @return Key The key object for the JWK
89
     *
90
     * @throws InvalidArgumentException     Provided JWK is empty
91
     * @throws UnexpectedValueException     Provided JWK was invalid
92
     * @throws DomainException              OpenSSL failure
93
     *
94
     * @uses createPemFromModulusAndExponent
95
     */
1441 ariadna 96
    public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key
1 efrain 97
    {
98
        if (empty($jwk)) {
99
            throw new InvalidArgumentException('JWK must not be empty');
100
        }
101
 
102
        if (!isset($jwk['kty'])) {
103
            throw new UnexpectedValueException('JWK must contain a "kty" parameter');
104
        }
105
 
106
        if (!isset($jwk['alg'])) {
107
            if (\is_null($defaultAlg)) {
108
                // The "alg" parameter is optional in a KTY, but an algorithm is required
109
                // for parsing in this library. Use the $defaultAlg parameter when parsing the
110
                // key set in order to prevent this error.
111
                // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
112
                throw new UnexpectedValueException('JWK must contain an "alg" parameter');
113
            }
114
            $jwk['alg'] = $defaultAlg;
115
        }
116
 
117
        switch ($jwk['kty']) {
118
            case 'RSA':
119
                if (!empty($jwk['d'])) {
120
                    throw new UnexpectedValueException('RSA private keys are not supported');
121
                }
122
                if (!isset($jwk['n']) || !isset($jwk['e'])) {
123
                    throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
124
                }
125
 
126
                $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
127
                $publicKey = \openssl_pkey_get_public($pem);
128
                if (false === $publicKey) {
129
                    throw new DomainException(
130
                        'OpenSSL error: ' . \openssl_error_string()
131
                    );
132
                }
133
                return new Key($publicKey, $jwk['alg']);
134
            case 'EC':
135
                if (isset($jwk['d'])) {
136
                    // The key is actually a private key
137
                    throw new UnexpectedValueException('Key data must be for a public key');
138
                }
139
 
140
                if (empty($jwk['crv'])) {
141
                    throw new UnexpectedValueException('crv not set');
142
                }
143
 
144
                if (!isset(self::EC_CURVES[$jwk['crv']])) {
145
                    throw new DomainException('Unrecognised or unsupported EC curve');
146
                }
147
 
148
                if (empty($jwk['x']) || empty($jwk['y'])) {
149
                    throw new UnexpectedValueException('x and y not set');
150
                }
151
 
152
                $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
153
                return new Key($publicKey, $jwk['alg']);
154
            case 'OKP':
155
                if (isset($jwk['d'])) {
156
                    // The key is actually a private key
157
                    throw new UnexpectedValueException('Key data must be for a public key');
158
                }
159
 
160
                if (!isset($jwk['crv'])) {
161
                    throw new UnexpectedValueException('crv not set');
162
                }
163
 
164
                if (empty(self::OKP_SUBTYPES[$jwk['crv']])) {
165
                    throw new DomainException('Unrecognised or unsupported OKP key subtype');
166
                }
167
 
168
                if (empty($jwk['x'])) {
169
                    throw new UnexpectedValueException('x not set');
170
                }
171
 
172
                // This library works internally with EdDSA keys (Ed25519) encoded in standard base64.
173
                $publicKey = JWT::convertBase64urlToBase64($jwk['x']);
174
                return new Key($publicKey, $jwk['alg']);
1441 ariadna 175
            case 'oct':
176
                if (!isset($jwk['k'])) {
177
                    throw new UnexpectedValueException('k not set');
178
                }
179
 
180
                return new Key(JWT::urlsafeB64Decode($jwk['k']), $jwk['alg']);
1 efrain 181
            default:
182
                break;
183
        }
184
 
185
        return null;
186
    }
187
 
188
    /**
189
     * Converts the EC JWK values to pem format.
190
     *
191
     * @param   string  $crv The EC curve (only P-256 & P-384 is supported)
192
     * @param   string  $x   The EC x-coordinate
193
     * @param   string  $y   The EC y-coordinate
194
     *
195
     * @return  string
196
     */
197
    private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
198
    {
199
        $pem =
200
            self::encodeDER(
201
                self::ASN1_SEQUENCE,
202
                self::encodeDER(
203
                    self::ASN1_SEQUENCE,
204
                    self::encodeDER(
205
                        self::ASN1_OBJECT_IDENTIFIER,
206
                        self::encodeOID(self::OID)
207
                    )
208
                    . self::encodeDER(
209
                        self::ASN1_OBJECT_IDENTIFIER,
210
                        self::encodeOID(self::EC_CURVES[$crv])
211
                    )
212
                ) .
213
                self::encodeDER(
214
                    self::ASN1_BIT_STRING,
215
                    \chr(0x00) . \chr(0x04)
216
                    . JWT::urlsafeB64Decode($x)
217
                    . JWT::urlsafeB64Decode($y)
218
                )
219
            );
220
 
1441 ariadna 221
        return \sprintf(
1 efrain 222
            "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
223
            wordwrap(base64_encode($pem), 64, "\n", true)
224
        );
225
    }
226
 
227
    /**
228
     * Create a public key represented in PEM format from RSA modulus and exponent information
229
     *
230
     * @param string $n The RSA modulus encoded in Base64
231
     * @param string $e The RSA exponent encoded in Base64
232
     *
233
     * @return string The RSA public key represented in PEM format
234
     *
235
     * @uses encodeLength
236
     */
237
    private static function createPemFromModulusAndExponent(
238
        string $n,
239
        string $e
240
    ): string {
241
        $mod = JWT::urlsafeB64Decode($n);
242
        $exp = JWT::urlsafeB64Decode($e);
243
 
244
        $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
245
        $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
246
 
247
        $rsaPublicKey = \pack(
248
            'Ca*a*a*',
249
            48,
250
            self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
251
            $modulus,
252
            $publicExponent
253
        );
254
 
255
        // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
256
        $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
257
        $rsaPublicKey = \chr(0) . $rsaPublicKey;
258
        $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
259
 
260
        $rsaPublicKey = \pack(
261
            'Ca*a*',
262
            48,
263
            self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
264
            $rsaOID . $rsaPublicKey
265
        );
266
 
267
        return "-----BEGIN PUBLIC KEY-----\r\n" .
268
            \chunk_split(\base64_encode($rsaPublicKey), 64) .
269
            '-----END PUBLIC KEY-----';
270
    }
271
 
272
    /**
273
     * DER-encode the length
274
     *
275
     * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4.  See
276
     * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
277
     *
278
     * @param int $length
279
     * @return string
280
     */
281
    private static function encodeLength(int $length): string
282
    {
283
        if ($length <= 0x7F) {
284
            return \chr($length);
285
        }
286
 
287
        $temp = \ltrim(\pack('N', $length), \chr(0));
288
 
289
        return \pack('Ca*', 0x80 | \strlen($temp), $temp);
290
    }
291
 
292
    /**
293
     * Encodes a value into a DER object.
294
     * Also defined in Firebase\JWT\JWT
295
     *
296
     * @param   int     $type DER tag
297
     * @param   string  $value the value to encode
298
     * @return  string  the encoded object
299
     */
300
    private static function encodeDER(int $type, string $value): string
301
    {
302
        $tag_header = 0;
303
        if ($type === self::ASN1_SEQUENCE) {
304
            $tag_header |= 0x20;
305
        }
306
 
307
        // Type
308
        $der = \chr($tag_header | $type);
309
 
310
        // Length
311
        $der .= \chr(\strlen($value));
312
 
313
        return $der . $value;
314
    }
315
 
316
    /**
317
     * Encodes a string into a DER-encoded OID.
318
     *
319
     * @param   string $oid the OID string
320
     * @return  string the binary DER-encoded OID
321
     */
322
    private static function encodeOID(string $oid): string
323
    {
324
        $octets = explode('.', $oid);
325
 
326
        // Get the first octet
327
        $first = (int) array_shift($octets);
328
        $second = (int) array_shift($octets);
329
        $oid = \chr($first * 40 + $second);
330
 
331
        // Iterate over subsequent octets
332
        foreach ($octets as $octet) {
333
            if ($octet == 0) {
334
                $oid .= \chr(0x00);
335
                continue;
336
            }
337
            $bin = '';
338
 
339
            while ($octet) {
340
                $bin .= \chr(0x80 | ($octet & 0x7f));
341
                $octet >>= 7;
342
            }
343
            $bin[0] = $bin[0] & \chr(0x7f);
344
 
345
            // Convert to big endian if necessary
346
            if (pack('V', 65534) == pack('L', 65534)) {
347
                $oid .= strrev($bin);
348
            } else {
349
                $oid .= $bin;
350
            }
351
        }
352
 
353
        return $oid;
354
    }
355
}