实施Apple的设备检查应用程序证明协议
#安全 #csharp #ios #swift

在为应用程序创建后端时,您可能需要保护您的API,以确保除合法设备上运行的合法应用程序以外的其他代理无法使用该应用程序。这通过仅限于合法应用程序来降低API攻击表面。

作为开发人员,API通常受到用户身份验证和授权的保护;但是,在没有用户身份验证的情况下,您的API受到未保护。

在许多论坛上提出的建议,可以通过使用秘密“ API键”配置您的应用程序来保护API,这是最佳选择从您的应用程序包中提取)。

两者的答案是“我如何将API免于非法客户?”和“我应该如何保护未经身心的客户?”,“应用程序证明” 是! ðð¥³

Apple和Android具有应用程序证明的解决方案,但本文将重点介绍Apple的Device Check App Attest

基本流程

从应用程序中获取的数字证明“建立您的应用程序的完整性”文件显示,应用程序中涉及三个高级组件;

  • 您的应用程序,
  • 您的服务器,
  • 和App Actest服务。

涉及的涉及更多的东西,还有更多有关高级组件的信息。

High-Level App Attest Architecture

至关重要的是,如上图所示,“应用程序证明”服务是一种互联网暴露的服务,可能会不时失败或超时。

此外,设备上的Secure Enclave用于防止“您的应用”直接访问用于使用应用程序的私钥。

Basic Protocol Flow

  1. 您的服务器提供一串随机字符串,代表您的应用程序的挑战。
  2. 您的应用使用设备检查API创建一个加密密钥对。该密钥位于设备的“安全飞地”中,并且该操作用标识符字符串对该公共/私有密钥对的响应(密钥ID字符串是公共密钥的SHA256哈希)。
  3. 挑战数据的哈希以及密钥标识符通过Internet发送到Apple App App Appest 服务。该服务以证明数据为字符串。
  4. 证明数据已提供给您的服务器,服务器验证了证明数据。
  5. 向您的服务器的子序列请求伴随着主张数据,这些数据是在设备上生成的,并带有密钥标识符和请求主体。
  6. 然后对断言数据进行验证并处理请求。

让我们分解每个事物的工作方式。然后迭代该过程,尝试考虑身份验证和安全实践的HTTP标准。

警告:检查应用程序证明可用

上面未提及的“步骤零”是您应该在继续该过程之前检查应用程序的平台supports the App Attest service

let service = DCAppAttestService.shared
if service.isSupported {
    // Perform key generation and attestation.
}
// Continue with server access.

关于DCAppAttestService的烦人事实是,它在模拟器上不起作用。 ð£

文档指出它可以在以下平台版本上提供:

ios iPados macos tvos watchos
14.0+ 14.0+ 11.0+。 15.0+ 9.0+

产生挑战(.net)

响应您的应用程序中的某些请求,您的服务器用一串随机字节响应,这些字符称为“挑战”。

在安全性和密码学的情况下,随机不仅是 prng,应该在上是密码随机的,因此攻击者无法轻易预测序列。<<<<<<<<<<<<< /p>

在.net中,我们可以使用System.Security.Cryptography名称空间中的koude1,我们只需在类的静态实例上调用GetBytes(length)即可以所需长度的长度获得一个密码随机的字节序列。

生成公共/私钥对和ID(SWIFT)

Apple文档对此提供了详细信息,您将需要在本地缓存或存储中持续存在关键标识符响应,因此可以在此过程中以后使用。

生成的密钥对和标识符是运行应用程序的每个设备上的每个用户帐户唯一的。因此,您只需要在代码中存储一个密钥标识符。

生成密钥对的基本代码是:

service.generateKey { keyId, error in
    guard error == nil else { /* Handle the error. */ }

    // Cache keyId for subsequent operations.
}

我更喜欢使用async/await而不是回调,因此在这里进行了重构:

public func getKeyId() async throws -> String {
    if let keyId = try _keyId.value {
        return keyId
    }

    return try await withCheckedThrowingContinuation { continuation in
        _service.generateKey { keyId, error in
            if let keyIdValue = keyId {
                do {
                    try self._keyId.set(value: keyIdValue)
                    continuation.resume(returning: keyIdValue)
                }
                catch {
                    continuation.resume(throwing: AppAttestError.GetKey(error))
                }
            }
            else {
                continuation.resume(throwing: AppAttestError.GetKey(error))
            }
        }
    }
}

上面的代码利用self._keyId实例来管理生成的密钥对的重复使用。该实例是一种用作device keychain secret storage接口的类型,该实现将在稍后显示。

它也使用AppAttestError,然后,我稍后再共享此代码。

  1. 尝试获取存储的keyId值并提早返回。
  2. 异步生成密钥对并处理发电或持续存在的键标识符
  3. 使用延续捕获密钥标识符响应(或错误)。

