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回调返回
注意事项
- 如果私钥是加密格式(带密码),需添加额外的解密逻辑
- 若使用EC私钥,需将
extractPrivateKey方法中的kSecAttrKeyType改为kSecAttrKeyTypeEC,并调整密钥大小 - 测试时可临时开启ATS允许任意加载(仅测试用),生产环境需配置符合要求的ATS规则
内容的提问来源于stack exchange,提问作者Coder87
相关产品推荐
相关产品推荐

