You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

使用.p12文件签名并通过Node Crypto验证iOS API调用合法性

嘿,看你已经在搭建iOS和Node.js之间的API签名验证体系了,我来帮你把现有流程理清楚,再补上.p12相关的关键细节,让整个验证链路跑通:

现有密钥生成与后端逻辑梳理

1. RSA密钥对生成

你已经用OpenSSL生成了加密的私钥和对应的公钥,我把命令整理成更规范的格式(去掉多余的转义符):

# 生成带AES256密码保护的2048位RSA私钥
openssl genrsa -aes256 -passout pass:pass -out signature_private_key.pem 2048

# 从加密私钥导出公钥(后端用于验证签名)
openssl rsa -in signature_private_key.pem -passin pass:pass -out signature_public_key.pem -outform PEM -pubout

这里要注意:私钥最终会打包成.p12文件给iOS端使用,公钥留在Node.js后端做签名验证。

2. Node.js后端签名字符串生成与验证逻辑

你写的createSignatureString方法是生成待签名字符串的核心,我帮你补全完整的验证流程:

const crypto = require('crypto');
const fs = require('fs');

class ApiSignatureValidator {
  // 生成待签名的原始字符串(和iOS端规则必须完全一致)
  static createSignatureString(bodyString, apiKey, timestamp) {
    // 对请求体做SHA256哈希并转base64
    const bodyHash = crypto
      .createHash("sha256")
      .update(bodyString)
      .digest("base64");
    
    // 按约定拼接关键参数:apiKey + 时间戳 + 请求体哈希
    // 这里的分隔符和顺序要和iOS端严格对齐!
    return `${apiKey}.${timestamp}.${bodyHash}`;
  }

  // 用公钥验证iOS端发来的签名
  static validateSignature(signatureRaw, clientSignature, publicKeyPath) {
    try {
      const publicKey = fs.readFileSync(publicKeyPath, 'utf8');
      const verifier = crypto.createVerify('SHA256');
      verifier.update(signatureRaw);
      // 验证base64格式的签名,算法要和iOS端匹配
      return verifier.verify(publicKey, clientSignature, 'base64');
    } catch (err) {
      console.error('签名验证失败:', err);
      return false;
    }
  }
}

关键点:后端必须用原始请求体字符串计算哈希,不能用解析后的JSON对象(否则可能因为键顺序、空格差异导致哈希不一致)。

iOS端使用.p12文件签名的核心步骤

.p12是包含私钥和证书的加密容器,iOS端需要从中提取私钥来生成签名,以下是Swift实现的关键代码:

1. 从.p12文件提取私钥

import Security

func extractPrivateKey(fromP12Data p12Data: Data, password: String) -> SecKey? {
    let importOptions: [String: Any] = [
        kSecImportExportPassphrase as String: password
    ]
    
    var importedItems: CFArray?
    let status = SecPKCS12Import(p12Data as CFData, importOptions as CFDictionary, &importedItems)
    
    guard status == errSecSuccess,
          let items = importedItems as? [[String: Any]],
          let identityDict = items.first,
          let identity = identityDict[kSecImportItemIdentity as String] as? SecIdentity else {
        return nil
    }
    
    var privateKey: SecKey?
    let keyStatus = SecIdentityCopyPrivateKey(identity, &privateKey)
    return keyStatus == errSecSuccess ? privateKey : nil
}

2. 生成签名并随请求发送

func generateRequestSignature(body: Data, apiKey: String, timestamp: String, privateKey: SecKey) -> String? {
    // 计算请求体的SHA256哈希(和后端逻辑一致)
    let bodyHash = body.sha256().base64EncodedString()
    // 拼接待签名字符串(和后端createSignatureString的规则完全相同)
    let signatureRaw = "\(apiKey).\(timestamp).\(bodyHash)"
    
    guard let rawData = signatureRaw.data(using: .utf8) else { return nil }
    
    var error: Unmanaged<CFError>?
    // 使用RSA PKCS#1 v1.5 + SHA256签名,和后端验证算法匹配
    guard let signatureData = SecKeyCreateSignature(
        privateKey,
        .rsaSignatureMessagePKCS1v15SHA256,
        rawData as CFData,
        &error
    ) as Data? else {
        print("签名生成失败: \(error?.takeRetainedValue() ?? CFError())")
        return nil
    }
    
    return signatureData.base64EncodedString()
}

// 给Data扩展SHA256计算方法
extension Data {
    func sha256() -> Data {
        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        self.withUnsafeBytes { buffer in
            _ = CC_SHA256(buffer.baseAddress, CC_LONG(self.count), &hash)
        }
        return Data(hash)
    }
}

iOS端发送请求时,要把生成的签名、apiKey、timestamp一起放在请求头或参数里,供后端验证。

端到端验证的重要注意事项
  • 时间戳防重放:后端要检查timestamp是否在有效时间窗口内(比如5分钟),防止攻击者重复使用旧的合法请求。
  • 参数一致性:两端的待签名字符串拼接规则、哈希算法、签名算法必须完全一致,差一个字符都会导致验证失败。
  • .p12安全保护:iOS端不要硬编码.p12的密码,建议用Keychain存储;同时.p12文件要避免被逆向工具提取,可以考虑做二进制混淆。

内容的提问来源于stack exchange,提问作者huge-iguana

火山引擎 最新活动