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.
SafetyNet Attestation Google , . Google, , .
Google , . SafetyNet , .
:
, .
, .
, ( «» — , , Android).
:
, . API , .
( ), . Backend, SafetyNet , .
, . . : ctsProfileMatch basicIntegrity. — .
, , . : - -, , — () . , , , .
:
:
. Backend (nonce) . (nonce) , .
JSW- ., nonce, . JWS, , , ( , Google Store), , (). JWS, .
JWS Backend . . JWS , , . , , , , , , .
JWS
Google online- JWS, JWS Google. Google JWS.
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.