完整性裁定

本页面介绍如何解读和使用返回的完整性裁定。无论您发出标准 API 请求还是经典 API 请求,完整性裁定都以相似内容采用相同格式返回。完整性裁定会传达有关设备、应用和账号有效性的信息。您的应用服务器可以使用已解密且经过验证的裁定中的所得载荷,以确定如何在应用中针对特定操作或请求采取最佳措施。

返回的完整性裁定格式

载荷是纯文本 JSON 格式,包含完整性信号以及开发者提供的信息。

一般载荷结构如下

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: { ... }
}

在检查每个完整性裁定之前,您必须首先检查 requestDetails 字段中的值是否与原始请求的值匹配。以下部分更详细地描述了每个字段。

请求详情字段

requestDetails 字段包含有关请求的信息,包括开发者在标准请求的 requestHash 和经典请求的 nonce 中提供的信息。

对于标准 API 请求

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the request.
  requestPackageName: "com.package.name"
  // Request hash provided by the developer.
  requestHash: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the integrity token
  // was requested.
  timestampMillis: "1675655009345"
}

这些值应与原始请求的值匹配。因此,请通过确保 requestPackageNamerequestHash 与原始请求中发送的内容匹配来验证 JSON 载荷的 requestDetails 部分,如以下代码段所示

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val requestHash = requestDetails.getString("requestHash")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

RequestDetails requestDetails =
    decodeIntegrityTokenResponse
    .getTokenPayloadExternal()
    .getRequestDetails();
String requestPackageName = requestDetails.getRequestPackageName();
String requestHash = requestDetails.getRequestHash();
long timestampMillis = requestDetails.getTimestampMillis();
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request.
    || !requestHash.equals(expectedRequestHash)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

对于经典 API 请求

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the
  // request.
  requestPackageName: "com.package.name"
  // base64-encoded URL-safe no-wrap nonce provided by the developer.
  nonce: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the request was made
  // (computed on the server).
  timestampMillis: "1617893780"
}

这些值应与原始请求的值匹配。因此,请通过确保 requestPackageNamenonce 与原始请求中发送的内容匹配来验证 JSON 载荷的 requestDetails 部分,如以下代码段所示

Kotlin

val requestDetails = JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName = requestDetails.getString("requestPackageName")
val nonce = requestDetails.getString("nonce")
val timestampMillis = requestDetails.getLong("timestampMillis")
val currentTimestampMillis = ...

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

Java

JSONObject requestDetails =
    new JSONObject(payload).getJSONObject("requestDetails");
String requestPackageName = requestDetails.getString("requestPackageName");
String nonce = requestDetails.getString("nonce");
long timestampMillis = requestDetails.getLong("timestampMillis");
long currentTimestampMillis = ...;

// Ensure the token is from your app.
if (!requestPackageName.equals(expectedPackageName)
        // Ensure the token is for this specific request. See 'Generate a nonce'
        // section of the doc on how to store/compute the expected nonce.
    || !nonce.equals(expectedNonce)
        // Ensure the freshness of the token.
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
        // The token is invalid! See below for further checks.
        ...
}

应用完整性字段

appIntegrity 字段包含与软件包相关的信息。

appIntegrity: {
  // PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, or UNEVALUATED.
  appRecognitionVerdict: "PLAY_RECOGNIZED"
  // The package name of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  packageName: "com.package.name"
  // The sha256 digest of app certificates (base64-encoded URL-safe).
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"]
  // The version of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  versionCode: "42"
}

appRecognitionVerdict 可能包含以下值

PLAY_RECOGNIZED
该应用和证书与 Google Play 分发的版本匹配。
UNRECOGNIZED_VERSION
证书或软件包名称与 Google Play 记录不匹配。
UNEVALUATED
未评估应用完整性。遗漏了必要的要求,例如设备不够可信。

为了确保令牌是由您创建的应用生成的,请验证应用完整性是否如预期,如以下代码段所示

Kotlin

val appIntegrity = JSONObject(payload).getJSONObject("appIntegrity")
val appRecognitionVerdict = appIntegrity.getString("appRecognitionVerdict")

if (appRecognitionVerdict == "PLAY_RECOGNIZED") {
    // Looks good!
}

Java

JSONObject appIntegrity =
    new JSONObject(payload).getJSONObject("appIntegrity");
