发出传统 API 请求

如果您只打算发出适用于大多数开发者的标准 API 请求,可以直接跳至完整性判定部分。本页介绍了如何发出传统 API 请求(Android 4.4 [API 级别 19] 或更高版本均支持)以获取完整性判定结果。

注意事项

比较标准请求和传统请求

您可以根据应用的安全和反滥用需求,发出标准请求、传统请求或结合利用这两种请求。标准请求适用于所有应用和游戏,可用于检查任何操作或服务器调用是否真实,同时将针对可重放和渗漏的一些防护任务委托给 Google Play。发出传统请求的开销更大,并且您要负责正确实现这些请求,以防发生渗漏和特定类型的攻击。发出传统请求的频率应低于标准请求,例如,偶尔发出一次性的请求,用于检查某项重要性或敏感性非常高的操作是否真实。

下表重点介绍了这两种请求之间的主要区别:

标准 API 请求 传统 API 请求
前提条件
所需的最低 Android SDK 版本 Android 5.0(API 级别 21)或更高版本 Android 4.4(API 级别 19)或更高版本
Google Play 要求 Google Play 商店和 Google Play 服务 Google Play 商店和 Google Play 服务
集成详情
需要 API 预热 ✔️(几秒)
典型的请求延迟时间 几百毫秒 几秒
潜在请求频率 频繁(对任何操作或请求进行按需检查) 不频繁(针对最重要的操作或最敏感的请求进行一次性检查)
超时 大多数预热需时短于 10 秒,但会涉及到服务器调用,因此建议设置较长的超时(例如 1 分钟)。在客户端处理判定请求 大多数请求需时短于 10 秒,但会涉及服务器调用,因此建议设置较长的超时(例如 1 分钟)
完整性判定令牌
包含设备、应用和账号详情 ✔️ ✔️
令牌缓存 由 Google Play 保护的设备端缓存 不推荐
通过 Google Play 服务器解密和验证令牌 ✔️ ✔️
典型的服务器到服务器解密请求延迟时间 几十毫秒,99.9% 的可用性 几十毫秒,99.9% 的可用性
在安全的服务器环境中本地解密和验证令牌 ✔️
在客户端解密和验证令牌
完整性判定新鲜度 由 Google Play 进行一些自动缓存和刷新 针对每个请求重新计算所有判定结果
限制
每个应用每天可发出请求的次数 默认为 10,000 次(可以申请增加) 默认为 10,000 次(可以申请增加)
每个应用实例每分钟可发出的请求次数 预热:每分钟 5 次
完整性令牌:无公开限制*
完整性令牌:每分钟 5 次
保护
缓解篡改和类似攻击 使用 requestHash 字段 根据请求数据结合使用 nonce 字段和内容绑定
缓解重放和类似攻击 由 Google Play 的自动缓解 结合使用 nonce 字段和服务器端逻辑

* 在值较高时,所有请求(包括没有公开限制的请求)都受非公开防御限制的约束

降低发出传统请求的频率

生成完整性令牌会耗费时间、数据流量和电池电量,而且每个应用每天可以发出传统请求的次数设有上限。因此,只有当您想要获得较标准请求更高的额外保障时,才能发出传统请求以检查最重要或最敏感的操作是否真实。您不应针对高频或不重要的操作发出传统请求。切勿只要应用转到前台就发出传统请求,也不要在后台每隔几分钟就发出一次传统请求,还应避免同时从大量设备进行调用。如果应用发出过多的传统请求调用,则可能会受到限制,从而保护用户免受不正确实现的影响。

避免缓存判定结果

缓存判定结果会增加渗漏和重放等攻击的风险,在这种情况下,正常判定结果会在不受信任的环境中被重复使用。如果您考虑发出传统请求,然后将其缓存以供日后使用,建议您改为按需执行标准请求。标准请求会涉及设备上进行一些缓存,但 Google Play 会使用额外的保护技术来缓解重放攻击和渗漏的风险。

使用 Nonce 字段保护传统请求

Play Integrity API 提供了一个名为 nonce 的字段,可用于进一步保护应用免受某些攻击的侵害,例如重放攻击和篡改攻击。Play Integrity API 会在带签名的完整性响应中返回您在此字段中设置的值。请严格遵守关于如何生成 Nonce 的指南,以保护应用免遭攻击。

