SafetyNet Attestation API

SafetyNet Attestation API 是一种反滥用 API,让应用开发者能够评估运行其应用的 Android 设备。该 API 应该用作滥用检测系统的一部分,以帮助确定您的服务器是否与在真实 Android 设备上运行的真实应用互动。

SafetyNet Attestation API 提供采用加密签名的证明,用于评估设备的完整性。为了创建证明,该 API 会检查设备的软件和硬件环境,以查找是否存在完整性问题,并将相应数据与已获批 Android 设备的参考数据进行比较。生成的证明会绑定到调用方应用提供的 Nonce。该证明还包含生成时间戳以及发起请求的应用的元数据。

该 API 用于实现以下用例:

  • 充当独立的反滥用或应用安全机制。请将该 API 与我们发布的应用安全性最佳做法以及您的一套产品专用反滥用信号结合使用。
  • 在设备未连接到互联网时运行。在此类情况下,该 API 会返回错误。
  • 直接在发起调用的应用中解读其响应。将所有反滥用决策逻辑移至您掌控的服务器中。
  • 提供有关系统修改的精细信号。该 API 提供各种布尔值,用于表示不同程度的系统完整性。
  • 纳入应用专属用例的信号,例如设备标识符、GPS 模拟状态和屏幕锁定状态。
  • 替换或实现强健的 DRM 检查。
  • 仅用于检查设备是否启用了 root 权限,因为该 API 旨在检查设备的总体完整性。

概览

SafetyNet Attestation API 使用以下工作流程:

  1. SafetyNet Attestation API 收到您的应用发起的调用。该调用包含一个 Nonce。
  2. SafetyNet Attestation 服务评估运行时环境,并向 Google 的服务器请求已签名的评估结果证明。
  3. Google 的服务器将已签名的证明发送给设备上的 SafetyNet Attestation 服务。
  4. SafetyNet Attestation 服务将此已签名的证明返回给您的应用。
  5. 您的应用将已签名的此证明转发给您的服务器。
  6. 服务器会验证响应,并将其用于反滥用决策。您的服务器会向您的应用传达其发现的结果。

此流程的图形描述如图 1 所示:

图 1. SafetyNet Attestation API 协议

注意:其他文档和核对清单

在 SafetyNet Attestation API 的整个初始化、配置和激活过程中,除了这份主要文档之外,另请了解并遵循以下建议:

获取 API 密钥

要调用 SafetyNet Attestation API 的方法,您必须使用 API 密钥。要创建并嵌入此密钥,请完成以下步骤:

  1. 转到 Google API 控制台中的页面。
  2. 搜索并选择 Android Device Verification API。系统会显示 Android Device Verification API 信息中心屏幕。
  3. 如果该 API 尚未启用,请点击启用
  4. 如果显示创建凭据按钮,请点击该按钮,以生成 API 密钥。如果没有显示该按钮,请点击所有 API 凭据下拉列表,然后选择与您的已启用 Android Device Verification API 的项目相关联的 API 密钥。
  5. 在左侧边栏中,点击凭据。复制显示的 API 密钥
  6. 在调用 SafetyNetClient 类的 attest() 方法时使用此 API 密钥。

创建此 API 密钥后,加入 SafetyNet API 客户端邮寄名单

API 配额和监控

重要提示:调用 SafetyNet Attestation API 的默认配额(每个项目)为每天 10000 次请求。

如果您的应用需要增加配额,请先提出请求,然后再面向用户部署应用。为此,请按以下步骤操作:

  1. 仔细阅读所提供的有关此 API 的文档。如果请求不符合推荐的最佳做法,则可能会被拒。
  2. 填写配额请求表单
  3. 等待确认电子邮件,收到此电子邮件即表明您的请求已得到处理。我们通常会在 2-3 个工作日内处理您的请求。

注意:无论您为项目配置的配额是多少,每个应用实例每分钟最多可以发出 5 次请求。如果超过此限额,在那一分钟内发出的后续请求都会返回错误。

实现应用的重试机制时,请谨记这一行为。

无论您应用的 API 配额是多少,我们都建议您设置配额监控和提醒

检查 Google Play 服务版本

您必须先确保用户设备上安装了正确的 Google Play 服务版本,然后再使用 SafetyNet Attestation API。如果安装的版本不正确,您的应用可能会在调用该 API 后停止响应。如果您的应用检测到安装的版本不正确,您应该要求用户更新其设备上的 Google Play 服务应用

要检查安装的 Google Play 服务版本与您使用的 Android SDK 版本是否兼容,请调用 isGooglePlayServicesAvailable() 方法,如以下代码段所示:

