Atestación de SafetyNet: descripción e implementación de la verificación en PHP

Tuve que profundizar en este tema en detalle mientras trabajaba en proporcionar mecanismos de verificación de dispositivos estándar para varias plataformas móviles. La tarea se redujo al desarrollo de una implementación completa de la verificación del token JWS utilizando el protocolo SafetyNet en el lado del servidor.





Google . , , . SafetyNet .





, SafetyNet Attestation. - . , . PHP composer .





— PHP, JWS.





: Google , .





SafetyNet Attestation Google , . Google, , .





Google , . SafetyNet , .





:





  1. , .





  2. , .





  3. , ( «» — , , Android).





:





  1. , . API , .





  2. ( ), . Backend, SafetyNet , .





  3. , . . : ctsProfileMatch basicIntegrity. — .





, , . : - -, , — () . , , , .





:





:





  1. . Backend (nonce) . (nonce) , .





  2. JSW- ., nonce, . JWS, , , ( , Google Store), , (). JWS, .





  3. JWS Backend . . JWS , , . , , , , , , .





JWS

Google online- JWS, JWS Google. Google JWS.





JWS . : 10 000 ( — ), . .





JWS, ( ).





JWS

JWS (base64 ) , (header.body.signature):





:





eyJhbGciOiJSUzI1NiIsICJ4NWMiOiBbInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4xIiwgInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4yIl19.ewogICJub25jZSI6ICJ2ZXJ5c2VjdXJlbm91bmNlIiwKICAidGltZXN0YW1wTXMiOiAxNTM5ODg4NjUzNTAzLAogICJhcGtQYWNrYWdlTmFtZSI6ICJ2ZXJ5Lmdvb2QuYXBwIiwKICAiYXBrRGlnZXN0U2hhMjU2IjogInh5eHl4eXh5eHl4eXh5eHl5eHl4eXg9IiwKICAiY3RzUHJvZmlsZU1hdGNoIjogdHJ1ZSwKICAiYXBrQ2VydGlmaWNhdGVEaWdlc3RTaGEyNTYiOiBbCiAgICAieHl4eXh5eHl4eXh5eHl4eXh5eD09PT09Lz0iCiAgXSwKICAiYmFzaWNJbnRlZ3JpdHkiOiB0cnVlCn0=.c2lnbmF0dXJl





base64 :





Header :





json_decode(
	base64_decode(
		“eyJhbGciOiJSUzI1NiIsICJ4NWMiOiBbInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4xIiwgInZlcnlzZWN1cmVwdWJsaWNzZXJ0Y2hhaW4yIl19”
	)
)

=

{
	"alg":"RS256",
	"x5c":[
	"verysecurepublicsertchain1",
	"verysecurepublicsertchain2"
	]
}
      
      



Body:





json_decode(
	base64_decode(
		“ewogICJub25jZSI6ICJ2ZXJ5c2VjdXJlbm91bmNlIiwKICAidGltZXN0YW1wTXMiOiAxNTM5ODg4NjUzNTAzLAogICJhcGtQYWNrYWdlTmFtZSI6ICJ2ZXJ5Lmdvb2QuYXBwIiwKICAiYXBrRGlnZXN0U2hhMjU2IjogInh5eHl4eXh5eHl4eXh5eHl5eHl4eXg9IiwKICAiY3RzUHJvZmlsZU1hdGNoIjogdHJ1ZSwKICAiYXBrQ2VydGlmaWNhdGVEaWdlc3RTaGEyNTYiOiBbCiAgICAieHl4eXh5eHl4eXh5eHl4eXh5eD09PT09Lz0iCiAgXSwKICAiYmFzaWNJbnRlZ3JpdHkiOiB0cnVlCn0=”
	)
)

=

{
	"nonce":"verysecurenounce",
	"timestampMs":1539888653503,
	"apkPackageName":"very.good.app",
	"apkDigestSha256":"xyxyxyxyxyxyxyxyyxyxyx=",
	"ctsProfileMatch":true,
	"apkCertificateDigestSha256":[
		"xyxyxyxyxyxyxyxyxyx=====/="
	],
	"basicIntegrity":true
}
      
      



Signature





json_decode(
	base64_decode(
		“c2lnbmF0dXJl”
	)
)

= 

“signature”
      
      



, JWS.





Header:





  • alg — , Header Body JWS. .





  • x5c — ( ). .





Body:





  • nonce — .





  • timestampMs — .





  • apkPackageName — , .





  • apkDigestSha256 — , Google Play.





  • ctsProfileMatch — , Google ( , Google).





  •  apkCertificateDigestSha256 — ( ), Google Play.





  • basicIntegrity — ( ctsProfileMatch) .





Signature





, , JWS ( ) Header, . — , , Google.





JWS. :





1. , , , :





[$checkMethod, $algorithm] = JWT::$supported_algs[$statement->getHeader()->getAlgorithm()];

if ($checkMethod != 'openssl') {
   throw new CheckSignatureException('Not supported algorithm function');
}
      
      



2. , ( ), Header ( x5c), ( ):





private function extractAlgorithm(array $headers): string
{
   if (empty($headers['alg'])) {
       throw new EmptyAlgorithmField('Empty alg field in headers');
   }

   return $headers['alg'];
}

