【问题标题】:How to decode an iOS assertion object?如何解码 iOS 断言对象?
【发布时间】:2021-05-31 17:18:51
【问题描述】:

我使用 DCAppAttestService 生成一个断言对象并将其发送到我的 Cloud Functions 服务器 - 以验证应用内购买是否来自我的应用的合法实例。

这是我的 Swift 代码,当进行应用内购买时:

  1. 从我的服务器检索一次性质询(字符串)
  2. 使用该字符串以及客户端数据来生成哈希
  3. 使用该哈希和设备的keyID,调用generateAssertion()
  4. 发送断言对象和客户端数据对象的 base64 编码字符串

斯威夫特:

func callSensitiveFunction(name: String, data: [String:String], completion: @escaping (HTTPSCallableResult?, Error?)->()){
    var functions = Functions.functions()
    var adjustedData: [String:String] = data
    guard let keyID = KeychainWrapper.standard.string(forKey: "attestKeyID") else {
        print("no attest ID, return")
        return
    }
    let service = DCAppAttestService.shared
    // Retrieve the challenge string
    functions.httpsCallable("getChallengeString").call(data){ (result, error) in
        if let challengeString = result?.data as? String {
            // Retrieve the keyID (public key) which was saved at first launch
            guard let attestKeyID = KeychainWrapper.standard.string(forKey: "attestKeyID") else { return print("No attestKeyID found") }
            // Add challenge string to request
            adjustedData["challenge"] = challengeString
            // Hash client data (which includes challenge string)
            guard let clientData = try? JSONEncoder().encode(adjustedData) else { return }
            let clientDataHash = Data(SHA256.hash(data: clientData))
            service.generateAssertion(attestKeyID, clientDataHash: clientDataHash) { assertion, err in
                guard error == nil else { return print("generateAssertion() error: \(error)") }
                let assertionString = assertion?.base64EncodedString() ?? ""
                let clientDataString = clientData.base64EncodedString()
                adjustedData["assertionString"] = assertionString
                adjustedData["clientData"] = clientDataString
                functions.httpsCallable(name).call(adjustedData){ (result, error) in
                    completion(result, error)
                    print("function \(name) completed")
                }
            }
        }
    }
}

但是,当我在后端 (Node.js) 中收到 assertionString 时 - 我不知道如何解码它。使用本教程here 我已经做到了这一点:

const cbor = require("cbor");
const base64url = require('base64url');

