2020 */
2121class JWK
2222{
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+
2333 /**
2434 * Parse a set of JWK keys
2535 *
@@ -114,6 +124,26 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key
114124 );
115125 }
116126 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 ' ]);
117147 default :
118148 // Currently only RSA is supported
119149 break ;
@@ -122,6 +152,45 @@ public static function parseKey(array $jwk, string $defaultAlg = null): ?Key
122152 return null ;
123153 }
124154
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+
125194 /**
126195 * Create a public key represented in PEM format from RSA modulus and exponent information
127196 *
@@ -188,4 +257,68 @@ private static function encodeLength(int $length): string
188257
189258 return \pack ('Ca* ' , 0x80 | \strlen ($ temp ), $ temp );
190259 }
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+ }
191324}
0 commit comments