private function extractCertificateChain(array $headers): X509
{
   if (empty($headers['x5c'])) {
       throw new MissingCertificates('Missing certificates');
   }

   $x509 = new X509();
   if ($x509->loadX509(array_shift($headers['x5c'])) === false) {
       throw new CertificateLoadError('Failed to load certificate');
   }

   while ($textCertificate = array_shift($headers['x5c'])) {
       if ($x509->loadCA($textCertificate) === false) {
           throw new CertificateCALoadError('Failed to load certificate');
       }
   }

   if ($x509->loadCA(RootGoogleCertService::rootCertificate()) === false) {
       throw new RootCertificateError('Failed to load Root-CA certificate');
   }

   return $x509;
}
      
      



3. ( ):





private function guardCertificateChain(StatementHeader $header): bool
{
   if (!$header->getCertificateChain()->validateSignature()) {
       throw new CertificateChainError('Certificate chain signature is not valid');
   }

   return true;
}
      
      



4. hostname Google (ISSUINGHOSTNAME = 'attest.android.com'):





private function guardAttestHostname(StatementHeader $header): bool
{
   $commonNames = $header->getCertificateChain()->getDNProp('CN');
   $issuingHostname = $commonNames[0] ?? null;

   if ($issuingHostname !== self::ISSUING_HOSTNAME) {
       throw new CertificateHostnameError(
           'Certificate isn\'t issued for the hostname ' . self::ISSUING_HOSTNAME
       );
   }

   return true;
}
      
      



JWS

, . :





1. nounce.





. JWS, Body nonce , :





private function guardNonce(Nonce $nonce, StatementBody $statementBody): bool
{
   $statementNonce = $statementBody->getNonce();

   if (!$statementNonce->isEqual($nonce)) {
       throw new WrongNonce('Invalid nonce');
   }

   return true;
}
      
      



2. , .





, , . 





, : ctsProfileMatch basicIntegrity. ctsProfileMatch — , Google Play Google. basicIntegrity — , .





private function guardDeviceIsNotRooted(StatementBody $statementBody): bool
{
   $ctsProfileMatch = $statementBody->getCtsProfileMatch();
   $basicIntegrity = $statementBody->getBasicIntegrity();

   if (empty($ctsProfileMatch) || !$ctsProfileMatch) {
       throw new ProfileMatchFieldError('Device is rooted');
   }

   if (empty($basicIntegrity) || !$basicIntegrity) {
       throw new BasicIntegrityFieldError('Device can be rooted');
   }

   return true;
}
      
      



3. .





. , Google . , — .





private function guardTimestamp(StatementBody $statementBody): bool
{
   $timestampDiff = $this->config->getTimeStampDiffInterval();
   $timestampMs = $statementBody->getTimestampMs();

   if (abs(microtime(true) * 1000 - $timestampMs) > $timestampDiff) {
       throw new TimestampFieldError('TimestampMS and the current time is more than ' . $timestampDiff . ' MS');
   }

   return true;
}
      
      



4. .



: apkDigestSha256 apkCertificateDigestSha256. apkDigestSha256 Google . 2018 - — - , JWS ( — ).





apkCertificateDigestSha256. sha1 , apk Google Play.





private function guardApkCertificateDigestSha256(StatementBody $statementBody): bool
{
   $apkCertificateDigestSha256 = $this->config->getApkCertificateDigestSha256();
   $testApkCertificateDigestSha256 = $statementBody->getApkCertificateDigestSha256();

   if (empty($testApkCertificateDigestSha256)) {
       throw new ApkDigestShaError('Empty apkCertificateDigestSha256 field');
   }

   $configSha256 = [];
   foreach ($apkCertificateDigestSha256 as $sha256) {
       $configSha256[] = base64_encode(hex2bin($sha256));
   }

   foreach ($testApkCertificateDigestSha256 as $digestSha) {
       if (in_array($digestSha, $configSha256)) {
           return true;
       }
   }

   throw new ApkDigestShaError('apkCertificateDigestSha256 is not valid');
}
      
      



5. , .





JWS .





private function guardApkPackageName(StatementBody $statementBody): bool
{
   $apkPackageName = $this->config->getApkPackageName();
   $testApkPackageName = $statementBody->getApkPackageName();

   if (empty($testApkPackageName)) {
       throw new ApkNameError('Empty apkPackageName field');
   }

   if (!in_array($testApkPackageName, $apkPackageName)) {
       throw new ApkNameError('apkPackageName ' . $testApkPackageName. ' not equal ' . join(", ", $apkPackageName));
   }

   return true;
}
      
      



, , Header Body JWS Google. Header c Body ( ".") :





protected function guardSignature(Statement $statement): bool
{
   $jwsHeaders = $statement->getRawHeaders();
   $jwsBody = $statement->getRawBody();

   $signData = $jwsHeaders . '.' . $jwsBody;

   $stringPublicKey = (string)$statement->getHeader()->getCertificateChain()->getPublicKey();

   [$checkMethod, $algorithm] = JWT::$supported_algs[$statement->getHeader()->getAlgorithm()];

   if ($checkMethod != 'openssl') {
       throw new CheckSignatureException('Not supported algorithm function');
   }

   if (openssl_verify($signData, $statement->getSignature(), $stringPublicKey, $algorithm) < 1) {
       throw new CheckSignatureException('Signature is invalid');
   }

   return true;
}
      
      



. PHP

, PHP, JWS.





Packagist .








All Articles