String appRecognitionVerdict =
    appIntegrity.getString("appRecognitionVerdict");

if (appRecognitionVerdict.equals("PLAY_RECOGNIZED")) {
    // Looks good!
}

您还可以手动检查应用软件包名称、应用版本和应用证书。

设备完整性字段

deviceIntegrity 字段可包含一个值 deviceRecognitionVerdict,该值有一个或多个标签,表示设备强制执行应用完整性的程度。如果设备不符合任何标签的标准,则 deviceIntegrity 字段为空。

deviceIntegrity: {
  // "MEETS_DEVICE_INTEGRITY" is one of several possible values.
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
}

默认情况下,deviceRecognitionVerdict 可包含以下值

MEETS_DEVICE_INTEGRITY
该应用正在 Play 保护机制认证的真实 Android 设备上运行。在 Android 13 及更高版本上,有硬件支持的证据表明设备引导加载程序已锁定,并且加载的 Android 操作系统是经过认证的设备制造商映像。
空值(空白值)
该应用正在一台有攻击迹象(例如 API hooking)或系统遭到入侵(例如已取得 Root 权限)的设备上运行,或者该应用未在实体设备上运行(例如未通过 Google Play 完整性检查的模拟器)。

为了确保令牌来自可信设备,请验证 deviceRecognitionVerdict 是否如预期,如以下代码段所示

Kotlin

val deviceIntegrity =
    JSONObject(payload).getJSONObject("deviceIntegrity")
val deviceRecognitionVerdict =
    if (deviceIntegrity.has("deviceRecognitionVerdict")) {
        deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    } else {
        ""
    }

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

Java

JSONObject deviceIntegrity =
    new JSONObject(payload).getJSONObject("deviceIntegrity");
String deviceRecognitionVerdict =
    deviceIntegrity.has("deviceRecognitionVerdict")
    ? deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString()
    : "";

if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) {
    // Looks good!
}

如果您的测试设备无法满足设备完整性要求,请确保已安装出厂 ROM(例如,通过重置设备)并且引导加载程序已锁定。您还可以在您的 Play 管理中心内创建 Play 完整性 API 测试

有条件的设备标签

如果您的应用将发布到适用于 PC 的 Google Play 游戏,则 deviceRecognitionVerdict 还可以包含以下标签

MEETS_VIRTUAL_INTEGRITY
该应用正在搭载 Google Play 服务的 Android 模拟器上运行。该模拟器通过了系统完整性检查并满足核心 Android 兼容性要求。

可选设备信息和设备召回

如果您选择在完整性裁定中接收其他标签,则 deviceRecognitionVerdict 可以包含以下其他标签

MEETS_BASIC_INTEGRITY
该应用正在通过基本系统完整性检查的设备上运行。设备引导加载程序可以锁定或解锁,引导状态可以验证或未验证。该设备可能未经 Play 保护机制认证,在这种情况下,Google 无法提供任何安全性、隐私权或应用兼容性保证。在 Android 13 及更高版本上,MEETS_BASIC_INTEGRITY 裁定仅要求证明的信任根由 Google 提供。
MEETS_STRONG_INTEGRITY
该应用正在 Play 保护机制认证的真实 Android 设备上运行,且已安装近期安全更新。
  • 在 Android 13 及更高版本上,MEETS_STRONG_INTEGRITY 裁定要求设备满足 MEETS_DEVICE_INTEGRITY,并且所有分区(包括 Android 操作系统分区补丁和供应商分区补丁)在过去一年内进行了安全更新。
  • 在 Android 12 及更低版本上,MEETS_STRONG_INTEGRITY 裁定仅要求提供硬件支持的引导完整性证明,而要求设备具备近期安全更新。因此,使用 MEETS_STRONG_INTEGRITY 时,建议同时考虑 deviceAttributes 字段中的 Android SDK 版本。

如果设备满足每个标签的标准,则设备完整性裁定中会返回多个设备标签。

设备属性

您还可以选择启用设备属性,这将告知您设备上运行的 Android 操作系统的 Android SDK 版本。将来,可能会扩展以包含其他设备属性。

SDK 版本值是 Build.VERSION_CODES 中定义的 Android SDK 版本号。如果必要要求缺失,则不会评估 SDK 版本。在这种情况下,sdkVersion 字段未设置;因此,deviceAttributes 字段为空。这可能因为以下原因发生:

  • 设备不够可信。
  • 设备上存在技术问题。

