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 / format / apple.php
wp-2fa / includes / classes / Admin / Methods / passkeys / format Last commit date
android-key.php 1 day ago android-safety-net.php 1 day ago apple.php 1 day ago format-base.php 1 day ago index.php 1 day ago none.php 1 day ago packed.php 1 day ago tpm.php 1 day ago u2f.php 1 day ago
apple.php
187 lines
1 <?php
2 /**
3 * Passkeys formatters
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 namespace WP2FA\Passkeys\Format;
13
14 use WP2FA\Methods\Passkeys\Byte_Buffer;
15 use WP2FA\Methods\Passkeys\Web_Authn_Exception;
16 use WP2FA\Admin\Methods\passkeys\Authenticator_Data;
17
18 /**
19 * Responsible for apple format
20 *
21 * @since 3.0.0
22 */
23 class Apple extends Format_Base {
24
25 /**
26 * X5c certificate
27 *
28 * @var string
29 *
30 * @since 3.0.0
31 */
32 private $x5c;
33
34 /**
35 * Default constructor
36 *
37 * @param \StdClass $attestion_object - attestation object.
38 * @param \WP2FA\Admin\Methods\passkeys\Authenticator_Data $authenticator_data - Authentication data.
39 *
40 * @throws Web_Authn_Exception - if can not be created.
41 *
42 * @since 3.0.0
43 */
44 public function __construct( $attestion_object, Authenticator_Data $authenticator_data ) {
45 parent::__construct( $attestion_object, $authenticator_data );
46
47 // check packed data.
48 $att_stmt = $this->attestation_object['attStmt'];
49
50 // certificate for validation.
51 if ( \array_key_exists( 'x5c', $att_stmt ) && \is_array( $att_stmt['x5c'] ) && \count( $att_stmt['x5c'] ) > 0 ) {
52
53 // The attestation certificate attestn_cert MUST be the first element in the array.
54 $attestn_cert = array_shift( $att_stmt['x5c'] );
55
56 if ( ! ( $attestn_cert instanceof Byte_Buffer ) ) {
57 throw new Web_Authn_Exception( 'invalid x5c certificate', Web_Authn_Exception::INVALID_DATA );
58 }
59
60 $this->x5c = $attestn_cert->getBinaryString();
61
62 // certificate chain.
63 foreach ( $att_stmt['x5c'] as $chain ) {
64 if ( $chain instanceof Byte_Buffer ) {
65 $this->x5c_chain[] = $chain->getBinaryString();
66 }
67 }
68 } else {
69 throw new Web_Authn_Exception( 'invalid Apple attestation statement: missing x5c', Web_Authn_Exception::INVALID_DATA );
70 }
71 }
72
73 /**
74 * Returns the key certificate in PEM format
75 *
76 * @return string|null
77 *
78 * @since 3.0.0
79 */
80 public function get_certificate_pem() {
81 return $this->_create_certificate_pem( $this->x5c );
82 }
83
84 /**
85 * Validates the attestation
86 *
87 * @param string $client_data_hash - Hash collected.
88 *
89 * @throws Web_Authn_Exception - Throws exception if validation fails.
90 *
91 * @since 3.0.0
92 */
93 public function validate_attestation( $client_data_hash ) {
94 return $this->_validate_ver_x5c( $client_data_hash );
95 }
96
97 /**
98 * Validates the certificate against root certificates
99 *
100 * @param array $root_cas - Array with values.
101 *
102 * @return boolean
103 *
104 * @throws Web_Authn_Exception - Throws exception.
105 *
106 * @since 3.0.0
107 */
108 public function validate_root_certificate( $root_cas ) {
109 $chain_c = $this->_create_x5c_chain_file();
110 if ( $chain_c ) {
111 $root_cas[] = $chain_c;
112 }
113
114 $v = \openssl_x509_checkpurpose( $this->get_certificate_pem(), -1, $root_cas );
115 if ( -1 === $v ) {
116 throw new Web_Authn_Exception( 'error on validating root certificate: ' . \openssl_error_string(), Web_Authn_Exception::CERTIFICATE_NOT_TRUSTED );
117 }
118 return $v;
119 }
120
121 /**
122 * Validate if x5c is present
123 *
124 * @param string $client_data_hash - Hash collected.
125 *
126 * @return bool
127 *
128 * @throws Web_Authn_Exception - Throws exception.
129 *
130 * @since 3.0.0
131 */
132 protected function _validate_ver_x5c( $client_data_hash ) {
133 $public_key = \openssl_pkey_get_public( $this->get_certificate_pem() );
134
135 if ( false === $public_key ) {
136 throw new Web_Authn_Exception( 'invalid public key: ' . \openssl_error_string(), Web_Authn_Exception::INVALID_PUBLIC_KEY );
137 }
138
139 // Concatenate Authenticator_Data and client_data_hash to form nonce_to_hash.
140 $nonce_to_hash = $this->authenticator_data->get_binary();
141 $nonce_to_hash .= $client_data_hash;
142
143 // Perform SHA-256 hash of nonce_to_hash to produce nonce.
144 $nonce = hash( 'SHA256', $nonce_to_hash, true );
145
146 $cred_cert = openssl_x509_read( $this->get_certificate_pem() );
147 if ( false === $cred_cert ) {
148 throw new Web_Authn_Exception( 'invalid x5c certificate: ' . \openssl_error_string(), Web_Authn_Exception::INVALID_DATA );
149 }
150
151 $key_data = openssl_pkey_get_details( openssl_pkey_get_public( $cred_cert ) );
152 $key = is_array( $key_data ) && array_key_exists( 'key', $key_data ) ? $key_data['key'] : null;
153
154 // Verify that nonce equals the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in cred_cert.
155 $parsed_cred_cert = openssl_x509_parse( $cred_cert );
156 $nonce_extension = $parsed_cred_cert['extensions']['1.2.840.113635.100.8.2'] ?? '';
157
158 // nonce padded by ASN.1 string: 30 24 A1 22 04 20
159 // 30 — type tag indicating sequence
160 // 24 — 36 byte following
161 // A1 — Enumerated [1]
162 // 22 — 34 byte following
163 // 04 — type tag indicating octet string
164 // 20 — 32 byte following.
165
166 $asn1_padding = "\x30\x24\xA1\x22\x04\x20";
167 if ( substr( $nonce_extension, 0, strlen( $asn1_padding ) ) === $asn1_padding ) {
168 $nonce_extension = substr( $nonce_extension, strlen( $asn1_padding ) );
169 }
170
171 if ( $nonce_extension !== $nonce ) {
172 throw new Web_Authn_Exception( 'nonce doesn\'t equal the value of the extension with OID 1.2.840.113635.100.8.2', Web_Authn_Exception::INVALID_DATA );
173 }
174
175 // Verify that the credential public key equals the Subject Public Key of cred_cert.
176 $auth_key_data = openssl_pkey_get_details( openssl_pkey_get_public( $this->authenticator_data->get_public_key_pem() ) );
177 $auth_key = is_array( $auth_key_data ) && array_key_exists( 'key', $auth_key_data ) ? $auth_key_data['key'] : null;
178
179 if ( null === $key || $key !== $auth_key ) {
180 throw new Web_Authn_Exception( 'credential public key doesn\'t equal the Subject Public Key of credCert', Web_Authn_Exception::INVALID_DATA );
181 }
182
183 return true;
184 }
185
186 }
187