1<?php
2
3namespace Firebase\JWT;
4
5use DomainException;
6use InvalidArgumentException;
7use 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 */
21class JWK
22{
23    /**
24     * Parse a set of JWK keys
25     *
26     * @param array<mixed> $jwks The JSON Web Key Set as an associative array
27     *
28     * @return array<string, Key> An associative array of key IDs (kid) to Key objects
29     *
30     * @throws InvalidArgumentException     Provided JWK Set is empty
31     * @throws UnexpectedValueException     Provided JWK Set was invalid
32     * @throws DomainException              OpenSSL failure
33     *
34     * @uses parseKey
35     */
36    public static function parseKeySet(array $jwks): array
37    {
38        $keys = [];
39
40        if (!isset($jwks['keys'])) {
41            throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
42        }
43
44        if (empty($jwks['keys'])) {
45            throw new InvalidArgumentException('JWK Set did not contain any keys');
46        }
47
48        foreach ($jwks['keys'] as $k => $v) {
49            $kid = isset($v['kid']) ? $v['kid'] : $k;
50            if ($key = self::parseKey($v)) {
51                $keys[(string) $kid] = $key;
52            }
53        }
54
55        if (0 === \count($keys)) {
56            throw new UnexpectedValueException('No supported algorithms found in JWK Set');
57        }
58
59        return $keys;
60    }
61
62    /**
63     * Parse a JWK key
64     *
65     * @param array<mixed> $jwk An individual JWK
66     *
67     * @return Key The key object for the JWK
68     *
69     * @throws InvalidArgumentException     Provided JWK is empty
70     * @throws UnexpectedValueException     Provided JWK was invalid
71     * @throws DomainException              OpenSSL failure
72     *
73     * @uses createPemFromModulusAndExponent
74     */
75    public static function parseKey(array $jwk): ?Key
76    {
77        if (empty($jwk)) {
78            throw new InvalidArgumentException('JWK must not be empty');
79        }
80
81        if (!isset($jwk['kty'])) {
82            throw new UnexpectedValueException('JWK must contain a "kty" parameter');
83        }
84
85        if (!isset($jwk['alg'])) {
86            // The "alg" parameter is optional in a KTY, but is required for parsing in
87            // this library. Add it manually to your JWK array if it doesn't already exist.
88            // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
89            throw new UnexpectedValueException('JWK must contain an "alg" parameter');
90        }
91
92        switch ($jwk['kty']) {
93            case 'RSA':
94                if (!empty($jwk['d'])) {
95                    throw new UnexpectedValueException('RSA private keys are not supported');
96                }
97                if (!isset($jwk['n']) || !isset($jwk['e'])) {
98                    throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
99                }
100
101                $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
102                $publicKey = \openssl_pkey_get_public($pem);
103                if (false === $publicKey) {
104                    throw new DomainException(
105                        'OpenSSL error: ' . \openssl_error_string()
106                    );
107                }
108                return new Key($publicKey, $jwk['alg']);
109            default:
110                // Currently only RSA is supported
111                break;
112        }
113
114        return null;
115    }
116
117    /**
118     * Create a public key represented in PEM format from RSA modulus and exponent information
119     *
120     * @param string $n The RSA modulus encoded in Base64
121     * @param string $e The RSA exponent encoded in Base64
122     *
123     * @return string The RSA public key represented in PEM format
124     *
125     * @uses encodeLength
126     */
127    private static function createPemFromModulusAndExponent(
128        string $n,
129        string $e
130    ): string {
131        $mod = JWT::urlsafeB64Decode($n);
132        $exp = JWT::urlsafeB64Decode($e);
133
134        $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
135        $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
136
137        $rsaPublicKey = \pack(
138            'Ca*a*a*',
139            48,
140            self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
141            $modulus,
142            $publicExponent
143        );
144
145        // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
146        $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
147        $rsaPublicKey = \chr(0) . $rsaPublicKey;
148        $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
149
150        $rsaPublicKey = \pack(
151            'Ca*a*',
152            48,
153            self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
154            $rsaOID . $rsaPublicKey
155        );
156
157        $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" .
158            \chunk_split(\base64_encode($rsaPublicKey), 64) .
159            '-----END PUBLIC KEY-----';
160
161        return $rsaPublicKey;
162    }
163
164    /**
165     * DER-encode the length
166     *
167     * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4.  See
168     * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
169     *
170     * @param int $length
171     * @return string
172     */
173    private static function encodeLength(int $length): string
174    {
175        if ($length <= 0x7F) {
176            return \chr($length);
177        }
178
179        $temp = \ltrim(\pack('N', $length), \chr(0));
180
181        return \pack('Ca*', 0x80 | \strlen($temp), $temp);
182    }
183}
184