产生证明(Swift)

检索挑战并获得关键标识符后,我们应该生成证明数据。请记住,此操作是针对Apple Ran Web服务进行的;从我的实验中,这项服务经常出现或暂时无法使用,因此请确保在您的设计中考虑这一点。

生成证明数据时,我们需要同时利用我们之前收到的关键标识符的挑战。

苹果实施此步骤的说明在Certify the key pairs as valid下显示。

import CryptoKit

let challenge = <# Data from your server. #>
let hash = Data(SHA256.hash(data: challenge))

service.attestKey(keyId, clientDataHash: hash) { attestation, error in
    guard error == nil else { /* Handle error and return. */ }

    // Send the attestation object to your server for verification.
}

和以前一样,这是使用async的明智实现。

public func generateAttestation(keyId: String, challenge: Data) async throws -> Data {
    let challengeHash = Data(SHA256.hash(data: challenge))

    return try await withCheckedThrowingContinuation { continuation in
        _service.attestKey(keyId, clientDataHash: challengeHash) { attestation, error in
            if let attestationValue = attestation {
                continuation.resume(returning: attestationValue)
            }
            else if let dcError = error as? DCError, dcError.code == .invalidKey {
                do {
                    try self._keyId.remove()
                    continuation.resume(throwing: AppAttestError.GenerateAttestation(error))
                }
                catch {
                    continuation.resume(throwing: AppAttestError.GenerateAttestation(error))
                }
            }
            else {
                continuation.resume(throwing: AppAttestError.GenerateAttestation(error))
            }
        }
    }
}

上面的代码处理“无效密钥”应用程序证明错误的情况。在这种情况下,我们应该使本地密钥标识符高速缓存无效,以使其重新创建。

验证证明声明

验证证明语句的步骤概述了here

这代表了协议实施的大部分,有很多步骤和许多概念需要理解。让我们尝试将其分解以使其消化。

高级步骤

  1. 解码证明数据
  2. 验证语句证书
  3. 验证加密nonce
  4. 验证针对语句证书的密钥标识符
  5. 验证应用标识符
  6. 验证签名计数器
  7. 验证开发或产品环境
  8. 验证针对凭据标识符的密钥标识符
  9. 主张数据验证的商店证明数据

...yikesð

我不希望所有这些都有意义,所以请继续阅读...

证明对象结构

要解码认证语句,我们需要介绍:

...yikesð

我认为分解此问题的最佳方法可能是在证明数据中显示类型的层次结构,然后详细说明其编码和语义。

Image description

  • attestation:表示由App Actest Service的认证数据返回表示的CBOR编码地图。
    • fmt:CBOR编码的字符串,标识语句格式。对于Apple的设备检查应用程序证明,它始终是“ Apple-Appattest”。
    • attStmt:证明“语句”,编码为CBOR地图。
    • x5c:CBOR编码的字节数组数组,每个字节数组均被编码为X509证书。
      • 索引0:应用程序生成的密钥对的公共证书。
      • Index 1:中间签名证书。该链由koude16-> intermediate-> auth cert
      • 代表
    • receipt:CBOR编码的字节数组可以发送到应用程序App Sectest服务,以获取有关与证明相关的风险指标的信息。
    • authData:或"Authenticator Data"在Web身份验证规范中定义。在大多数情况下,该结构中的数据是固定宽度,因此我们可以将字节直接元用到结构(主要是ð)。
    • rpidHash:顾名思义,这是RPID或relying party identifier的哈希。在应用程序中,RP始终是您的应用程序的"App ID"
    • flags:描述身份数据的位数;它包括证明数据吗?用户是否已验证?此验证数据有扩展吗?这样的东西...
    • signCount:签名签名内容的计数器。解析证明语句时,此数字为零。
    • attestedCredentialData:包含用于相应证明的credential data。 App证明使用关键标识符作为凭证标识符
      • aaguid:确定应用程序是否证明环境是开发还是生产。
      • credentialIdLength:长度credentialId的Big-Endian UInt16值。
      • credentialId:证明的凭证标识符。出于APP证明的目的,关键标识符和凭据标识符是相同的值。
      • credentialPublicKeyCOSE键,它的编码是一件事情,但是由于Apple App Apps Advents不利用此字段,我不会花时间进入它。

解码证明对象

解码认证对象的第一级需要CBOR解码。为了解码CBOR编码的对象,您可以将其拉到诸如Dahomey.Cbor之类的库中,或者自己编写。中间的某个地方是使用Microsoft与koude31提供的内容。您仍然需要做很多举重并编写一个挑战者,但是课程可以帮助您。

要更好地了解CBOR编码,请看一下my article here