使用指数退避算法重试传统请求

环境条件(例如互联网连接不稳定或设备过载)可能会导致设备完整性检查失败。这样可能会导致系统不为本来可信的设备生成任何标签。为了缓解这些情况,请包含使用指数退避算法进行重试的选项。

概览

概要介绍 Play Integrity API 设计的序列图

当用户在您的应用中执行您要通过完整性检查保护的重要操作时,请完成以下步骤:

  1. 应用的服务器端后端生成一个唯一的值,并将其发送到客户端逻辑。剩余步骤会将该逻辑称为您的“应用”。
  2. 您的应用会基于唯一值以及重要操作的内容创建 nonce,然后会调用 Play Integrity API,并传入 nonce
  3. 您的应用收到来自 Play Integrity API 的已签名且加密的判定。
  4. 应用将已签名且加密的判定结果传递到应用后端。
  5. 应用后端将判定结果发送到 Google Play 服务器。Google Play 服务器解密并验证判定结果,并将结果返回给应用的后端。
  6. 应用后端根据令牌载荷中包含的信号来决定如何继续操作。
  7. 应用后端将决策结果发送到您的应用。

生成 Nonce

使用 Play Integrity API 保护应用中的某项操作时,您可以利用 nonce 字段来缓解特定类型的攻击,例如中间人 (PITM) 篡改攻击和重放攻击。Play Integrity API 会在带签名的完整性响应中返回您在此字段中设置的值。

nonce 字段中设置的值必须采用正确的格式:

  • String
  • 具有网址安全性
  • 编码为 Base64 且没有换行
  • 最少 16 个字符
  • 最多 500 个字符

以下是在 Play Integrity API 中使用 nonce 字段的一些常用方法。为了获得最强大的 nonce 保护,您可以组合使用以下方法。

添加请求哈希值以防范篡改

您可以在传统 API 请求中使用 nonce 参数,方法类似于在标准 API 请求中使用 requestHash 参数,以保护请求的内容不被篡改。

请求完整性判定时:

  1. 计算正在发生的用户操作或服务器请求中的所有关键请求参数(例如,稳定请求序列化的 SHA256)的摘要。
  2. 使用 setNoncenonce 字段设置为计算出的摘要的值。

收到完整性判定结果后:

  1. 解码并验证完整性令牌,然后从 nonce 字段获取摘要。
  2. 采用与应用中相同的方式计算请求摘要(例如,稳定请求序列化的 SHA256)。
  3. 比较应用端摘要和服务器端摘要。如果二者不符,则表示请求不可信。

添加唯一值以防范重放攻击

为防止恶意用户重复使用之前来自 Play Integrity API 的响应,您可以使用 nonce 字段对每条消息进行唯一标识。

请求完整性判定时:

  1. 获取一个全局唯一值,防止恶意用户做出预测。例如,在服务器端生成的加密安全随机数就可以,会话 ID 或事务 ID 等既有 ID 也可以。一种更简单的变通方式是在设备上生成随机数,不过方式的安全性较低。我们建议创建不小于 128 位的值。
  2. 调用 setNonce() 以将 nonce 字段设置为第 1 步中的唯一值。

收到完整性判定结果后:

  1. 解码并验证完整性令牌,然后从 nonce 字段获取唯一值。
  2. 如果第 1 步中的值是在服务器上生成的,请检查收到的唯一值是否是生成的值之一,以及该值是否是第一次使用(服务器需要在合理期限内保留生成的值的记录)。如果收到的唯一值已被使用或未在记录中显示,则拒绝相应请求
  3. 否则,如果唯一值是在设备上生成的,请检查收到的值是否是第一次使用(服务器需要在合理期限内保留已显示的值的记录)。如果收到的唯一值已被使用,则拒绝相应请求。

结合利用这两种保护措施来防范篡改攻击和重放攻击(推荐)

您可以使用 nonce 字段同时防范篡改攻击和重放攻击。为此,请按上文所述生成唯一值,并将其包含在请求中。然后,计算请求哈希值,确保将唯一值包含为哈希值的一部分。将这两种方法结合到一起的实现方式如下:

