20
20
*/
21
21
class JWK
22
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
+ // 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported)
30
+ // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
31
+ ];
32
+
23
33
/**
24
34
* Parse a set of JWK keys
25
35
*
@@ -114,6 +124,26 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key
114
124
);
115
125
}
116
126
return new Key ($ publicKey , $ jwk ['alg ' ]);
127
+ case 'EC ' :
128
+ if (isset ($ jwk ['d ' ])) {
129
+ // The key is actually a private key
130
+ throw new UnexpectedValueException ('Key data must be for a public key ' );
131
+ }
132
+
133
+ if (empty ($ jwk ['crv ' ])) {
134
+ throw new UnexpectedValueException ('crv not set ' );
135
+ }
136
+
137
+ if (!isset (self ::EC_CURVES [$ jwk ['crv ' ]])) {
138
+ throw new DomainException ('Unrecognised or unsupported EC curve ' );
139
+ }
140
+
141
+ if (empty ($ jwk ['x ' ]) || empty ($ jwk ['y ' ])) {
142
+ throw new UnexpectedValueException ('x and y not set ' );
143
+ }
144
+
145
+ $ publicKey = self ::createPemFromCrvAndXYCoordinates ($ jwk ['crv ' ], $ jwk ['x ' ], $ jwk ['y ' ]);
146
+ return new Key ($ publicKey , $ jwk ['alg ' ]);
117
147
default :
118
148
// Currently only RSA is supported
119
149
break ;
@@ -122,6 +152,45 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key
122
152
return null ;
123
153
}
124
154
155
+ /**
156
+ * Converts the EC JWK values to pem format.
157
+ *
158
+ * @param string $crv The EC curve (only P-256 is supported)
159
+ * @param string $x The EC x-coordinate
160
+ * @param string $y The EC y-coordinate
161
+ *
162
+ * @return string
163
+ */
164
+ private static function createPemFromCrvAndXYCoordinates (string $ crv , string $ x , string $ y ): string
165
+ {
166
+ $ pem =
167
+ self ::encodeDER (
168
+ self ::ASN1_SEQUENCE ,
169
+ self ::encodeDER (
170
+ self ::ASN1_SEQUENCE ,
171
+ self ::encodeDER (
172
+ self ::ASN1_OBJECT_IDENTIFIER ,
173
+ self ::encodeOID (self ::OID )
174
+ )
175
+ . self ::encodeDER (
176
+ self ::ASN1_OBJECT_IDENTIFIER ,
177
+ self ::encodeOID (self ::EC_CURVES [$ crv ])
178
+ )
179
+ ) .
180
+ self ::encodeDER (
181
+ self ::ASN1_BIT_STRING ,
182
+ chr (0x00 ) . chr (0x04 )
183
+ . JWT ::urlsafeB64Decode ($ x )
184
+ . JWT ::urlsafeB64Decode ($ y )
185
+ )
186
+ );
187
+
188
+ return sprintf (
189
+ "-----BEGIN PUBLIC KEY----- \n%s \n-----END PUBLIC KEY----- \n" ,
190
+ wordwrap (base64_encode ($ pem ), 64 , "\n" , true )
191
+ );
192
+ }
193
+
125
194
/**
126
195
* Create a public key represented in PEM format from RSA modulus and exponent information
127
196
*
@@ -188,4 +257,68 @@ private static function encodeLength(int $length): string
188
257
189
258
return \pack ('Ca* ' , 0x80 | \strlen ($ temp ), $ temp );
190
259
}
260
+
261
+ /**
262
+ * Encodes a value into a DER object.
263
+ * Also defined in Firebase\JWT\JWT
264
+ *
265
+ * @param int $type DER tag
266
+ * @param string $value the value to encode
267
+ * @return string the encoded object
268
+ */
269
+ private static function encodeDER (int $ type , string $ value ): string
270
+ {
271
+ $ tag_header = 0 ;
272
+ if ($ type === self ::ASN1_SEQUENCE ) {
273
+ $ tag_header |= 0x20 ;
274
+ }
275
+
276
+ // Type
277
+ $ der = \chr ($ tag_header | $ type );
278
+
279
+ // Length
280
+ $ der .= \chr (\strlen ($ value ));
281
+
282
+ return $ der . $ value ;
283
+ }
284
+
285
+ /**
286
+ * Encodes a string into a DER-encoded OID.
287
+ *
288
+ * @param string $oid the OID string
289
+ * @return string the binary DER-encoded OID
290
+ */
291
+ private static function encodeOID (string $ oid ): string
292
+ {
293
+ $ octets = explode ('. ' , $ oid );
294
+
295
+ // Get the first octet
296
+ $ first = (int ) array_shift ($ octets );
297
+ $ second = (int ) array_shift ($ octets );
298
+ $ oid = chr ($ first * 40 + $ second );
299
+
300
+ // Iterate over subsequent octets
301
+ foreach ($ octets as $ octet ) {
302
+ if ($ octet == 0 ) {
303
+ $ oid .= chr (0x00 );
304
+ continue ;
305
+ }
306
+ $ bin = '' ;
307
+
308
+ while ($ octet ) {
309
+ $ bin .= chr (0x80 | ($ octet & 0x7f ));
310
+ $ octet >>= 7 ;
311
+ }
312
+ $ bin [0 ] = $ bin [0 ] & chr (0x7f );
313
+
314
+ // Convert to big endian if necessary
315
+ if (pack ('V ' , 65534 ) == pack ('L ' , 65534 )) {
316
+ $ oid .= strrev ($ bin );
317
+ } else {
318
+ $ oid .= $ bin ;
319
+ }
320
+ }
321
+
322
+ return $ oid ;
323
+ }
191
324
}
0 commit comments