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-attestation-object.php
283 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Responsible for the proper attestation object extracting |
| 4 | * |
| 5 | * @package wp-2fa |
| 6 | * @since 3.0.0 |
| 7 | * @copyright 2026 Melapress |
| 8 | * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 |
| 9 | * @link https://wordpress.org/plugins/wp-2fa/ |
| 10 | */ |
| 11 | |
| 12 | declare(strict_types=1); |
| 13 | |
| 14 | namespace WP2FA\Methods\Passkeys; |
| 15 | |
| 16 | use WP2FA\Passkeys\Format\Tpm; |
| 17 | use WP2FA\Passkeys\Format\U2f; |
| 18 | use WP2FA\Passkeys\Format\None; |
| 19 | use WP2FA\Passkeys\Format\Apple; |
| 20 | use WP2FA\Passkeys\Format\Packed; |
| 21 | use WP2FA\Passkeys\Format\Android_Key; |
| 22 | use WP2FA\Methods\Passkeys\Cbor_Decoder; |
| 23 | use WP2FA\Passkeys\Format\Android_Safety_Net; |
| 24 | use WP2FA\Admin\Methods\passkeys\Authenticator_Data; |
| 25 | |
| 26 | defined( 'ABSPATH' ) || exit; // Exit if accessed directly. |
| 27 | |
| 28 | if ( ! class_exists( '\WP2FA\Methods\Passkeys\Attestation_Object' ) ) { |
| 29 | |
| 30 | /** |
| 31 | * Attestation object class |
| 32 | * |
| 33 | * @since 3.0.0 |
| 34 | */ |
| 35 | class Attestation_Object { |
| 36 | |
| 37 | /** |
| 38 | * Data passed from the Authenticator. |
| 39 | * |
| 40 | * @var Authenticator_Data |
| 41 | * |
| 42 | * @since 3.0.0 |
| 43 | */ |
| 44 | private $authenticator_data; |
| 45 | |
| 46 | /** |
| 47 | * Format of the authenticator |
| 48 | * |
| 49 | * @var Format_Base |
| 50 | * |
| 51 | * @since 3.0.0 |
| 52 | */ |
| 53 | private $attestation_format; |
| 54 | |
| 55 | /** |
| 56 | * Format name |
| 57 | * |
| 58 | * @var string |
| 59 | * |
| 60 | * @since 3.0.0 |
| 61 | */ |
| 62 | private $attestation_format_name; |
| 63 | |
| 64 | /** |
| 65 | * Default constructor |
| 66 | * |
| 67 | * @param string $binary - The binary string. |
| 68 | * @param array $allowed_formats - Allowed formats. |
| 69 | * |
| 70 | * @since 3.0.0 |
| 71 | * |
| 72 | * @throws Web_Authn_Exception - when format is invalid. |
| 73 | */ |
| 74 | public function __construct( $binary, $allowed_formats ) { |
| 75 | $enc = Cbor_Decoder::decode( $binary ); |
| 76 | // validation. |
| 77 | if ( ! \is_array( $enc ) || ! \array_key_exists( 'fmt', $enc ) || ! is_string( $enc['fmt'] ) ) { |
| 78 | throw new Web_Authn_Exception( 'invalid attestation format', Web_Authn_Exception::INVALID_DATA ); |
| 79 | } |
| 80 | |
| 81 | if ( ! \array_key_exists( 'attStmt', $enc ) || ! \is_array( $enc['attStmt'] ) ) { |
| 82 | throw new Web_Authn_Exception( 'invalid attestation format (attStmt not available)', Web_Authn_Exception::INVALID_DATA ); |
| 83 | } |
| 84 | |
| 85 | if ( ! \array_key_exists( 'authData', $enc ) || ! \is_object( $enc['authData'] ) || ! ( $enc['authData'] instanceof Byte_Buffer ) ) { |
| 86 | throw new Web_Authn_Exception( 'invalid attestation format (authData not available)', Web_Authn_Exception::INVALID_DATA ); |
| 87 | } |
| 88 | |
| 89 | $this->authenticator_data = new Authenticator_Data( $enc['authData']->getBinaryString() ); |
| 90 | $this->attestation_format_name = $enc['fmt']; |
| 91 | |
| 92 | // Format ok? |
| 93 | if ( ! in_array( $this->attestation_format_name, $allowed_formats ) ) { |
| 94 | throw new Web_Authn_Exception( 'invalid atttestation format: ' . $this->attestation_format_name, Web_Authn_Exception::INVALID_DATA ); |
| 95 | } |
| 96 | |
| 97 | switch ( $this->attestation_format_name ) { |
| 98 | case 'android-key': |
| 99 | $this->attestation_format = new Android_Key( $enc, $this->authenticator_data ); |
| 100 | break; |
| 101 | case 'android-safetynet': |
| 102 | $this->attestation_format = new Android_Safety_Net( $enc, $this->authenticator_data ); |
| 103 | break; |
| 104 | case 'apple': |
| 105 | $this->attestation_format = new Apple( $enc, $this->authenticator_data ); |
| 106 | break; |
| 107 | case 'fido-u2f': |
| 108 | $this->attestation_format = new U2f( $enc, $this->authenticator_data ); |
| 109 | break; |
| 110 | case 'none': |
| 111 | $this->attestation_format = new None( $enc, $this->authenticator_data ); |
| 112 | break; |
| 113 | case 'packed': |
| 114 | $this->attestation_format = new Packed( $enc, $this->authenticator_data ); |
| 115 | break; |
| 116 | case 'tpm': |
| 117 | $this->attestation_format = new Tpm( $enc, $this->authenticator_data ); |
| 118 | break; |
| 119 | default: |
| 120 | throw new Web_Authn_Exception( 'invalid attestation format: ' . $enc['fmt'], Web_Authn_Exception::INVALID_DATA ); |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Returns the attestation format name |
| 126 | * |
| 127 | * @return string |
| 128 | * |
| 129 | * @since 3.0.0 |
| 130 | */ |
| 131 | public function get_attestation_format_name() { |
| 132 | return $this->attestation_format_name; |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Returns the attestation format class |
| 137 | * |
| 138 | * @return Format\Format_Base |
| 139 | * |
| 140 | * @since 3.0.0 |
| 141 | */ |
| 142 | public function get_attestation_format() { |
| 143 | return $this->attestation_format; |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * Returns the attestation public key in PEM format |
| 148 | * |
| 149 | * @return Authenticator_Data |
| 150 | * |
| 151 | * @since 3.0.0 |
| 152 | */ |
| 153 | public function get_authenticator_data() { |
| 154 | return $this->authenticator_data; |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * Returns the certificate chain as PEM |
| 159 | * |
| 160 | * @return string|null |
| 161 | * |
| 162 | * @since 3.0.0 |
| 163 | */ |
| 164 | public function get_certificate_chain() { |
| 165 | return $this->attestation_format->get_certificate_chain(); |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * Returns the certificate issuer as string |
| 170 | * |
| 171 | * @return string |
| 172 | * |
| 173 | * @since 3.0.0 |
| 174 | */ |
| 175 | public function get_certificate_issuer() { |
| 176 | $pem = $this->get_certificate_pem(); |
| 177 | $issuer = ''; |
| 178 | if ( $pem ) { |
| 179 | $cert_info = \openssl_x509_parse( $pem ); |
| 180 | if ( \is_array( $cert_info ) && \array_key_exists( 'issuer', $cert_info ) && \is_array( $cert_info['issuer'] ) ) { |
| 181 | |
| 182 | $cn = $cert_info['issuer']['CN'] ?? ''; |
| 183 | $o = $cert_info['issuer']['O'] ?? ''; |
| 184 | $ou = $cert_info['issuer']['OU'] ?? ''; |
| 185 | |
| 186 | if ( $cn ) { |
| 187 | $issuer .= $cn; |
| 188 | } |
| 189 | if ( $issuer && ( $o || $ou ) ) { |
| 190 | $issuer .= ' (' . trim( $o . ' ' . $ou ) . ')'; |
| 191 | } else { |
| 192 | $issuer .= trim( $o . ' ' . $ou ); |
| 193 | } |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | return $issuer; |
| 198 | } |
| 199 | |
| 200 | /** |
| 201 | * Returns the certificate subject as string |
| 202 | * |
| 203 | * @return string |
| 204 | * |
| 205 | * @since 3.0.0 |
| 206 | */ |
| 207 | public function get_certificate_subject() { |
| 208 | $pem = $this->get_certificate_pem(); |
| 209 | $subject = ''; |
| 210 | if ( $pem ) { |
| 211 | $cert_info = \openssl_x509_parse( $pem ); |
| 212 | if ( \is_array( $cert_info ) && \array_key_exists( 'subject', $cert_info ) && \is_array( $cert_info['subject'] ) ) { |
| 213 | |
| 214 | $cn = $cert_info['subject']['CN'] ?? ''; |
| 215 | $o = $cert_info['subject']['O'] ?? ''; |
| 216 | $ou = $cert_info['subject']['OU'] ?? ''; |
| 217 | |
| 218 | if ( $cn ) { |
| 219 | $subject .= $cn; |
| 220 | } |
| 221 | if ( $subject && ( $o || $ou ) ) { |
| 222 | $subject .= ' (' . trim( $o . ' ' . $ou ) . ')'; |
| 223 | } else { |
| 224 | $subject .= trim( $o . ' ' . $ou ); |
| 225 | } |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | return $subject; |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Returns the key certificate in PEM format |
| 234 | * |
| 235 | * @return string |
| 236 | * |
| 237 | * @since 3.0.0 |
| 238 | */ |
| 239 | public function get_certificate_pem() { |
| 240 | return $this->attestation_format->get_certificate_pem(); |
| 241 | } |
| 242 | |
| 243 | /** |
| 244 | * Checks validity of the signature |
| 245 | * |
| 246 | * @param string $client_data_hash - The data from the client. |
| 247 | * |
| 248 | * @return bool |
| 249 | * |
| 250 | * @since 3.0.0 |
| 251 | */ |
| 252 | public function validate_attestation( $client_data_hash ) { |
| 253 | return $this->attestation_format->validate_attestation( $client_data_hash ); |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * Validates the certificate against root certificates |
| 258 | * |
| 259 | * @param array $root_cas - Root certificate. |
| 260 | * |
| 261 | * @return boolean |
| 262 | * |
| 263 | * @since 3.0.0 |
| 264 | */ |
| 265 | public function validate_root_certificate( $root_cas ) { |
| 266 | return $this->attestation_format->validate_root_certificate( $root_cas ); |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Checks if the RpId-Hash is valid |
| 271 | * |
| 272 | * @param string $rp_id_hash - Hash string. |
| 273 | * |
| 274 | * @return bool |
| 275 | * |
| 276 | * @since 3.0.0 |
| 277 | */ |
| 278 | public function validate_rp_id_hash( $rp_id_hash ) { |
| 279 | return $rp_id_hash === $this->authenticator_data->get_rp_id_hash(); |
| 280 | } |
| 281 | } |
| 282 | } |
| 283 |