请求完整性判定时:

  1. 用户发起重要操作。
  2. 获取此操作的唯一值,如添加唯一值以防范重放攻击部分中所述。
  3. 准备好要保护的消息。在消息中添加第 2 步中的唯一值。
  4. 应用会计算它要保护的消息的摘要,如添加请求哈希值以防范篡改部分中所述。由于消息包含相应唯一值,因此该唯一值是哈希值的一部分。
  5. 使用 setNonce()nonce 字段设置为上一步中计算出的摘要。

收到完整性判定结果后:

  1. 从请求中获取唯一值
  2. 解码并验证完整性令牌,然后从 nonce 字段获取摘要。
  3. 添加请求哈希值以防范篡改部分中所述,在服务器端重新计算摘要,并检查其是否与从完整性令牌中获取的摘要相符。
  4. 添加唯一值以防范重放攻击部分中所述,检查唯一值的有效性。

以下序列图展示了服务器端 nonce 的相关步骤:

显示如何防范篡改攻击和重放攻击的序列图

请求完整性判定

生成 nonce 后,您可以向 Google Play 请求完整性判定。为此,请完成以下步骤:

  1. 创建一个 IntegrityManager,如以下示例所示。
  2. 构造一个 IntegrityTokenRequest,通过关联构建器中的 setNonce() 方法提供 nonce。在 Google Play 之外专门分发的应用和 SDK 还必须通过 setCloudProjectNumber() 方法指定其 Google Cloud 项目编号。Google Play 中的应用已在 Play 管理中心关联到 Cloud 项目,因此不需要在请求中设置 Cloud 项目编号。
  3. 使用该管理器调用 requestIntegrityToken(),并提供 IntegrityTokenRequest

Kotlin

// Receive the nonce from the secure server.
val nonce: String = ...

// Create an instance of a manager.
val integrityManager =
    IntegrityManagerFactory.create(applicationContext)

// Request the integrity token by providing a nonce.
val integrityTokenResponse: Task<IntegrityTokenResponse> =
    integrityManager.requestIntegrityToken(
        IntegrityTokenRequest.builder()
             .setNonce(nonce)
             .build())

Java

import com.google.android.gms.tasks.Task; ...

// Receive the nonce from the secure server.
String nonce = ...

// Create an instance of a manager.
IntegrityManager integrityManager =
    IntegrityManagerFactory.create(getApplicationContext());

// Request the integrity token by providing a nonce.
Task<IntegrityTokenResponse> integrityTokenResponse =
    integrityManager
        .requestIntegrityToken(
            IntegrityTokenRequest.builder().setNonce(nonce).build());

Unity

IEnumerator RequestIntegrityTokenCoroutine() {
    // Receive the nonce from the secure server.
    var nonce = ...

    // Create an instance of a manager.
    var integrityManager = new IntegrityManager();

    // Request the integrity token by providing a nonce.
    var tokenRequest = new IntegrityTokenRequest(nonce);
    var requestIntegrityTokenOperation =
        integrityManager.RequestIntegrityToken(tokenRequest);

    // Wait for PlayAsyncOperation to complete.
    yield return requestIntegrityTokenOperation;

    // Check the resulting error code.
    if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError)
    {
        AppendStatusLog("IntegrityAsyncOperation failed with error: " +
                requestIntegrityTokenOperation.Error);
        yield break;
    }

    // Get the response.
    var tokenResponse = requestIntegrityTokenOperation.GetResult();
}

原生

/// Create an IntegrityTokenRequest opaque object.
const char* nonce = RequestNonceFromServer();
IntegrityTokenRequest* request;
IntegrityTokenRequest_create(&request);
IntegrityTokenRequest_setNonce(request, nonce);

/// Prepare an IntegrityTokenResponse opaque type pointer and call
/// IntegerityManager_requestIntegrityToken().
IntegrityTokenResponse* response;
IntegrityErrorCode error_code =
        IntegrityManager_requestIntegrityToken(request, &response);

