|
20 | 20 | */
|
21 | 21 | class JWK
|
22 | 22 | {
|
| 23 | + private static $oid = '1.2.840.10045.2.1'; |
| 24 | + private static $asn1ObjectIdentifier = 0x06; |
| 25 | + private static $asn1Integer = 0x02; // also defined in JWT |
| 26 | + private static $asn1Sequence = 0x10; // also defined in JWT |
| 27 | + private static $asn1BitString = 0x03; |
| 28 | + private static $curves = [ |
| 29 | + 'P-256' => '1.2.840.10045.3.1.7', // Len: 64 |
| 30 | + // 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported) |
| 31 | + // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) |
| 32 | + ]; |
| 33 | + |
23 | 34 | /**
|
24 | 35 | * Parse a set of JWK keys
|
25 | 36 | *
|
@@ -103,12 +114,132 @@ public static function parseKey(array $jwk)
|
103 | 114 | );
|
104 | 115 | }
|
105 | 116 | return new Key($publicKey, $jwk['alg']);
|
| 117 | + case 'EC': |
| 118 | + if (isset($jwk['d'])) { |
| 119 | + // The key is actually a private key |
| 120 | + throw new UnexpectedValueException('Key data must be for a public key'); |
| 121 | + } |
| 122 | + |
| 123 | + if (empty($jwk['crv'])) { |
| 124 | + throw new UnexpectedValueException('crv not set'); |
| 125 | + } |
| 126 | + |
| 127 | + if (!isset(self::$curves[$jwk['crv']])) { |
| 128 | + throw new DomainException('Unrecognised or unsupported EC curve'); |
| 129 | + } |
| 130 | + |
| 131 | + if (empty($jwk['x']) || empty($jwk['y'])) { |
| 132 | + throw new UnexpectedValueException('x and y not set'); |
| 133 | + } |
| 134 | + |
| 135 | + $oid = self::$curves[$jwk['crv']]; |
| 136 | + $publicKey = self::ecJwkToPem($oid, $jwk['x'], $jwk['y']); |
| 137 | + return new Key($publicKey, $jwk['alg']); |
106 | 138 | default:
|
107 | 139 | // Currently only RSA is supported
|
108 | 140 | break;
|
109 | 141 | }
|
110 | 142 | }
|
111 | 143 |
|
| 144 | + /** |
| 145 | + * Encodes a string into a DER-encoded OID. |
| 146 | + * |
| 147 | + * @param string $oid the OID string |
| 148 | + * @return string the binary DER-encoded OID |
| 149 | + */ |
| 150 | + private static function encodeOID($oid) |
| 151 | + { |
| 152 | + $octets = explode('.', $oid); |
| 153 | + |
| 154 | + // Get the first octet |
| 155 | + $oid = chr(array_shift($octets) * 40 + array_shift($octets)); |
| 156 | + |
| 157 | + // Iterate over subsequent octets |
| 158 | + foreach ($octets as $octet) { |
| 159 | + if ($octet == 0) { |
| 160 | + $oid .= chr(0x00); |
| 161 | + continue; |
| 162 | + } |
| 163 | + $bin = ''; |
| 164 | + |
| 165 | + while ($octet) { |
| 166 | + $bin .= chr(0x80 | ($octet & 0x7f)); |
| 167 | + $octet >>= 7; |
| 168 | + } |
| 169 | + $bin[0] = $bin[0] & chr(0x7f); |
| 170 | + |
| 171 | + // Convert to big endian if necessary |
| 172 | + if (pack('V', 65534) == pack('L', 65534)) { |
| 173 | + $oid .= strrev($bin); |
| 174 | + } else { |
| 175 | + $oid .= $bin; |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + return $oid; |
| 180 | + } |
| 181 | + |
| 182 | + /** |
| 183 | + * Converts the EC JWK values to pem format. |
| 184 | + * |
| 185 | + * @param string $oid the OID string |
| 186 | + * @param string $x |
| 187 | + * @return string $y |
| 188 | + */ |
| 189 | + private static function ecJwkToPem($oid, $x, $y) |
| 190 | + { |
| 191 | + $pem = |
| 192 | + self::encodeDER( |
| 193 | + self::$asn1Sequence, |
| 194 | + self::encodeDER( |
| 195 | + self::$asn1Sequence, |
| 196 | + self::encodeDER( |
| 197 | + self::$asn1ObjectIdentifier, |
| 198 | + self::encodeOID(self::$oid) |
| 199 | + ) |
| 200 | + . self::encodeDER( |
| 201 | + self::$asn1ObjectIdentifier, |
| 202 | + self::encodeOID($oid) |
| 203 | + ) |
| 204 | + ) . |
| 205 | + self::encodeDER( |
| 206 | + self::$asn1BitString, |
| 207 | + chr(0x00) . chr(0x04) |
| 208 | + . JWT::urlsafeB64Decode($x) |
| 209 | + . JWT::urlsafeB64Decode($y) |
| 210 | + ) |
| 211 | + ); |
| 212 | + |
| 213 | + return sprintf( |
| 214 | + "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", |
| 215 | + wordwrap(base64_encode($pem), 64, "\n", true) |
| 216 | + ); |
| 217 | + } |
| 218 | + |
| 219 | + /** |
| 220 | + * Encodes a value into a DER object. |
| 221 | + * Also defined in Firebase\JWT\JWT |
| 222 | + * |
| 223 | + * @param int $type DER tag |
| 224 | + * @param string $value the value to encode |
| 225 | + * @return string the encoded object |
| 226 | + */ |
| 227 | + private static function encodeDER($type, $value) |
| 228 | + { |
| 229 | + $tag_header = 0; |
| 230 | + if ($type === self::$asn1Sequence) { |
| 231 | + $tag_header |= 0x20; |
| 232 | + } |
| 233 | + |
| 234 | + // Type |
| 235 | + $der = \chr($tag_header | $type); |
| 236 | + |
| 237 | + // Length |
| 238 | + $der .= \chr(\strlen($value)); |
| 239 | + |
| 240 | + return $der . $value; |
| 241 | + } |
| 242 | + |
112 | 243 | /**
|
113 | 244 | * Create a public key represented in PEM format from RSA modulus and exponent information
|
114 | 245 | *
|
|
0 commit comments