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

如何解决Openssl 验证失败,iOS Secure Enclave 创建的签名

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

一般的工作流程是:用户在iOS上创建一个账户,在设备上创建一个随机密钥对,私钥保留在Secure Enclave中,而公钥转换为ASN.1格式,PEM编码并上传到服务器。当用户稍后对数据进行签名时,数据是 JSONEncoded,使用 sha512 散列,并由他们在 Secure Enclave 中的私钥签名。然后将其打包为 base64EncodedString 负载,并发送到服务器进行验证。服务器首先使用 openssl_digest 验证哈希,然后使用 openssl_verify 检查签名。

我一直无法获得 openssl_verify 方法来成功验证签名。我还尝试使用 PHPseclib 库(以更深入地了解验证失败的原因)但没有成功。我了解 PHPseclib 使用 openssl 库(如果可用),但即使禁用了该库,PHPseclib 的内部验证也会失败,因为模数后的结果值不匹配。有趣的是,PHPseclib 将公钥转换为具有大量填充的 PKCS8 格式。

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

iOS 公钥上传方法- 我使用 CryptoExportImportManager 将原始字节转换为 DER,添加 ASN.1 标头,并添加 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
}

上传后其中一个公钥的示例

-----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 解码 Data 对象并散列原始对象,然后重新编码它(因为这是不透明的代码这可能不完全准确,但结果是一样的)。因此,在检查用户数据的散列时,我必须先对对象进行 base64decode,然后再执行散列。我曾假设 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 的验证函数是否正常工作后,我几乎考虑完全删除该问题,但意识到如果我在研究中发现了与此类似的问题,它可能为我节省了很多时间。希望对遇到类似问题的其他人来说,这会有所帮助。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?
Java在半透明框架/面板/组件上重新绘画。
Java“ Class.forName()”和“ Class.forName()。newInstance()”之间有什么区别?
在此环境中不提供编译器。也许是在JRE而不是JDK上运行?
Java用相同的方法在一个类中实现两个接口。哪种接口方法被覆盖?
Java 什么是Runtime.getRuntime()。totalMemory()和freeMemory()?
java.library.path中的java.lang.UnsatisfiedLinkError否*****。dll
JavaFX“位置是必需的。” 即使在同一包装中
Java 导入两个具有相同名称的类。怎么处理?
Java 是否应该在HttpServletResponse.getOutputStream()/。getWriter()上调用.close()?
Java RegEx元字符(。)和普通点?