PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 4.0.0
WP 2FA – Two-factor authentication for WordPress v4.0.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / classes / Admin / Methods / passkeys / class-authenticator-data.php
wp-2fa / includes / classes / Admin / Methods / passkeys Last commit date
assets 23 hours ago format 23 hours ago helpers 23 hours ago class-ajax-passkeys.php 23 hours ago class-api-register.php 23 hours ago class-api-signin.php 23 hours ago class-attestation-object.php 23 hours ago class-authenticate-server.php 23 hours ago class-authenticator-data.php 23 hours ago class-byte-buffer.php 23 hours ago class-chor-decoder.php 23 hours ago class-passkeys-endpoints.php 23 hours ago class-passkeys-user-profile.php 23 hours ago class-passkeys-wizard-steps.php 23 hours ago class-passkeys.php 23 hours ago class-pending-2fa-helper.php 23 hours ago class-source-repository.php 23 hours ago class-web-authn-exception.php 23 hours ago class-web-authn.php 23 hours ago index.php 23 hours ago
class-authenticator-data.php
604 lines
1 <?php
2
3 namespace WP2FA\Admin\Methods\passkeys;
4
5 use WP2FA\Methods\Passkeys\Byte_Buffer;
6 use WP2FA\Methods\Passkeys\Cbor_Decoder;
7 use WP2FA\Methods\Passkeys\Web_Authn_Exception;
8
9 /**
10 * Undocumented class
11 *
12 * @since 3.0.0
13 */
14 class Authenticator_Data {
15
16 /** @var string */
17 protected $_binary;
18 /** @var \stdClass */
19 protected $rp_id_hash;
20 /** @var \stdClass */
21 protected $_flags;
22 /** @var int */
23 protected $sign_count;
24 /** @var \stdClass */
25 protected $attested_credential_data;
26 /** @var array */
27 protected $_extensionData;
28 // Cose encoded keys.
29 private const COSE_KTY = 1;
30 private const COSE_ALG = 3;
31 // Cose curve.
32 private const COSE_CRV = -1;
33 private const COSE_X = -2;
34 private const COSE_Y = -3;
35 // Cose RSA PS256.
36 private const COSE_N = -1;
37 private const COSE_E = -2;
38 // EC2 key type.
39 private const EC2_TYPE = 2;
40 private const EC2_ES256 = -7;
41 private const EC2_P256 = 1;
42 // RSA key type.
43 private const RSA_TYPE = 3;
44 private const RSA_RS256 = -257;
45 // OKP key type.
46 private const OKP_TYPE = 1;
47 private const OKP_ED25519 = 6;
48 private const OKP_EDDSA = -8;
49
50 /**
51 * Parsing the Authenticator_Data binary.
52 *
53 * @param string $binary - Default comment.
54 *
55 * @throws Web_Authn_Exception - Default comment.
56 */
57 public function __construct( $binary ) {
58 if ( ! \is_string( $binary ) || \strlen( $binary ) < 37 ) {
59 throw new Web_Authn_Exception( 'Invalid Authenticator_Data input', Web_Authn_Exception::INVALID_DATA );
60 }
61 $this->_binary = $binary;
62
63 // Read infos from binary.
64 // https://www.w3.org/TR/Web_Authn/#sec-authenticator-data .
65
66 // RP ID.
67 $this->rp_id_hash = \substr( $binary, 0, 32 );
68
69 // flags (1 byte).
70 $flags = \unpack( 'Cflags', \substr( $binary, 32, 1 ) )['flags'];
71 $this->_flags = $this->_read_flags( $flags );
72
73 // signature counter: 32-bit unsigned big-endian integer.
74 $this->sign_count = \unpack( 'Nsigncount', \substr( $binary, 33, 4 ) )['signcount'];
75
76 $offset = 37;
77 // https://www.w3.org/TR/Web_Authn/#sec-attested-credential-data .
78 if ( $this->_flags->attested_data_included ) {
79 $this->attested_credential_data = $this->_read_attest_data( $binary, $offset );
80 }
81
82 if ( $this->_flags->extension_data_included ) {
83 $this->_read_extension_data( \substr( $binary, $offset ) );
84 }
85 }
86
87 /**
88 * Authenticator Attestation Globally Unique Identifier, a unique number
89 * that identifies the model of the authenticator (not the specific instance
90 * of the authenticator)
91 * The aaguid may be 0 if the user is using a old u2f device and/or if
92 * the browser is using the fido-u2f format.
93 *
94 * @return string
95 *
96 * @throws Web_Authn_Exception - Default comment.
97 */
98 public function get_aaguid() {
99 if ( ! ( $this->attested_credential_data instanceof \stdClass ) ) {
100 throw new Web_Authn_Exception( 'credential data not included in authenticator data', Web_Authn_Exception::INVALID_DATA );
101 }
102 return $this->attested_credential_data->aaguid;
103 }
104
105 /**
106 * Returns the Authenticator_Data as binary
107 *
108 * @return string
109 */
110 public function get_binary() {
111 return $this->_binary;
112 }
113
114 /**
115 * Returns the credential_id
116 *
117 * @return string
118 *
119 * @throws Web_Authn_Exception - Default comment.
120 */
121 public function get_credential_id() {
122 if ( ! ( $this->attested_credential_data instanceof \stdClass ) ) {
123 throw new Web_Authn_Exception( 'credential id not included in authenticator data', Web_Authn_Exception::INVALID_DATA );
124 }
125 return $this->attested_credential_data->credential_id;
126 }
127
128 /**
129 * Returns the public key in PEM format
130 *
131 * @return string
132 *
133 * @throws Web_Authn_Exception - Default comment.
134 */
135 public function get_public_key_pem() {
136 if ( ! ( $this->attested_credential_data instanceof \stdClass ) || ! isset( $this->attested_credential_data->credential_public_key ) ) {
137 throw new Web_Authn_Exception( 'credential data not included in authenticator data', Web_Authn_Exception::INVALID_DATA );
138 }
139
140 $der = null;
141 switch ( $this->attested_credential_data->credential_public_key->kty ?? null ) {
142 case self::EC2_TYPE:
143 $der = $this->_get_ec2_der();
144 break;
145 case self::RSA_TYPE:
146 $der = $this->_get_rsa_der();
147 break;
148 case self::OKP_TYPE:
149 $der = $this->_get_okp_der();
150 break;
151 default:
152 throw new Web_Authn_Exception( 'invalid key type', Web_Authn_Exception::INVALID_DATA );
153 }
154
155 $pem = '-----BEGIN PUBLIC KEY-----' . "\n";
156 $pem .= \chunk_split( \base64_encode( $der ), 64, "\n" );
157 $pem .= '-----END PUBLIC KEY-----' . "\n";
158 return $pem;
159 }
160
161 /**
162 * Returns the public key in U2F format
163 *
164 * @return string
165 *
166 * @throws Web_Authn_Exception - Default comment.
167 */
168 public function get_public_key_u2f() {
169 if ( ! ( $this->attested_credential_data instanceof \stdClass ) || ! isset( $this->attested_credential_data->credential_public_key ) ) {
170 throw new Web_Authn_Exception( 'credential data not included in authenticator data', Web_Authn_Exception::INVALID_DATA );
171 }
172 if ( ( $this->attested_credential_data->credential_public_key->kty ?? null ) !== self::EC2_TYPE ) {
173 throw new Web_Authn_Exception( 'signature algorithm not ES256', Web_Authn_Exception::INVALID_PUBLIC_KEY );
174 }
175 return "\x04" . // ECC uncompressed.
176 $this->attested_credential_data->credential_public_key->x .
177 $this->attested_credential_data->credential_public_key->y;
178 }
179
180 /**
181 * Returns the SHA256 hash of the relying party id (=hostname)
182 *
183 * @return string
184 */
185 public function get_rp_id_hash() {
186 return $this->rp_id_hash;
187 }
188
189 /**
190 * Returns the sign counter
191 *
192 * @return int
193 */
194 public function get_sign_count() {
195 return $this->sign_count;
196 }
197
198 /**
199 * Returns true if the user is present
200 *
201 * @return boolean
202 */
203 public function get_user_present() {
204 return $this->_flags->user_present;
205 }
206
207 /**
208 * Returns true if the user is verified
209 *
210 * @return boolean
211 */
212 public function get_user_verified() {
213 return $this->_flags->user_verified;
214 }
215
216 /**
217 * Returns true if the backup is eligible
218 *
219 * @return boolean
220 */
221 public function get_is_backup_eligible() {
222 return $this->_flags->is_backup_eligible;
223 }
224
225 /**
226 * Returns true if the current credential is backed up
227 *
228 * @return boolean
229 */
230 public function get_is_backup() {
231 return $this->_flags->is_backup;
232 }
233
234 // -----------------------------------------------
235 // PRIVATE
236 // -----------------------------------------------
237
238 /**
239 * Returns DER encoded EC2 key
240 *
241 * @return string
242 */
243 private function _get_ec2_der() {
244 return $this->_der_sequence(
245 $this->_der_sequence(
246 $this->_der_oid( "\x2A\x86\x48\xCE\x3D\x02\x01" ) . // OID 1.2.840.10045.2.1 ecPublicKey.
247 $this->_der_oid( "\x2A\x86\x48\xCE\x3D\x03\x01\x07" ) // 1.2.840.10045.3.1.7 prime256v1.
248 ) .
249 $this->_der_bit_string( $this->get_public_key_u2f() )
250 );
251 }
252
253 /**
254 * Returns DER encoded EdDSA key
255 *
256 * @return string
257 */
258 private function _get_okp_der() {
259 return $this->_der_sequence(
260 $this->_der_sequence(
261 $this->_der_oid( "\x2B\x65\x70" ) // OID 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm).
262 ) .
263 $this->_der_bit_string( $this->attested_credential_data->credential_public_key->x )
264 );
265 }
266
267 /**
268 * Returns DER encoded RSA key
269 *
270 * @return string
271 */
272 private function _get_rsa_der() {
273 return $this->_der_sequence(
274 $this->_der_sequence(
275 $this->_der_oid( "\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01" ) . // OID 1.2.840.113549.1.1.1 rsaEncryption.
276 $this->_der_null_value()
277 ) .
278 $this->_der_bit_string(
279 $this->_der_sequence(
280 $this->_der_unsigned_integer( $this->attested_credential_data->credential_public_key->n ) .
281 $this->_der_unsigned_integer( $this->attested_credential_data->credential_public_key->e )
282 )
283 )
284 );
285 }
286
287 /**
288 * Reads the flags from flag byte
289 *
290 * @param string $bin_flag - Default comment.
291 *
292 * @return \stdClass
293 */
294 private function _read_flags( $bin_flag ) {
295 $flags = new \stdClass();
296
297 $flags->bit_0 = ! ! ( $bin_flag & 1 );
298 $flags->bit_1 = ! ! ( $bin_flag & 2 );
299 $flags->bit_2 = ! ! ( $bin_flag & 4 );
300 $flags->bit_3 = ! ! ( $bin_flag & 8 );
301 $flags->bit_4 = ! ! ( $bin_flag & 16 );
302 $flags->bit_5 = ! ! ( $bin_flag & 32 );
303 $flags->bit_6 = ! ! ( $bin_flag & 64 );
304 $flags->bit_7 = ! ! ( $bin_flag & 128 );
305
306 // named flags.
307 $flags->user_present = $flags->bit_0;
308 $flags->user_verified = $flags->bit_2;
309 $flags->is_backup_eligible = $flags->bit_3;
310 $flags->is_backup = $flags->bit_4;
311 $flags->attested_data_included = $flags->bit_6;
312 $flags->extension_data_included = $flags->bit_7;
313 return $flags;
314 }
315
316 /**
317 * Read attested data
318 *
319 * @param string $binary - Default comment.
320 * @param int $end_offset - Default comment.
321 *
322 * @return \stdClass
323 *
324 * @throws Web_Authn_Exception - Default comment.
325 */
326 private function _read_attest_data( $binary, &$end_offset ) {
327 $attested_c_data = new \stdClass();
328 if ( \strlen( $binary ) <= 55 ) {
329 throw new Web_Authn_Exception( 'Attested data should be present but is missing', Web_Authn_Exception::INVALID_DATA );
330 }
331
332 // The AAGUID of the authenticator.
333 $attested_c_data->aaguid = \substr( $binary, 37, 16 );
334
335 // Byte length L of Credential ID, 16-bit unsigned big-endian integer.
336 $length = \unpack( 'nlength', \substr( $binary, 53, 2 ) )['length'];
337 $attested_c_data->credential_id = \substr( $binary, 55, $length );
338
339 // set end offset.
340 $end_offset = 55 + $length;
341
342 // extract public key.
343 $attested_c_data->credential_public_key = $this->_read_credential_public_key( $binary, 55 + $length, $end_offset );
344
345 return $attested_c_data;
346 }
347
348 /**
349 * Reads COSE key-encoded elliptic curve public key in EC2 format
350 *
351 * @param string $binary - Default comment.
352 * @param int $offset - Default comment.
353 * @param int $end_offset - Default comment.
354 *
355 * @return \stdClass
356 *
357 * @throws Web_Authn_Exception - Default comment.
358 */
359 private function _read_credential_public_key( $binary, $offset, &$end_offset ) {
360 $enc = Cbor_Decoder::decode_in_place( $binary, $offset, $end_offset );
361
362 // COSE key-encoded elliptic curve public key in EC2 format.
363 $cred_p_key = new \stdClass();
364 $cred_p_key->kty = $enc[ self::COSE_KTY ];
365 $cred_p_key->alg = $enc[ self::COSE_ALG ];
366
367 switch ( $cred_p_key->alg ) {
368 case self::EC2_ES256:
369 $this->_read_credential_public_key_es256( $cred_p_key, $enc );
370 break;
371 case self::RSA_RS256:
372 $this->_read_credential_public_key_rs256( $cred_p_key, $enc );
373 break;
374 case self::OKP_EDDSA:
375 $this->_read_credential_public_key_eddsa( $cred_p_key, $enc );
376 break;
377 }
378
379 return $cred_p_key;
380 }
381
382 /**
383 * Extract EDDSA informations from cose
384 *
385 * @param \stdClass $cred_p_key - Default comment.
386 * @param \stdClass $enc - Default comment.
387 *
388 * @throws Web_Authn_Exception - Default comment.
389 */
390 private function _read_credential_public_key_eddsa( &$cred_p_key, $enc ) {
391 $cred_p_key->crv = $enc[ self::COSE_CRV ];
392 $cred_p_key->x = $enc[ self::COSE_X ] instanceof Byte_Buffer ? $enc[ self::COSE_X ]->getBinaryString() : null;
393 unset( $enc );
394
395 // Validation.
396 if ( self::OKP_TYPE !== $cred_p_key->kty ) {
397 throw new Web_Authn_Exception( 'public key not in OKP format', Web_Authn_Exception::INVALID_PUBLIC_KEY );
398 }
399
400 if ( self::OKP_EDDSA !== $cred_p_key->alg ) {
401 throw new Web_Authn_Exception( 'signature algorithm not EdDSA', Web_Authn_Exception::INVALID_PUBLIC_KEY );
402 }
403
404 if ( self::OKP_ED25519 !== $cred_p_key->crv ) {
405 throw new Web_Authn_Exception( 'curve not Ed25519', Web_Authn_Exception::INVALID_PUBLIC_KEY );
406 }
407
408 if ( \strlen( $cred_p_key->x ) !== 32 ) {
409 throw new Web_Authn_Exception( 'Invalid X-coordinate', Web_Authn_Exception::INVALID_PUBLIC_KEY );
410 }
411 }
412
413 /**
414 * Extract ES256 information from cose
415 *
416 * @param \stdClass $cred_p_key - Default comment.
417 * @param \stdClass $enc - Default comment.
418 *
419 * @throws Web_Authn_Exception - Default comment.
420 */
421 private function _read_credential_public_key_es256( &$cred_p_key, $enc ) {
422 $cred_p_key->crv = $enc[ self::COSE_CRV ];
423 $cred_p_key->x = $enc[ self::COSE_X ] instanceof Byte_Buffer ? $enc[ self::COSE_X ]->getBinaryString() : null;
424 $cred_p_key->y = $enc[ self::COSE_Y ] instanceof Byte_Buffer ? $enc[ self::COSE_Y ]->getBinaryString() : null;
425 unset( $enc );
426
427 // Validation.
428 if ( self::EC2_TYPE !== $cred_p_key->kty ) {
429 throw new Web_Authn_Exception( 'public key not in EC2 format', Web_Authn_Exception::INVALID_PUBLIC_KEY );
430 }
431
432 if ( self::EC2_ES256 !== $cred_p_key->alg ) {
433 throw new Web_Authn_Exception( 'signature algorithm not ES256', Web_Authn_Exception::INVALID_PUBLIC_KEY );
434 }
435
436 if ( self::EC2_P256 !== $cred_p_key->crv ) {
437 throw new Web_Authn_Exception( 'curve not P-256', Web_Authn_Exception::INVALID_PUBLIC_KEY );
438 }
439
440 if ( \strlen( $cred_p_key->x ) !== 32 ) {
441 throw new Web_Authn_Exception( 'Invalid X-coordinate', Web_Authn_Exception::INVALID_PUBLIC_KEY );
442 }
443
444 if ( \strlen( $cred_p_key->y ) !== 32 ) {
445 throw new Web_Authn_Exception( 'Invalid Y-coordinate', Web_Authn_Exception::INVALID_PUBLIC_KEY );
446 }
447 }
448
449 /**
450 * Extract RS256 informations from COSE
451 *
452 * @param \stdClass $cred_p_key - Default comment.
453 * @param \stdClass $enc - Default comment.
454 *
455 * @throws Web_Authn_Exception - Default comment.
456 */
457 private function _read_credential_public_key_rs256( &$cred_p_key, $enc ) {
458 $cred_p_key->n = $enc[ self::COSE_N ] instanceof Byte_Buffer ? $enc[ self::COSE_N ]->getBinaryString() : null;
459 $cred_p_key->e = $enc[ self::COSE_E ] instanceof Byte_Buffer ? $enc[ self::COSE_E ]->getBinaryString() : null;
460 unset( $enc );
461
462 // Validation.
463 if ( self::RSA_TYPE !== $cred_p_key->kty ) {
464 throw new Web_Authn_Exception( 'public key not in RSA format', Web_Authn_Exception::INVALID_PUBLIC_KEY );
465 }
466
467 if ( self::RSA_RS256 !== $cred_p_key->alg ) {
468 throw new Web_Authn_Exception( 'signature algorithm not ES256', Web_Authn_Exception::INVALID_PUBLIC_KEY );
469 }
470
471 if ( \strlen( $cred_p_key->n ) !== 256 ) {
472 throw new Web_Authn_Exception( 'Invalid RSA modulus', Web_Authn_Exception::INVALID_PUBLIC_KEY );
473 }
474
475 if ( \strlen( $cred_p_key->e ) !== 3 ) {
476 throw new Web_Authn_Exception( 'Invalid RSA public exponent', Web_Authn_Exception::INVALID_PUBLIC_KEY );
477 }
478 }
479
480 /**
481 * Reads cbor encoded extension data.
482 *
483 * @param string $binary - Default comment.
484 *
485 * @return array
486 *
487 * @throws Web_Authn_Exception - Default comment.
488 */
489 private function _read_extension_data( $binary ) {
490 $ext = Cbor_Decoder::decode( $binary );
491 if ( ! \is_array( $ext ) ) {
492 throw new Web_Authn_Exception( 'invalid extension data', Web_Authn_Exception::INVALID_DATA );
493 }
494
495 return $ext;
496 }
497
498
499 // ---------------
500 // DER functions
501 // ---------------
502
503 /**
504 * Undocumented function
505 *
506 * @param [type] $len - Default comment.
507 *
508 * @return string
509 *
510 * @since 3.0.0
511 */
512 private function _der_length( $len ) {
513 if ( $len < 128 ) {
514 return \chr( $len );
515 }
516 $len_bytes = '';
517 while ( $len > 0 ) {
518 $len_bytes = \chr( $len % 256 ) . $len_bytes;
519 $len = \intdiv( $len, 256 );
520 }
521 return \chr( 0x80 | \strlen( $len_bytes ) ) . $len_bytes;
522 }
523
524 /**
525 * Undocumented function
526 *
527 * @param [type] $contents - Default comment.
528 *
529 * @return string
530 *
531 * @since 3.0.0
532 */
533 private function _der_sequence( $contents ) {
534 return "\x30" . $this->_der_length( \strlen( $contents ) ) . $contents;
535 }
536
537 /**
538 * Undocumented function
539 *
540 * @param [type] $encoded - Default comment.
541 *
542 * @return string
543 *
544 * @since 3.0.0
545 */
546 private function _der_oid( $encoded ) {
547 return "\x06" . $this->_der_length( \strlen( $encoded ) ) . $encoded;
548 }
549
550 /**
551 * Undocumented function
552 *
553 * @param [type] $bytes - Default comment.
554 *
555 * @return string
556 *
557 * @since 3.0.0
558 */
559 private function _der_bit_string( $bytes ) {
560 return "\x03" . $this->_der_length( \strlen( $bytes ) + 1 ) . "\x00" . $bytes;
561 }
562
563 /**
564 * Undocumented function
565 *
566 * @return string
567 *
568 * @since 3.0.0
569 */
570 private function _der_null_value() {
571 return "\x05\x00";
572 }
573
574 /**
575 * Undocumented function
576 *
577 * @param [type] $bytes - Default comment.
578 *
579 * @return string
580 *
581 * @since 3.0.0
582 */
583 private function _der_unsigned_integer( $bytes ) {
584 $len = \strlen( $bytes );
585
586 // Remove leading zero bytes.
587 for ( $i = 0; $i < ( $len - 1 ); $i++ ) {
588 if ( \ord( $bytes[ $i ] ) !== 0 ) {
589 break;
590 }
591 }
592 if ( 0 !== $i ) {
593 $bytes = \substr( $bytes, $i );
594 }
595
596 // If most significant bit is set, prefix with another zero to prevent it being seen as negative number.
597 if ( ( \ord( $bytes[0] ) & 0x80 ) !== 0 ) {
598 $bytes = "\x00" . $bytes;
599 }
600
601 return "\x02" . $this->_der_length( \strlen( $bytes ) ) . $bytes;
602 }
603 }
604