SafetyNet Attestation - description and implementation of the check in PHP

I had to dive into this topic in detail while working on providing standard device verification mechanisms for various mobile platforms. The task came down to the development of a full-fledged implementation of JWS token verification using the SafetyNet protocol on the server side.





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