/// ...
/// Proceed to polling iff error_code == INTEGRITY_NO_ERROR
if (error_code != INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.
/// Note, the polling shouldn't block the thread where the IntegrityManager
/// is running.

IntegrityResponseStatus response_status;

/// Check for error codes.
IntegrityErrorCode error_code =
        IntegrityTokenResponse_getStatus(response, &response_status);
if (error_code == INTEGRITY_NO_ERROR
    && response_status == INTEGRITY_RESPONSE_COMPLETED)
{
    const char* integrity_token = IntegrityTokenResponse_getToken(response);
    SendTokenToServer(integrity_token);
}
/// ...
/// Remember to free up resources.
IntegrityTokenRequest_destroy(request);
IntegrityTokenResponse_destroy(response);
IntegrityManager_destroy();

解密和验证完整性判定

在您请求完整性判定时,Play Integrity API 会提供已签名的响应令牌。您在请求中包含的 nonce 会成为响应令牌的一部分。

令牌格式

令牌是指嵌套的 JSON 网络令牌 (JWT),即 JSON 网络签名 (JWS)JSON 网络加密 (JWE)。JWE 和 JWS 组件使用紧凑的序列化来表示。

加密/签名算法在各种 JWT 实现中得到了很好的支持:

  • JWE 对 alg 使用 A256KW,对 enc 使用 A256GCM

  • JWS 使用 ES256。

在 Google 服务器上进行解密和验证(推荐做法)

借助 Play Integrity API,您可以在 Google 服务器上解密和验证完整性判定结果,从而提高应用的安全性。为此,请完成以下步骤:

  1. 创建服务账号 (位于与您的应用相关联的 Google Cloud 项目中)。
  2. 在应用服务器上,使用 playintegrity 范围从服务账号凭据中提取访问令牌,然后发出以下请求:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. 读取 JSON 响应。

在本地进行解密和验证

如果您选择管理和下载响应加密密钥,可以在您自己的安全服务器环境中解密和验证返回的令牌。您可以使用 IntegrityTokenResponse#token() 方法获取返回的令牌。

以下示例展示了如何在应用的后端将用于在 Play 管理中心进行签名验证的 AES 密钥和 DER 编码的 EC 公钥,解码为特定于语言(在本例中为 Java 编程语言)的密钥。请注意,这些密钥是使用默认标记以 base64 编码的。

Kotlin

// base64OfEncodedDecryptionKey is provided through Play Console.
var decryptionKeyBytes: ByteArray =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT)

// Deserialized encryption (symmetric) key.
var decryptionKey: SecretKey = SecretKeySpec(
    decryptionKeyBytes,
    /* offset= */ 0,
    AES_KEY_SIZE_BYTES,
    AES_KEY_TYPE
)

// base64OfEncodedVerificationKey is provided through Play Console.
var encodedVerificationKey: ByteArray =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT)

// Deserialized verification (public) key.
var verificationKey: PublicKey = KeyFactory.getInstance(EC_KEY_TYPE)
    .generatePublic(X509EncodedKeySpec(encodedVerificationKey))

Java

// base64OfEncodedDecryptionKey is provided through Play Console.
byte[] decryptionKeyBytes =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT);

// Deserialized encryption (symmetric) key.
SecretKey decryptionKey =
    new SecretKeySpec(
        decryptionKeyBytes,
        /* offset= */ 0,
        AES_KEY_SIZE_BYTES,
        AES_KEY_TYPE);

// base64OfEncodedVerificationKey is provided through Play Console.
byte[] encodedVerificationKey =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT);
// Deserialized verification (public) key.
PublicKey verificationKey =
    KeyFactory.getInstance(EC_KEY_TYPE)
        .generatePublic(new X509EncodedKeySpec(encodedVerificationKey));

接下来,使用这些密钥先解密完整性令牌(JWE 部分),然后再验证和提取嵌套的 JWS 部分。

Kotlin

val jwe: JsonWebEncryption =
    JsonWebStructure.fromCompactSerialization(integrityToken) as JsonWebEncryption
jwe.setKey(decryptionKey)

// This also decrypts the JWE token.
val compactJws: String = jwe.getPayload()

val jws: JsonWebSignature =
    JsonWebStructure.fromCompactSerialization(compactJws) as JsonWebSignature
jws.setKey(verificationKey)

// This also verifies the signature.
val payload: String = jws.getPayload()

Java

JsonWebEncryption jwe =
    (JsonWebEncryption)JsonWebStructure
        .fromCompactSerialization(integrityToken);
jwe.setKey(decryptionKey);

// This also decrypts the JWE token.
String compactJws = jwe.getPayload();

JsonWebSignature jws =
    (JsonWebSignature) JsonWebStructure.fromCompactSerialization(compactJws);
jws.setKey(verificationKey);

// This also verifies the signature.
String payload = jws.getPayload();

生成的载荷是包含完整性判定的纯文本令牌。