Skip to content

Commit deb37c7

Browse files
committed
Android-SafetyNet: ctsProfileMatch
Allow decide in processCreate() if you want accept only devices with ctsProfileMatch (default) or basicIntegrity is suffisant.
1 parent fd22585 commit deb37c7

3 files changed

Lines changed: 27 additions & 7 deletions

File tree

src/Attestation/Format/AndroidSafetyNet.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ public function __construct($AttestionObject, AuthenticatorData $authenticatorDa
7272
}
7373
}
7474

75+
/**
76+
* ctsProfileMatch: A stricter verdict of device integrity.
77+
* If the value of ctsProfileMatch is true, then the profile of the device running your app matches
78+
* the profile of a device that has passed Android compatibility testing and
79+
* has been approved as a Google-certified Android device.
80+
* @return bool
81+
*/
82+
public function ctsProfileMatch() {
83+
return isset($this->_payload->ctsProfileMatch) ? !!$this->_payload->ctsProfileMatch : false;
84+
}
85+
7586

7687
/*
7788
* returns the key certificate in PEM format
@@ -95,13 +106,13 @@ public function validateAttestation($clientDataHash) {
95106

96107
// Verify that attestationCert is issued to the hostname "attest.android.com"
97108
$certInfo = \openssl_x509_parse($this->getCertificatePem());
98-
if (!\is_array($certInfo) || !$certInfo['subject'] || $certInfo['subject']['CN'] !== 'attest.android.com') {
99-
throw new WebAuthnException('invalid certificate CN in JWS (' . $certInfo['subject']['CN']. ')', WebAuthnException::INVALID_DATA);
109+
if (!\is_array($certInfo) || ($certInfo['subject']['CN'] ?? '') !== 'attest.android.com') {
110+
throw new WebAuthnException('invalid certificate CN in JWS (' . ($certInfo['subject']['CN'] ?? '-'). ')', WebAuthnException::INVALID_DATA);
100111
}
101112

102-
// Verify that the ctsProfileMatch attribute in the payload of response is true.
103-
if (empty($this->_payload->ctsProfileMatch)) {
104-
throw new WebAuthnException('invalid ctsProfileMatch in payload', WebAuthnException::INVALID_DATA);
113+
// Verify that the basicIntegrity attribute in the payload of response is true.
114+
if (empty($this->_payload->basicIntegrity)) {
115+
throw new WebAuthnException('invalid basicIntegrity in payload', WebAuthnException::INVALID_DATA);
105116
}
106117

107118
// check certificate

src/WebAuthn.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,11 @@ public function getSignatureCounter() {
286286
* @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin)
287287
* @param bool $requireUserPresent false, if the device must NOT check user presence (e.g. by pressing a button)
288288
* @param bool $failIfRootMismatch false, if there should be no error thrown if root certificate doesn't match
289+
* @param bool $requireCtsProfileMatch false, if you don't want to check if the device is approved as a Google-certified Android device.
289290
* @return \stdClass
290291
* @throws WebAuthnException
291292
*/
292-
public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true) {
293+
public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true, $requireCtsProfileMatch=true) {
293294
$clientDataHash = \hash('sha256', $clientDataJSON, true);
294295
$clientData = \json_decode($clientDataJSON);
295296
$challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
@@ -330,6 +331,13 @@ public function processCreate($clientDataJSON, $attestationObject, $challenge, $
330331
throw new WebAuthnException('invalid certificate signature', WebAuthnException::INVALID_SIGNATURE);
331332
}
332333

334+
// Android-SafetyNet: if required, check for Compatibility Testing Suite (CTS).
335+
if ($requireCtsProfileMatch && $attestationObject->getAttestationFormat() instanceof Attestation\Format\AndroidSafetyNet) {
336+
if (!$attestationObject->getAttestationFormat()->ctsProfileMatch()) {
337+
throw new WebAuthnException('invalid ctsProfileMatch: device is not approved as a Google-certified Android device.', WebAuthnException::ANDROID_NOT_TRUSTED);
338+
}
339+
}
340+
333341
// 15. If validation is successful, obtain a list of acceptable trust anchors
334342
$rootValid = is_array($this->_caFiles) ? $attestationObject->validateRootCertificate($this->_caFiles) : null;
335343
if ($failIfRootMismatch && is_array($this->_caFiles) && !$rootValid) {
@@ -483,7 +491,7 @@ public function processGet($clientDataJSON, $authenticatorData, $signature, $cre
483491
* Downloads root certificates from FIDO Alliance Metadata Service (MDS) to a specific folder
484492
* https://fidoalliance.org/metadata/
485493
* @param string $certFolder Folder path to save the certificates in PEM format.
486-
* @param bool $deleteCerts=true
494+
* @param bool $deleteCerts delete certificates in the target folder before adding the new ones.
487495
* @return int number of cetificates
488496
* @throws WebAuthnException
489497
*/

src/WebAuthnException.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class WebAuthnException extends \Exception {
2020
const CRYPTO_STRONG = 13;
2121
const BYTEBUFFER = 14;
2222
const CBOR = 15;
23+
const ANDROID_NOT_TRUSTED = 16;
2324

2425
public function __construct($message = "", $code = 0, $previous = null) {
2526
parent::__construct($message, $code, $previous);

0 commit comments

Comments
 (0)