如果您选择接收 deviceAttributes,则 deviceIntegrity 字段将包含以下额外字段

deviceIntegrity: {
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
  deviceAttributes: {
    // 33 is one possible value, which represents Android 13 (Tiramisu).
    sdkVersion: 33
  }
}

如果未评估 SDK 版本,deviceAttributes 字段将设置为如下所示

deviceIntegrity: {
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
  deviceAttributes: {}  // sdkVersion field is not set.
}

近期设备活动

您还可以选择启用近期设备活动,这将告知您您的应用在过去一小时内在特定设备上请求完整性令牌的次数。您可以使用近期设备活动来保护您的应用免受意外的、过度活跃的设备攻击,这可能表明存在活跃的攻击。您可以根据您的应用在典型设备上每小时请求完整性令牌的预期次数,来确定对每个近期设备活动级别的信任程度。

如果您选择接收 recentDeviceActivity,则 deviceIntegrity 字段将包含两个值

deviceIntegrity: {
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
  recentDeviceActivity: {
    // "LEVEL_2" is one of several possible values.
    deviceActivityLevel: "LEVEL_2"
  }
}

deviceActivityLevel 的定义在不同模式下有所不同,并且可能包含以下任一值

近期设备活动级别 此设备在过去一小时内每个应用的标准 API 完整性令牌请求次数 此设备在过去一小时内每个应用的经典 API 完整性令牌请求次数
LEVEL_1(最低) 10 次或更少 5 次或更少
LEVEL_2 11 到 25 次之间 6 到 10 次之间
LEVEL_3 26 到 50 次之间 11 到 15 次之间
LEVEL_4(最高) 超过 50 次 超过 15 次
UNEVALUATED 未评估近期设备活动。这可能因为以下原因发生:
  • 设备不够可信。
  • 设备上安装的应用版本对 Google Play 不可知。
  • 设备上存在技术问题。

设备召回(Beta 版)

您还可以选择启用设备召回,这允许您为特定设备存储一些自定义的、每设备数据,您可以在应用稍后在同一设备上再次安装时可靠地检索这些数据。请求完整性令牌后,您会发出单独的服务器到服务器调用,以针对特定设备修改设备召回值

如果您选择启用 deviceRecall,则 deviceIntegrity 字段将包含您为特定设备设置的设备召回信息

"deviceIntegrity": {
  "deviceRecognitionVerdict": ["MEETS_DEVICE_INTEGRITY"],
  "deviceRecall": {
    "values": {
      "bitFirst": true,
      "bitSecond": false,
      "bitThird": true
    },
    "writeDates": {
      // Write time in YYYYMM format in UTC.
      "yyyymmFirst": 202401,
      // Note that yyyymmSecond is not set because bitSecond is false.
      "yyyymmThird": 202310
    }
  }
}

deviceRecall 分为两个字段

  • values:召回您之前为此设备设置的位值。
  • writeDates:召回精确到年和月的 UTC 位写入日期。每次将召回位设置为 true 时,召回位的写入日期都会更新;当位设置为 false 时,该日期将移除。

如果设备召回信息不可用,设备召回值将为空

"deviceIntegrity": {
  "deviceRecognitionVerdict": ["MEETS_DEVICE_INTEGRITY"],
  "deviceRecall": {
    "values": {},
    "writeDates": {}
  }
}

账号详情字段

accountDetails 字段包含一个值 appLicensingVerdict,它表示登录到设备上的用户账号的应用的 Google Play 许可状态。如果用户账号拥有该应用的 Play 许可,则表示他们从 Google Play 下载或购买了该应用。

accountDetails: {
  // This field can be LICENSED, UNLICENSED, or UNEVALUATED.
  appLicensingVerdict: "LICENSED"
}

appLicensingVerdict 可能包含以下任一值

LICENSED
用户拥有应用授权。换句话说,用户在设备上从 Google Play 安装或更新了您的应用。
UNLICENSED
用户没有应用授权。例如,当用户旁加载您的应用或未从 Google Play 获取该应用时,就会发生这种情况。您可以向用户显示 GET_LICENSED 对话框来解决此问题。
UNEVALUATED

未评估许可详情,因为遗漏了必要的要求。

