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 / android-safety-net.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
android-safety-net.php
227 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 android format
20 *
21 * @since 3.0.0
22 */
23 class Android_Safety_Net extends Format_Base {
24
25 /**
26 * Signature chain
27 *
28 * @var string
29 *
30 * @since 3.0.0
31 */
32 private $signature;
33
34 /**
35 * Signed value
36 *
37 * @var string
38 *
39 * @since 3.0.0
40 */
41 private $signed_value;
42
43 /**
44 * X5c certificate
45 *
46 * @var string
47 *
48 * @since 3.0.0
49 */
50 private $x5c;
51
52 /**
53 * Payload
54 *
55 * @var \stdClass
56 *
57 * @since 3.0.0
58 */
59 private $payload;
60
61 /**
62 * Default constructor
63 *
64 * @param mixed $attestation_object - attestation object.
65 * @param \WP2FA\Admin\Methods\passkeys\Authenticator_Data $authenticator_data - Authentication data.
66 *
67 * @throws Web_Authn_Exception - if can not be created.
68 *
69 * @since 3.0.0
70 */
71 public function __construct( $attestation_object, Authenticator_Data $authenticator_data ) {
72 parent::__construct( $attestation_object, $authenticator_data );
73
74 // check data.
75 $att_stmt = $this->attestation_object['attStmt'];
76
77 if ( ! \array_key_exists( 'ver', $att_stmt ) || ! $att_stmt['ver'] ) {
78 throw new Web_Authn_Exception( 'invalid Android Safety Net Format', Web_Authn_Exception::INVALID_DATA );
79 }
80
81 if ( ! \array_key_exists( 'response', $att_stmt ) || ! ( $att_stmt['response'] instanceof Byte_Buffer ) ) {
82 throw new Web_Authn_Exception( 'invalid Android Safety Net Format', Web_Authn_Exception::INVALID_DATA );
83 }
84
85 $response = $att_stmt['response']->getBinaryString();
86
87 // Response is a JWS [RFC7515] object in Compact Serialization.
88 // JWSs have three segments separated by two period ('.') characters.
89 $parts = \explode( '.', $response );
90 unset( $response );
91 if ( \count( $parts ) !== 3 ) {
92 throw new Web_Authn_Exception( 'invalid JWS data', Web_Authn_Exception::INVALID_DATA );
93 }
94
95 $header = $this->_base64url_decode( $parts[0] );
96 $payload = $this->_base64url_decode( $parts[1] );
97 $this->signature = $this->_base64url_decode( $parts[2] );
98 $this->signed_value = $parts[0] . '.' . $parts[1];
99 unset( $parts );
100
101 $header = \json_decode( $header );
102 $payload = \json_decode( $payload );
103
104 if ( ! ( $header instanceof \stdClass ) ) {
105 throw new Web_Authn_Exception( 'invalid JWS header', Web_Authn_Exception::INVALID_DATA );
106 }
107 if ( ! ( $payload instanceof \stdClass ) ) {
108 throw new Web_Authn_Exception( 'invalid JWS payload', Web_Authn_Exception::INVALID_DATA );
109 }
110
111 if ( ! isset( $header->x5c ) || ! is_array( $header->x5c ) || count( $header->x5c ) === 0 ) {
112 throw new Web_Authn_Exception( 'No X.509 signature in JWS Header', Web_Authn_Exception::INVALID_DATA );
113 }
114
115 // algorithm.
116 if ( ! \in_array( $header->alg, array( 'RS256', 'ES256' ) ) ) {
117 throw new Web_Authn_Exception( 'invalid JWS algorithm ' . $header->alg, Web_Authn_Exception::INVALID_DATA );
118 }
119
120 $this->x5c = \base64_decode( $header->x5c[0] );
121 $this->payload = $payload;
122
123 if ( count( $header->x5c ) > 1 ) {
124 for ( $i = 1; $i < count( $header->x5c ); $i++ ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
125 $this->x5c_chain[] = \base64_decode( $header->x5c[ $i ] );
126 }
127 unset( $i );
128 }
129 }
130
131 /**
132 * A stricter verdict of device integrity.
133 * If the value of ctsProfileMatch is true, then the profile of the device running your app matches
134 * the profile of a device that has passed Android compatibility testing and
135 * has been approved as a Google-certified Android device.
136 *
137 * @return bool
138 *
139 * @since 3.0.0
140 */
141 public function ctsProfileMatch() {
142 return isset( $this->payload->ctsProfileMatch ) ? ! ! $this->payload->ctsProfileMatch : false;
143 }
144
145 /**
146 * Returns the key certificate in PEM format
147 *
148 * @return string
149 *
150 * @since 3.0.0
151 */
152 public function getCertificatePem() {
153 return $this->_create_certificate_pem( $this->x5c );
154 }
155
156 /**
157 * Validates the attestation
158 *
159 * @param string $client_data_hash - Hash collected.
160 *
161 * @throws Web_Authn_Exception - Throws exception if validation fails.
162 *
163 * @since 3.0.0
164 */
165 public function validate_attestation( $client_data_hash ) {
166 $public_key = \openssl_pkey_get_public( $this->getCertificatePem() );
167
168 // Verify that the nonce in the response is identical to the Base64 encoding
169 // of the SHA-256 hash of the concatenation of Authenticator_Data and client_data_hash.
170 if ( empty( $this->payload->nonce ) || \base64_encode( \hash( 'SHA256', $this->authenticator_data->get_binary() . $client_data_hash, true ) ) !== $this->payload->nonce ) {
171 throw new Web_Authn_Exception( 'invalid nonce in JWS payload', Web_Authn_Exception::INVALID_DATA );
172 }
173
174 // Verify that attestationCert is issued to the hostname "attest.android.com".
175 $cert_info = \openssl_x509_parse( $this->getCertificatePem() );
176 if ( ! \is_array( $cert_info ) || ( $cert_info['subject']['CN'] ?? '' ) !== 'attest.android.com' ) {
177 throw new Web_Authn_Exception( 'invalid certificate CN in JWS (' . ( $cert_info['subject']['CN'] ?? '-' ) . ')', Web_Authn_Exception::INVALID_DATA );
178 }
179
180 // Verify that the basicIntegrity attribute in the payload of response is true.
181 if ( empty( $this->payload->basicIntegrity ) ) {
182 throw new Web_Authn_Exception( 'invalid basicIntegrity in payload', Web_Authn_Exception::INVALID_DATA );
183 }
184
185 // check certificate.
186 return \openssl_verify( $this->signed_value, $this->signature, $public_key, OPENSSL_ALGO_SHA256 ) === 1;
187 }
188
189
190 /**
191 * Validates the certificate against root certificates
192 *
193 * @param array $root_cas - Array with values.
194 *
195 * @return boolean
196 *
197 * @throws Web_Authn_Exception - Throws exception.
198 *
199 * @since 3.0.0
200 */
201 public function validate_root_certificate( $root_cas ) {
202 $chain_c = $this->_create_x5c_chain_file();
203 if ( $chain_c ) {
204 $root_cas[] = $chain_c;
205 }
206
207 $v = \openssl_x509_checkpurpose( $this->get_certificate_pem(), -1, $root_cas );
208 if ( -1 === $v ) {
209 throw new Web_Authn_Exception( 'error on validating root certificate: ' . \openssl_error_string(), Web_Authn_Exception::CERTIFICATE_NOT_TRUSTED );
210 }
211 return $v;
212 }
213
214 /**
215 * Decode base64 url
216 *
217 * @param string $data - Data to decode.
218 *
219 * @return string
220 *
221 * @since 3.0.0
222 */
223 private function _base64url_decode( $data ) {
224 return \base64_decode( \strtr( $data, '-_', '+/' ) . \str_repeat( '=', 3 - ( 3 + \strlen( $data ) ) % 4 ) );
225 }
226 }
227