if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
            == ConnectionResult.SUCCESS) {
      // The SafetyNet Attestation API is available.
    } else {
      // Prompt user to update Google Play services.
    }
    

在运行 Google Play 服务 v13.0 及更高版本的设备上,SafetyNet Attestation API 还支持应用受到限制的 API 密钥。此功能可降低意外或未经授权使用配额受限的 API 密钥的风险。要使用此可选功能,请确认设备上 Google Play 服务的最低版本至少为 v13.0,如以下代码段所示:

if (GoogleApiAvailability.getInstance()
        .isGooglePlayServicesAvailable(context, 13000000) ==
        ConnectionResult.SUCCESS) {
      // The SafetyNet Attestation API is available.
    } else {
      // Prompt user to update Google Play Services.
    }
    

请求 SafetyNet 证明

当您在 Google API 控制台获取对 Android Device Verification API 有效的 API 密钥后,您的应用就可以使用 SafetyNet Attestation 服务了。为此,请完成以下步骤:

  1. 获取一个 Nonce。
  2. 请求 SafetyNet 证明。
  3. 将响应传输到您的服务器。
  4. 使用服务器上的响应以及其他反滥用信号来管理应用的行为。

为使您的应用能迅速响应,请在应用的主执行线程之外执行这些步骤。要详细了解如何创建单独的执行线程,请参阅将指令发送到多个线程

您应该执行这项检查,为在您的应用中执行的所有关键操作(包括登录、购买事件和获取新应用内商品)提供保护。调用 SafetyNet Attestation API 会导致延迟、移动数据用量和电池用量增加,因此最好在保证安全性和易用性之间找到平衡。例如,您可以选择在登录时请求 SafetyNet 证明,然后最多每 30 分钟重新检查一次。您还可以让服务器决定应用何时请求证明,使攻击者更加难以预测您的检查时间。

获取 Nonce

调用 SafetyNet Attestation API 时,您必须传入一个Nonce。生成的证明会包含此 Nonce,这样您就能确定证明属于您的 API 调用,而不是攻击者重放的。

用于 SafetyNet 请求的 Nonce 的长度应至少为 16 个字节。您的 Nonce 应该具有可变性,从而确保绝不重复使用同一 Nonce。最佳做法是,Nonce 部分派生自发送到服务器的数据。例如,用户名的哈希值与请求时间戳组合到一起形成 Nonce。

重要提示:在 Nonce 中添加尽可能多的数据。这样一来,攻击者就更加难以实施重放攻击了。例如,从用户名派生 Nonce 可限制攻击者对同一帐号实施重放攻击。不过,从购买事件的所有详细信息中派生 Nonce 会将 API 的响应消息限制为仅用于该购买事件。

收到 API 发来的带签名的响应后,请务必将带签名的响应中的 Nonce 与根据发送到您服务器的消息的其余部分重建的 Nonce 进行比较。这项检查可确保攻击者无法将从良好设备获取的已签名证明重复用于其他恶意创建的请求。

如需详细了解如何使用加密函数,请参阅有关如何使用加密的指南。

请求证明

与 Google Play 服务建立连接并创建 Nonce 后,您就可以发出 SafetyNet 证明请求了。您的请求可能不会立即得到响应,因此最好设置回调监听器,以处理该服务发出的响应。以下代码段中显示了示例监听器:

Kotlin

    SafetyNet.getClient(this).attest(nonce, API_KEY)
        .addOnSuccessListener(this) {
            // Indicates communication with the service was successful.
            // Use response.getJwsResult() to get the result data.
        }
        .addOnFailureListener(this) { e ->
            // An error occurred while communicating with the service.
            if (e is ApiException) {
                // An error with the Google Play services API contains some
                // additional details.
                val apiException = e as ApiException

                // You can retrieve the status code using the
                // apiException.statusCode property.
            } else {
                // A different, unknown type of error occurred.
                Log.d(FragmentActivity.TAG, "Error: " + e.message)
            }
        }
    

Java

    // The nonce should be at least 16 bytes in length.
    // You must generate the value of API_KEY in the Google APIs dashboard.
    SafetyNet.getClient(this).attest(nonce, API_KEY)
        .addOnSuccessListener(this,
            new OnSuccessListener<SafetyNetApi.AttestationResponse>() {
                @Override
                public void onSuccess(SafetyNetApi.AttestationResponse response) {
                    // Indicates communication with the service was successful.
                    // Use response.getJwsResult() to get the result data.
                }
            })
        .addOnFailureListener(this, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // An error occurred while communicating with the service.
                if (e instanceof ApiException) {
                    // An error with the Google Play services API contains some
                    // additional details.
                    ApiException apiException = (ApiException) e;
                    // You can retrieve the status code using the
                    // apiException.getStatusCode() method.
                } else {
                    // A different, unknown type of error occurred.
                    Log.d(TAG, "Error: " + e.getMessage());
                }
            }
        });
    

