Proyectos de Subversion Moodle

Rev

| 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
     */
55
    public static function parseKeySet(array $jwks, string $defaultAlg = null): array
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
     */
96
    public static function parseKey(array $jwk, string $defaultAlg = null): ?Key
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']);
175
            default:
176
                break;
177
        }
178
 
179
        return null;
180
    }
181
 
182
    /**
183
     * Converts the EC JWK values to pem format.
184
     *
185
     * @param   string  $crv The EC curve (only P-256 & P-384 is supported)
186
     * @param   string  $x   The EC x-coordinate
187
     * @param   string  $y   The EC y-coordinate
188
     *
189
     * @return  string
190
     */
191
    private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
192
    {
193
        $pem =
194
            self::encodeDER(
195
                self::ASN1_SEQUENCE,
196
                self::encodeDER(
197
                    self::ASN1_SEQUENCE,
198
                    self::encodeDER(
199
                        self::ASN1_OBJECT_IDENTIFIER,
200
                        self::encodeOID(self::OID)
201
                    )
202
                    . self::encodeDER(
203
                        self::ASN1_OBJECT_IDENTIFIER,
204
                        self::encodeOID(self::EC_CURVES[$crv])
205
                    )
206
                ) .
207
                self::encodeDER(
208
                    self::ASN1_BIT_STRING,
209
                    \chr(0x00) . \chr(0x04)
210
                    . JWT::urlsafeB64Decode($x)
211
                    . JWT::urlsafeB64Decode($y)
212
                )
213
            );
214
 
215
        return sprintf(
216
            "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
217
            wordwrap(base64_encode($pem), 64, "\n", true)
218
        );
219
    }
220
 
221
    /**
222
     * Create a public key represented in PEM format from RSA modulus and exponent information
223
     *
224
     * @param string $n The RSA modulus encoded in Base64
225
     * @param string $e The RSA exponent encoded in Base64
226
     *
227
     * @return string The RSA public key represented in PEM format
228
     *
229
     * @uses encodeLength
230
     */
231
    private static function createPemFromModulusAndExponent(
232
        string $n,
233
        string $e
234
    ): string {
235
        $mod = JWT::urlsafeB64Decode($n);
236
        $exp = JWT::urlsafeB64Decode($e);
237
 
238
        $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
239
        $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
240
 
241
        $rsaPublicKey = \pack(
242
            'Ca*a*a*',
243
            48,
244
            self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
245
            $modulus,
246
            $publicExponent
247
        );
248
 
249
        // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
250
        $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
251
        $rsaPublicKey = \chr(0) . $rsaPublicKey;
252
        $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
253
 
254
        $rsaPublicKey = \pack(
255
            'Ca*a*',
256
            48,
257
            self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
258
            $rsaOID . $rsaPublicKey
259
        );
260
 
261
        return "-----BEGIN PUBLIC KEY-----\r\n" .
262
            \chunk_split(\base64_encode($rsaPublicKey), 64) .
263
            '-----END PUBLIC KEY-----';
264
    }
265
 
266
    /**
267
     * DER-encode the length
268
     *
269
     * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4.  See
270
     * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
271
     *
272
     * @param int $length
273
     * @return string
274
     */
275
    private static function encodeLength(int $length): string
276
    {
277
        if ($length <= 0x7F) {
278
            return \chr($length);
279
        }
280
 
281
        $temp = \ltrim(\pack('N', $length), \chr(0));
282
 
283
        return \pack('Ca*', 0x80 | \strlen($temp), $temp);
284
    }
285
 
286
    /**
287
     * Encodes a value into a DER object.
288
     * Also defined in Firebase\JWT\JWT
289
     *
290
     * @param   int     $type DER tag
291
     * @param   string  $value the value to encode
292
     * @return  string  the encoded object
293
     */
294
    private static function encodeDER(int $type, string $value): string
295
    {
296
        $tag_header = 0;
297
        if ($type === self::ASN1_SEQUENCE) {
298
            $tag_header |= 0x20;
299
        }
300
 
301
        // Type
302
        $der = \chr($tag_header | $type);
303
 
304
        // Length
305
        $der .= \chr(\strlen($value));
306
 
307
        return $der . $value;
308
    }
309
 
310
    /**
311
     * Encodes a string into a DER-encoded OID.
312
     *
313
     * @param   string $oid the OID string
314
     * @return  string the binary DER-encoded OID
315
     */
316
    private static function encodeOID(string $oid): string
317
    {
318
        $octets = explode('.', $oid);
319
 
320
        // Get the first octet
321
        $first = (int) array_shift($octets);
322
        $second = (int) array_shift($octets);
323
        $oid = \chr($first * 40 + $second);
324
 
325
        // Iterate over subsequent octets
326
        foreach ($octets as $octet) {
327
            if ($octet == 0) {
328
                $oid .= \chr(0x00);
329
                continue;
330
            }
331
            $bin = '';
332
 
333
            while ($octet) {
334
                $bin .= \chr(0x80 | ($octet & 0x7f));
335
                $octet >>= 7;
336
            }
337
            $bin[0] = $bin[0] & \chr(0x7f);
338
 
339
            // Convert to big endian if necessary
340
            if (pack('V', 65534) == pack('L', 65534)) {
341
                $oid .= strrev($bin);
342
            } else {
343
                $oid .= $bin;
344
            }
345
        }
346
 
347
        return $oid;
348
    }
349
}