let verifyAppleAnonymousAttestation = (assertionString) => {
    let assertionBuffer = base64url.toBuffer(assertionString);
    console.log(`assertionBuffer: ${assertionBuffer}`) // assertionBuffer: �isignatureXG0E!���56��r��@��@J�r+X������ڳ%� D��c! 
    let assertionStruct = cbor.decodeAllSync(assertionBuffer)[0];
    console.log(`assertionStruct[0]: ${assertionStruct[0]}`) // assertionStruct: [object Object]

但是我不确定从这里去哪里来验证断言字符串。 Apple says assertion 被解码后看起来像这样:

{
  signature: <Buffer 30 45 02 20 ... >,
  authenticatorData: <Buffer 21 c9 9e 00 ... >
}

知道如何从assertionString 中提取所有验证数据吗?

【问题讨论】:

    标签: ios node.js swift encryption base64


    【解决方案1】:

    您在设备上缺少service.generateKey(您不应该尝试在没有Apple 的情况下自己生成keyId)和service.attestKey。这就是 Apple 设计 API 的方式,您可以从 docs 中看到这一点。您还应该使用您的Keychain 来存储keyId 以供将来仅在成功的请求链之后使用。不要在没有DCAppAttestService 的情况下将无效的keyIds 存储在您生成的钥匙串中,否则您将无法获得稳定的行为。

    DCAppAttestService 的一般页面说,您应该在使用它的不透明框架方法生成密钥后验证密钥,因为它不想为了安全而公开敏感的设备信息。你正在与框架作斗争。 :-)

    按照here提供的顺序执行步骤。

    • 通过调用 generateKeyWithCompletionHandler: 方法在 Secure Enclave 中创建一个加密密钥。
    • 要求 Apple 通过调用 attestKey:clientDataHash:completionHandler: 方法来验证密钥。
    • 使用 generateAssertion:clientDataHash:completionHandler: 方法。

    示例代码:

    var keyId: String? // Make this a property on self.
    var attestationObject: Data? // Make this a property on self.
    
    // Catch the errors this throws out when you call it.
    func callSensitiveFunction(name: String, data: [String:String], completion: @escaping (HTTPSCallableResult?, Error?)->()) throws {    
    // Check if keyId exists in Keychain, if it doesn't start the request flow.
    // Get clientData from first request (challenge) as you are.
    let clientDataHash = Data(SHA256.hash(data: clientData))
    let service = DCAppAttestService.shared
    guard service.isSupported else { return } 
    let group = DispatchGroup()
    group.enter()
    service.generateKey { keyId, error in
      // First check for errors in every step, obviously you can handle the errors differently (this is just a guide).
      guard !error else {
        group.leave()
        throw ValidationError(error.localizedDescription) // I assume we have defined a new enum Error type with associated values here. 
        return
      }
      self.keyId = keyId
      group.leave()
    }
    group.wait()
    guard let keyId = self.keyId {
      throw ValidationError("Missing keyId")
      return
    }
    group.enter()
    service.attestKey(keyId, clientDataHash: clientDataHash) { attestationObject, error in
      guard !error else {
        group.leave()
        throw ValidationError(error.localizedDescription)
        return
      }
      self.attestationObject = attestationObject
      group.leave()
    }
    // Now you need to send `attestationObject` to the server directly to validate server-side.
    group.wait()
    guard let attestationObject = self.attestationObject {
      throw ValidationError("Missing attestationObject")
      return
    }
    group.enter()
    functions.httpsCallable(name).call(attestationObject) { (result, error) in
      guard !error else {
        group.leave()
        throw ValidationError(error.localizedDescription)
        return
      }
      group.leave()
    }
    // Now you only generate the assertion if the key was valid.
    group.wait()
    service.generateAssertion(keyId, clientDataHash: clientDataHash) { data, error in
      guard !error else {
        throw ValidationError(error.localizedDescription)
        return
      }
      // NOW store in the Keychain.
      completion(result, error)
      print("function \(name) completed")
    }
    }
    

    我的回答仅限于 iOS 设备代码,但可以在 here 找到验证密钥服务器端的背景和指南。

    我还可以建议使用DispatchGroup 而不是嵌套的完成块——还要确保在后台队列中调用callSensitiveFunction。您得到问题的主要原因 - HTTPS 请求顺序需要准确,以及使用 Apple 的 API 指南。否则,您将检查错误的对象。 :) 我不确定你是否想抛出错误,而且我还没有测试过这段代码(我手边没有 API),但大致的想法就在那里。

    【讨论】:

    • 谢谢。我的代码中的guard let keyID 是在应用启动时使用service.generateKey() 生成的。所以我成功地向我的服务器发送了一个有效的断言对象(base64 编码的字符串)。我的问题是尝试在我的 Node.js 服务器上验证 assertion
    • 好的,谢谢。您是否也在代码中的其他地方调用service.attestKey
    • 我没有。我选择只验证服务器上的每个请求(应用内购买)。我认为没有必要加倍。
    • 我明白了。嗯,我认为这就是问题所在。 Apple 需要在 generateAssertion 之前在其自己的服务器上内部验证该密钥 - 看起来您遇到了 docs 中提到的问题。 A compromised version of your app could falsify the verification result, thus circumventing App Attest.
    • 谢谢 - 我会确保使用 attestKey() 验证密钥。但是,主要问题/复杂性仍然是验证服务器上的assertion
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-10
    • 2022-01-01
    • 2014-08-11
    • 1970-01-01
    • 2012-06-27
    相关资源
    最近更新 更多