onSuccess() 方法表示与服务之间的通信已成功建立,但并不表示设备已通过 SafetyNet 证明。下一部分介绍了如何解读证明结果及验证其完整性。

将 SafetyNet 证明响应传输到您的服务器

当您的应用与 SafetyNet 进行通信时,该服务会提供包含 SafetyNet 证明结果的响应,同时提供其他有助于您验证消息完整性的信息。证明结果作为 SafetyNetApi.AttestationResponse 对象提供。使用此对象的 getJwsResult() 方法可获取请求的数据。响应格式为 JSON Web Signature (JWS)

将 JWS 对象发送回您的服务器,以供验证和使用。

使用服务器上的 SafetyNet 证明响应

以下 JWS 摘录展示了负载数据的格式和示例内容:

{
      "timestampMs": 9860437986543,
      "nonce": "R2Rra24fVm5xa2Mg",
      "apkPackageName": "com.package.name.of.requesting.app",
      "apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the
                                      certificate used to sign requesting app"],
      "ctsProfileMatch": true,
      "basicIntegrity": true,
    }
    

已签名的证明的负载通常包含以下字段:

响应的时间戳

  • timestampMs:Google 的服务器生成 JWS 响应消息的时间,以自 UNIX 纪元以来的毫秒数表示。

发起调用的应用提供的数据

  • nonce:发起调用的应用传递到 API 的一次性令牌。

发起调用的应用的相关数据

  • apkPackageName:发起调用的应用的软件包名称。
  • apkCertificateDigestSha256:发起调用的应用的签名证书的 SHA-256 哈希的 base-64 编码表示法

完整性判定

  • ctsProfileMatch:以比较严格的标准判定设备的完整性。如果 ctsProfileMatch 的值为 true,则表示运行您应用的设备的配置文件与已通过 Android 兼容性测试的设备的配置文件相符。
  • basicIntegrity:以比较宽松的标准判定设备的完整性。如果只有 basicIntegrity 的值为 true,则表示运行您应用的设备可能没有被篡改。不过,该设备不一定已通过 Android 兼容性测试。

    如需详细了解 Android 兼容性测试,请参阅设计 Android 设备、Android 兼容性和兼容性测试套件 (CTS)

可选字段

  • error:与当前 API 请求相关的编码错误信息。
  • advice:关于如何让设备恢复良好状态的建议。

潜在完整性判定

JWS 消息包含以下 2 个参数,用于表示设备兼容性的检查结果:ctsProfileMatchbasicIntegrity。运行您应用的设备的状态会对每个参数值造成影响,如表 1 所示:

表 1. 设备状态如何影响 basicIntegrityctsProfileMatch 的值的示例

设备状态 ctsProfileMatch 的值 basicIntegrity 的值
设备真实且经过认证,并通过了 CTS 测试 true true
设备经过认证,且引导加载程序已解锁 false true
设备真实但未经过认证,例如制造商未申请认证时 false true
设备具有自定义 ROM(未启用 root 权限) false true
模拟器 false false
无设备(例如协议模拟脚本) false false
设备出现系统完整性受损的迹象,其中一种迹象表明可能出现 root 权限问题 false false
设备出现其他主动攻击的迹象,例如 API 挂接 false false

错误情形

JWS 消息还会显示以下几种类型的错误情况:

  • null 结果表示未成功完成服务调用。
  • JWS 中的 error 参数表示发生了错误,例如网络错误或攻击者伪造的错误。大多数错误是暂时的,当您对该服务发起另一个调用后,这些错误应该就不会显示了。您可能需要重试几次,每次重试之间的延迟会不断增加。
  • 如果设备遭到了篡改,也就是说,如果响应中的 basicIntegrity 设置为 false,则判定可能不包含发起调用的应用的相关数据,例如 apkPackageNameapkCertificateDigestSha256。如果我们的系统无法可靠地确定发起调用的应用,就会出现这种情况。

如果已签名的证明报告错误,该怎么做?

  • 重试。合法设备上的错误是暂时的,当您对该服务发起另一个调用后,这些错误应该就不会显示了。
  • 确认您的应用在受影响的设备上每秒调用 API 的次数不超过 5 次,并确认您项目的 API 配额尚未用尽。
  • 假设可能是攻击者有意触发错误情形,以仿冒其活动。