这可能因多种原因发生,包括以下原因

  • 设备不够可信。
  • 设备上安装的应用版本对 Google Play 不可知。
  • 用户未登录 Google Play。

为了检查用户是否拥有您的应用授权,请验证 appLicensingVerdict 是否如预期,如以下代码段所示

Kotlin

val accountDetails = JSONObject(payload).getJSONObject("accountDetails")
val appLicensingVerdict = accountDetails.getString("appLicensingVerdict")

if (appLicensingVerdict == "LICENSED") {
    // Looks good!
}

Java

JSONObject accountDetails =
    new JSONObject(payload).getJSONObject("accountDetails");
String appLicensingVerdict = accountDetails.getString("appLicensingVerdict");

if (appLicensingVerdict.equals("LICENSED")) {
    // Looks good!
}

环境详情字段

您还可以选择接收有关环境的其他信号。应用访问风险会告知您的应用是否存在可能用于截屏、显示叠加层或控制设备的其他正在运行的应用。Play 保护机制裁定会告知您设备上是否启用了 Google Play 保护机制以及是否发现了已知恶意软件。

如果您已在 Google Play 管理中心选择启用应用访问风险裁定或 Play 保护机制裁定,则您的 API 响应将包含 environmentDetails 字段。environmentDetails 字段可包含两个值:appAccessRiskVerdictplayProtectVerdict

应用访问风险裁定

启用后,Play 完整性 API 载荷中的 environmentDetails 字段将包含新的应用访问风险裁定。

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: {
      appAccessRiskVerdict: {
          // This field contains one or more responses, for example the following.
          appsDetected: ["KNOWN_INSTALLED", "UNKNOWN_INSTALLED", "UNKNOWN_CAPTURING"]
      }
 }
}

如果评估了应用访问风险,appAccessRiskVerdict 包含 appsDetected 字段,其中包含一个或多个响应。这些响应根据检测到的应用的安装来源分为以下两组之一

  • Play 应用或系统应用:由 Google Play 安装或由设备制造商预加载到设备系统分区(由 FLAG_SYSTEM 标识)上的应用。此类应用的响应以 KNOWN_ 为前缀。

  • 其他应用:未由 Google Play 安装的应用。这不包括由设备制造商预加载到系统分区的应用。此类应用的响应以 UNKNOWN_ 为前缀。

可以返回以下响应

KNOWN_INSTALLEDUNKNOWN_INSTALLED
安装了与相应安装来源匹配的应用。
KNOWN_CAPTURINGUNKNOWN_CAPTURING
有正在运行的应用已启用权限,这些权限可以在您的应用运行时用于查看屏幕。这不包括设备上运行的 Google Play 已知的任何已验证的无障碍服务。
KNOWN_CONTROLLINGUNKNOWN_CONTROLLING
有正在运行的应用已启用权限,这些权限可用于控制设备并直接控制对您的应用的输入,并且可用于捕获您应用的输入和输出。这不包括设备上运行的 Google Play 已知的任何已验证的无障碍服务。
KNOWN_OVERLAYSUNKNOWN_OVERLAYS
有正在运行的应用已启用权限,这些权限可用于在您的应用上显示叠加层。这不包括设备上运行的 Google Play 已知的任何已验证的无障碍服务。
空值(空白值)

如果必要要求缺失,则不评估应用访问风险。在这种情况下,appAccessRiskVerdict 字段为空。这可能因多种原因发生,包括以下原因

  • 设备不够可信。
  • 设备外形规格不是手机、平板电脑或可折叠设备。
  • 设备未运行 Android 6(API 级别 23)或更高版本。
  • 设备上安装的应用版本对 Google Play 不可知。
  • 设备上的 Google Play 商店版本已过时。
  • 用户账号没有 Play 许可。
  • 使用标准请求时带上 verdictOptOut 参数。
  • 使用标准请求时带上了 Play 完整性 API 库版本,该版本尚未支持标准请求的应用访问风险。

应用访问风险会自动排除已通过 Google Play 增强型无障碍功能审核(由设备上的任何应用商店安装)的已验证无障碍服务。“排除”意味着设备上运行的已验证无障碍服务不会在应用访问风险裁定中返回 capturing、controlling 或 overlays 响应。要为您的无障碍功能应用请求增强型 Google Play 无障碍功能审核,请在 Google Play 上发布,确保您的应用清单中 isAccessibilityTool 标志设置为 true,或请求审核

