You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中如何在HTTPS请求中添加X509证书与私钥?

给Azure IoT DPS请求添加X509证书认证的Swift实现

核心实现逻辑

要完成X509证书认证调用Azure IoT DPS API,需完成三个关键步骤:将PEM格式的证书/私钥转换为Security框架可识别的身份对象、配置URLSession处理客户端证书挑战、替换原有请求逻辑集成认证流程。

修改后的完整代码

import UIKit
import Foundation
import Security

@objc(AzureProvisionWithCertificate)
class AzureProvisionWithCertificate: NSObject, URLSessionDelegate {
    @objc(provisionAndUploadFile:withRegistrationId:withKey:withCertificate:withProvisionHost:withFileNameWithFolder:withModelId:withResolver:withRejecter:)
    func provisionAndUploadFile(scopeId: String, 
                                registrationId: String, 
                                key: String, 
                                certificate: String, 
                                provisionHost: String, 
                                fileNameWithFolder: String, 
                                modelId: String, 
                                resolve: @escaping RCTPromiseResolveBlock, 
                                reject: @escaping RCTPromiseRejectBlock) -> Void {
        
        // 解析PEM证书和私钥为SecIdentity
        guard let identity = createIdentity(fromPEMCertificate: certificate, pemPrivateKey: key) else {
            reject("IDENTITY_PARSE_FAILED", "证书或私钥解析失败", nil)
            return
        }
        
        // 构造请求URL
        guard let url = URL(string: "https://global.azure-devices-provisioning.net/\(scopeId)/registrations/\(registrationId)/register?api-version=2021-06-01") else {
            reject("INVALID_URL", "请求URL格式错误", nil)
            return
        }
        
        // 配置请求参数
        var request = URLRequest(url: url)
        request.httpMethod = "PUT"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("utf-8", forHTTPHeaderField: "Content-Encoding")
        
        let bodyData = ["registrationId": registrationId]
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: bodyData)
        } catch {
            reject("JSON_SERIALIZE_FAILED", "请求体序列化失败", error)
            return
        }
        
        // 配置带证书认证的URLSession
        let sessionConfig = URLSessionConfiguration.default
        let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
        
        // 发起请求
        let task = session.dataTask(with: request) { data, response, error in
            if let error = error {
                reject("REQUEST_FAILED", error.localizedDescription, error)
            } else if let data = data, let responseString = String(data: data, encoding: .utf8) {
                resolve("Response data: \(responseString)")
            } else {
                reject("NO_RESPONSE_DATA", "未收到有效响应数据", nil)
            }
        }
        task.resume()
    }
    
    // MARK: - URLSessionDelegate 处理客户端证书挑战
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate else {
            completionHandler(.performDefaultHandling, nil)
            return
        }
        
        guard let identity = currentIdentity else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        
        let credential = URLCredential(identity: identity, certificates: nil, persistence: .forSession)
        completionHandler(.useCredential, credential)
    }
    
    // MARK: - 辅助工具方法
    private var currentIdentity: SecIdentity?
    
    /// 从PEM格式证书和私钥创建SecIdentity
    private func createIdentity(fromPEMCertificate pemCert: String, pemPrivateKey pemKey: String) -> SecIdentity? {
        // 清理证书PEM格式
        let cleanedCert = pemCert.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----", with: "")
                                 .replacingOccurrences(of: "-----END CERTIFICATE-----", with: "")
                                 .trimmingCharacters(in: .whitespacesAndNewlines)
        
        guard let certData = Data(base64Encoded: cleanedCert),
              let certificate = SecCertificateCreateWithData(nil, certData as CFData) else {
            return nil
        }
        
        // 解析私钥
        guard let privateKey = extractPrivateKey(fromPEM: pemKey) else {
            return nil
        }
        
        // 创建SecIdentity
        var identity: SecIdentity?
        let status = SecIdentityCreateWithCertificate(nil, certificate, &identity)
        
        guard status == errSecSuccess, let createdIdentity = identity else {
            return nil
        }
        
        currentIdentity = createdIdentity
        return createdIdentity
    }
    
    /// 从PEM格式提取私钥
    private func extractPrivateKey(fromPEM pemKey: String) -> SecKey? {
        // 清理私钥PEM格式(兼容RSA/EC私钥)
        let cleanedKey = pemKey.replacingOccurrences(of: "-----BEGIN PRIVATE KEY-----", with: "")
                               .replacingOccurrences(of: "-----END PRIVATE KEY-----", with: "")
                               .replacingOccurrences(of: "-----BEGIN RSA PRIVATE KEY-----", with: "")
                               .replacingOccurrences(of: "-----END RSA PRIVATE KEY-----", with: "")
                               .trimmingCharacters(in: .whitespacesAndNewlines)
        
        guard let keyData = Data(base64Encoded: cleanedKey) else {
            return nil
        }
        
        // 私钥配置(根据实际密钥类型调整,默认RSA)
        let attributes: [CFString: Any] = [
            kSecAttrKeyType: kSecAttrKeyTypeRSA,
            kSecAttrKeyClass: kSecAttrKeyClassPrivate,
            kSecAttrKeySizeInBits: 2048,
            kSecReturnPersistentRef: kCFBooleanFalse!
        ]
        
        var error: Unmanaged<CFError>?
        return SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error)
    }
}

关键细节说明

  • PEM格式处理:必须移除证书/私钥的PEM头尾标识,再进行Base64解码
  • SecIdentity绑定:通过SecIdentityCreateWithCertificate将证书与私钥绑定为身份对象,这是客户端证书认证的核心
  • 挑战处理:实现URLSessionDelegate的didReceive challenge方法,在服务器发起客户端证书挑战时,返回包含身份的URLCredential
  • 错误捕获:在解析、序列化、请求各阶段添加错误处理,确保错误能通过Promise回调返回

注意事项

  1. 如果私钥是加密格式(带密码),需添加额外的解密逻辑
  2. 若使用EC私钥,需将extractPrivateKey方法中的kSecAttrKeyType改为kSecAttrKeyTypeEC,并调整密钥大小
  3. 测试时可临时开启ATS允许任意加载(仅测试用),生产环境需配置符合要求的ATS规则

内容的提问来源于stack exchange,提问作者Coder87

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 02:23:15