针对通过未来检查给出的建议

如果有,advice 参数会提供相关信息,帮助解释 SafetyNet Attestation API 为何在特定结果中将 ctsProfileMatchbasicIntegrity 设置为 false。该参数的值包含字符串列表,如下例中所示:

{"advice": "LOCK_BOOTLOADER,RESTORE_TO_FACTORY_ROM"}
    

在您的应用中,您可以将 advice 参数中的值解读为便于用户理解的消息,以帮助用户通过今后的 SafetyNet 证明,如以下列表所示:

LOCK_BOOTLOADER
用户应锁定其设备的引导加载程序。
RESTORE_TO_FACTORY_ROM
用户应将其设备恢复到简洁的出厂 ROM。

验证 SafetyNet 证明响应

您应该采取措施,以确保 SafetyNet 证明响应确实来自 SafetyNet 服务并包含与您的请求匹配的数据。

要验证 JWS 消息的来源,请完成以下步骤:

  1. 从 JWS 消息中提取 SSL 证书链。
  2. 验证 SSL 证书链并使用 SSL 主机名匹配来验证叶证书是否已颁发给主机名 attest.android.com
  3. 使用证书来验证 JWS 消息的签名。
  4. 检查 JWS 消息的数据,确保这些数据与原始请求中的数据匹配。具体而言,确保时间戳已经过验证,且应用签名证书的 Nonce、软件包名称和哈希值与预期值相符。

您应该使用标准加密解决方案来验证 JWS 语句,例如 android-play-safetynet 示例 API 用法(可在 GitHub 上获取)中的解决方案。

在初始测试和开发期间(但未进入生产环境),您可以调用在线 API 来验证 JWS 语句的签名。此外,android-play-safetynet 示例 API 用法(可在 GitHub 中获取)中也展示了这一过程。请注意,在线验证 API 仅用于早期阶段测试,且您每天的固定请求配额为 10000 次。

重要提示:在线验证 API 仅用于验证 JWS 消息是否已获得 SafetyNet Attestation API 的服务器的签名。此在线 API 无法验证负载中的字段是否与您的服务预期的值相符。

针对意外情况进行规划

我们建议您规划使用情况,以便将变更和服务中断考虑在内。

API 变更
在判定过程中,可能会随时出现新(实验性)字段。请确保这些额外字段不会中断您的解析器或使用逻辑。具体来说,在 SafetyNet API 客户端邮寄名单中公布实验性字段之前,请勿依赖这些字段。
SafetyNet Attestation API 不可用

在极少数情况下,SafetyNet Attestation API 会无法使用,因此强烈建议使用此 API 的用户开发出服务器端功能来动态控制对该 API 是否可用的依赖,以及此 API 及其响应的质量。

典型策略应该能够动态指示您的应用停止调用此 API 以及基于设备和基于用户的白名单,从而针对特定类别的设备和用户忽略 SafetyNet Attestation API 结果。

示例代码

如需获得有关使用 SafetyNet API 的更多指导,请访问 GitHub 查看相关示例代码

公告列表

我们强烈建议加入 SafetyNet API 客户端邮寄名单,以接收有关 SafetyNet Attestation API 的动态更新。

反馈

请考虑提供您对此 API 的反馈。我们会根据您的反馈优先开发此 API 的新特性和功能。

了解详情

要详细了解使用 SafetyNet Attestation API 时的最佳做法,请参阅以下链接:

附加服务条款

访问或使用 SafetyNet API 即表示您同意 Google API 服务条款及这些附加条款。请先阅读并了解所有适用的条款及政策,然后再使用这些 API。

SafetyNet 服务条款

与通过实际观察大量收集的数据一样,有可能存在误报和漏报情况。我们会尽我们所知呈现数据。我们会对检测机制进行大量测试以确保准确性,而且我们会尽全力逐步改善这些方法,以确保它们继续保持准确性。

您同意遵守所有适用的法律、法规和第三方权利(包括但不限于与数据或软件导入或导出相关的法律、隐私权和当地法律)。您不得使用这些 API 怂恿或宣扬非法活动或侵犯第三方权利。您不得违反 Google(或其关联公司)的任何其他服务条款。

您承认并理解,SafetyNet API 在运行时会收集各种软硬件信息,如设备数据、应用数据和 SafetyNet 证明结果等,并会将这些数据发送给 Google 进行分析。根据 Google API 服务条款第 3(d) 条的规定,您同意使用这些 API 即表示您负责就此类数据的收集及与 Google 共享提供任何必要的声明或同意。