Openssl 验证失败 iOS Secure Enclave 创建的签名

Openssl verify fails with iOS Secure Enclave created signature

我正在尝试对 iOS (14.4) 上的用户数据进行散列和签名,将其发送到我的服务器,并让服务器使用先前上传的 public 密钥验证散列和签名(在用户创建期间发送密钥对生成)。似乎很多人 运行 对此有疑问,但我能找到的所有答案都是 very old,不要考虑使用 Apple 的 Secure Enclave,或围绕签名并在同一 iOS 设备上进行验证。

一般的工作流程是:用户在iOS上创建一个帐户,并在设备上创建一个随机密钥对,私钥保留在Secure Enclave中,同时转换public密钥为 ASN.1 格式,PEM 编码并上传到服务器。当用户稍后对数据进行签名时,数据会经过 JSON 编码、使用 sha512 进行哈希处理,并由其在 Secure Enclave 中的私钥进行签名。然后将其打包成 base64EncodedString 负载,并发送到服务器进行验证。服务器首先使用 openssl_digest 验证哈希,然后使用 openssl_verify.

检查签名

我一直无法获取openssl_verify方法来成功验证签名。我也曾尝试使用 phpseclib 库(以更深入地了解验证失败的原因)但没有成功。我知道 phpseclib 使用 openssl 库(如果它可用),但即使禁用它,phpseclib 的内部验证也会失败,因为模数后的结果值不匹配。有趣的是,phpseclib 将 public 密钥转换为看起来像带有大量填充的 PKCS8 格式。

public 密钥似乎被 openssl 正确解析和加载,因为在验证之前创建了正确的引用。但是,由于私钥是不透明的(驻留在 Secure Enclave 中),我没有办法从外部“检查”签名本身如何 generated/encoded 或者是否会在 iOS 设备。我想知道我是否有编码错误,或者是否可以使用 Secure Enclave 中生成的密钥进行外部验证。

iOS Public 密钥上传方法- 我正在使用 CryptoExportImportManager 将原始字节转换为 DER,添加 ASN.1 header,并添加 BEGIN 和 END 键标记。

public func convertPublicKeyForExport() -> String?
{
  let keyData       = SecKeyCopyExternalRepresentation(publicKey!, nil)! as Data
  let keyType       = kSecAttrKeyTypeECSECPrimeRandom
  let keySize       = 256
  let exportManager = CryptoExportImportManager()
  let exportablePEMKey = exportManager.exportECPublicKeyToPEM(keyData, keyType: keyType as String,
                                                               keySize: keySize)
        
  return exportablePEMKey
}

public 键之一在上传后的样子示例

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf16tnH8YPjslaacdtdde4wRQs0PP
zj/nWgBC/JY5aeajHhbKAf75t6Umz6vFGBsdgM/AFMkeB4n2Qi96ePNjFg==
-----END PUBLIC KEY-----
let encoder = JSONEncoder()
guard let payloadJson = try? encoder.encode(["user_id": "\(user!.userID)", "random_id": randomID])
else
{
 onCompletion(nil, NSError())
 print("Failed creating data")
 return
}
let hash = SHA512.hash(data: payloadJson)
guard let signature               = signData(payload: payloadJson, key: (user?.userKey.privateKey)!) else
{
 print("Could not sign data payload")
 onCompletion(nil, NSError())
 return
}
let params = Payload(
 payload_hash: hash.hexString,
 payload_json: payloadJson,
 signatures: ["user": [
    "signature": signature.base64EncodedString(),
    "type": "ecdsa-sha512"
 ]]
)

let encoding = try? encoder.encode(params).base64EncodedString()

符号数据函数与 Apple 的文档代码非常接近,但我将其包括在内以供参考

private func signData(payload: Data, key: SecKey) -> Data?
{
  var error: Unmanaged<CFError>?
  guard let signature = SecKeyCreateSignature(key,
                                              SecKeyAlgorithm.ecdsaSignatureMessageX962SHA512,
                                              payload as CFData, &error)
  else
  {
     print("Signing payload failed with \(error)")
     return nil
  }
  print("Created signature as \(signature)")
  return signature as Data
}

我实际上是在写这个问题时进行额外的研究和实验时偶然发现了这个解决方案。这个问题当然与密钥或算法无关,而与 Apple 散列数据对象的方式有关。

我在尝试确定为什么我的哈希值在服务器端与在 iOS 设备上创建的哈希值不匹配时发现了一个类似的问题。用户 JSONEncoded 数据被散列并签名为 base64Encoded 数据对象,但我不知道(并且在我能发现的任何文档中都没有)iOS 解码数据对象并散列原始对象,并重新编码它(因为这是不透明的代码,可能不准确,但结果是一样的)。因此,在对用户数据进行哈希校验时,我必须先对对象进行base64解码,然后再进行哈希运算。我曾假设 Apple 会按原样对编码对象进行签名(为了不污染其完整性),但实际上,当 Apple 在签名前创建摘要时,它会对解码的原始对象进行哈希处理并在原始对象上创建签名。

因此解决方案是在将对象发送到 openssl_verify 函数之前再次对其进行 base64 解码。

正在检查服务器上的哈希值

public function is_hash_valid($payload) {

    $server_payload_hash = openssl_digest(base64_decode($payload["payload_json"]), "SHA512");
    $client_payload_hash = $payload["payload_hash"];

    if ($client_payload_hash != $server_payload_hash) {
        return false;
    }

    return true;
}

正在服务器上验证签名

function is_signature_valid($data, $signature, $public_key) {
        
    $public_key = openssl_get_publickey($public_key);

    $ok = openssl_verify(base64_decode($data), base64_decode($signature), $public_key, "SHA512");
    if ($ok === 1) {
        return true;
    } else {
        return false;
    }
}

发现这一点并验证 openssl_verify 和 phpseclib 的验证功能正常工作后,我几乎考虑完全删除该问题,但意识到如果我在研究中发现了与此类似的问题,它可能有节省了我很多时间。希望对遇到类似问题的其他人有所帮助。