应用访问风险裁定示例

下表提供了一些应用访问风险裁定示例及其含义(此表未列出所有可能的结果)

应用访问风险裁定响应示例 解读
appsDetected
["KNOWN_INSTALLED"]
安装的应用仅包含 Google Play 识别的应用或由设备制造商预加载到系统分区的应用。
没有正在运行的应用会导致出现 capturing、controlling 或 overlays 裁定。
appsDetected
["KNOWN_INSTALLED"、
"UNKNOWN_INSTALLED"、
"UNKNOWN_CAPTURING"]
安装了由 Google Play 安装的应用或由设备制造商预加载到系统分区的应用。
有其他正在运行的应用已启用权限,这些权限可用于查看屏幕或捕获其他输入和输出。
appsDetected
["KNOWN_INSTALLED"、
"KNOWN_CAPTURING"、
"UNKNOWN_INSTALLED"、
"UNKNOWN_CONTROLLING"]
有正在运行的 Play 应用或系统应用已启用权限,这些权限可用于查看屏幕或捕获其他输入和输出。
还有其他正在运行的应用已启用权限,这些权限可用于控制设备并直接控制对您的应用的输入。
appAccessRiskVerdict: {} 未评估应用访问风险,因为遗漏了必要的要求。例如,设备不够可信。

根据您的风险级别,您可以决定哪些裁定组合是可以接受的,以及您希望对哪些裁定采取行动。以下代码段演示了如何验证没有可能截屏或控制您的应用的应用正在运行的示例

Kotlin

val environmentDetails =
    JSONObject(payload).getJSONObject("environmentDetails")
val appAccessRiskVerdict =
    environmentDetails.getJSONObject("appAccessRiskVerdict")

if (appAccessRiskVerdict.has("appsDetected")) {
    val appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString()
    if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) {
        // Looks good!
    }
}

Java

JSONObject environmentDetails =
    new JSONObject(payload).getJSONObject("environmentDetails");
JSONObject appAccessRiskVerdict =
    environmentDetails.getJSONObject("appAccessRiskVerdict");

if (appAccessRiskVerdict.has("appsDetected")) {
    String appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString()
    if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) {
        // Looks good!
    }
}
修正应用访问风险裁定

根据您的风险级别,您可以决定在允许用户完成请求或操作之前对哪些应用访问风险裁定采取行动。检查应用访问风险裁定后,您可以向用户显示可选的 Google Play 提示。您可以显示 CLOSE_UNKNOWN_ACCESS_RISK 以要求用户关闭导致应用访问风险裁定的未知应用,或者显示 CLOSE_ALL_ACCESS_RISK 以要求用户关闭导致应用访问风险裁定的所有应用(已知和未知)。

Play 保护机制裁定

启用后,Play 完整性 API 载荷中的 environmentDetails 字段将包含 Play 保护机制裁定

environmentDetails: {
  playProtectVerdict: "NO_ISSUES"
}

playProtectVerdict 可能包含以下任一值

NO_ISSUES
Play 保护机制已开启,且未在设备上发现任何应用问题。
NO_DATA
Play 保护机制已开启,但尚未执行扫描。设备或 Play 商店应用可能最近已重置。
POSSIBLE_RISK
Play 保护机制已关闭。
MEDIUM_RISK
Play 保护机制已开启,并在设备上发现可能有害的应用。
HIGH_RISK
Play 保护机制已开启,并在设备上发现危险应用。
UNEVALUATED

未评估 Play 保护机制裁定。

这可能因多种原因发生,包括以下原因

  • 设备不够可信。
  • 用户账号没有 Play 许可。

使用 Play 保护机制裁定的指南

您的应用后端服务器可以根据裁定和您的风险承受能力决定如何操作。以下是一些建议和潜在的用户操作

NO_ISSUES
Play 保护机制已开启且未发现任何问题,因此用户无需执行任何操作。
POSSIBLE_RISKNO_DATA
收到这些裁定后,请要求用户检查 Play 保护机制是否已开启并已执行扫描。NO_DATA 应仅在极少数情况下出现。
MEDIUM_RISKHIGH_RISK
根据您的风险承受能力,您可以要求用户启动 Play 保护机制并针对 Play 保护机制警告采取行动。如果用户无法满足这些要求,您可以阻止他们进行服务器操作。