一、上篇总结
关于上一篇说的,客户端证书能否让网络框架(NSURLSession、AFNetworking)从钥匙串直接获取问题,经查询资料,得出的资料是不能。
无论根 CA 是否为正规 CA,描述文件(.mobileconfig)安装的客户端证书,都会被 iOS 存入系统 / 托管钥匙串(System/Managed Keychain) ,而非 NSURLSession(第三方 APP)可访问的「用户钥匙串(User Keychain)」,这是苹果的安全设计,具体隔离细节如下:
-
存储区域的访问权限边界系统 / 托管钥匙串属于「Apple 专属访问组」(如
com.apple.systemgroup.xxx、com.apple.managed.xxx),仅苹果自带系统进程(Safari、Mail、VPN、系统网络服务等)拥有访问权限,第三方 APP 的NSURLSession(包括你开发的 Objective-C APP)没有任何访问权限,哪怕配置了Keychain Sharing也无法突破这个限制。 -
「系统自动获取」的适用范围有严格前提,是有条件的,并非所有场景都适用:
- 适用场景:仅针对「系统进程」(如 Safari 访问双向认证站点),系统进程可以直接访问系统 / 托管钥匙串中的证书,自动完成双向认证,无需用户干预。
- 不适用场景:第三方 APP 的
NSURLSession,无法访问系统 / 托管钥匙串,也无法让系统「自动提取」该区域的证书 / 身份,必须依赖「用户钥匙串」中的证书,且需要 APP 手动配置权限和提取。
-
根 CA 合规性仅影响「证书信任」,不影响「证书访问权限」根 CA 是正规 CA 颁发的,仅能保证:证书在 iOS 中无需手动开启「完全信任」(自签名证书需要),系统会自动信任该证书的有效性,但无法改变证书的存储区域,也无法赋予第三方 APP 访问隔离钥匙串的权限。简单说:「证书是否被信任」和「证书是否能被 APP 访问」是两个独立的问题,根 CA 合规解决了前者,解决不了后者。
如果想要使用这种方式(通过描述文件安装后可以直接访问的),需要使用移动设备管理(MDM)方式进行分发安装(前提是开发者开发账号,支持这种方式)。
二、可行方式
-
如果还是想从钥匙串获取有效身份,则需要手动将证书导入NSURLSession可访问到的应用钥匙串组,相对于把证书放到app包里面,还是前者安全性高一点。
-
执行过程:在项目里面,先通过证书地址:https://域名/clientcer.pfx, 下载下来,然后通过代码导入到钥匙串,最后通过代码进行读取使用。
-
示例:
1)下载证书
//并发队列 - 异步执行 NSString *filePath = @"证书地址"; dispatch_queue_t queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); //串行队列:DISPATCH_QUEUE_SERIAL dispatch_async(queue, ^{ NSData *fileData = [NSData dataWithContentsOfURL:[NSURL URLWithString:filePath]]; NSString *pfxStr = [filePath componentsSeparatedByString:@"/"].lastObject; //通过下载的证书文件,手动导入到网络框架可访问到的【用户钥匙串】里面 [self importPFXFromDocument:pfxStr data:fileData certificatePWD:“客户端证书密码”]; });2)导入钥匙串
- (void)importPFXFromDocument:(NSString *)label data:(NSData *)pfxData certificatePWD:(NSString *)certificatePWD { if (!pfxData) { NSLog(@"未获取到证书文件"); return; } if (![certificatePWD isValid]) { NSLog(@"证书密码不允许为空"); return; } //导入新证书前,先删除旧证书 [self deleteCertificateWithLabel:label]; NSDictionary *options = @{ (__bridge id)kSecImportExportPassphrase:certificatePWD // 替换为实际密码 }; CFArrayRef items = NULL; OSStatus importStatus = SecPKCS12Import((__bridge CFDataRef)pfxData, (__bridge CFDictionaryRef)options, &items); if (importStatus != errSecSuccess) { NSLog(@"❌ SecPKCS12Import 失败,错误码: %d", (int)importStatus); // 根据需要处理错误,例如返回或抛出异常 if (items) CFRelease(items); return; // 或进行其他错误处理 } // 确保数组有元素 CFIndex count = items ? CFArrayGetCount(items) : 0; if (count == 0) { NSLog(@"❌ 导入后未获得任何项目"); if (items) CFRelease(items); return; } CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0); if (!identityDict) { NSLog(@"❌ 无法获取第一个项目字典"); CFRelease(items); return; } SecIdentityRef identity = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity); if (!identity) { NSLog(@"❌ 无法从字典中获取身份标识(SecIdentityRef)"); CFRelease(items); return; } // 保存到钥匙串 NSDictionary *addQuery = @{ (__bridge id)kSecValueRef: (__bridge id)identity, (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, (__bridge id)kSecAttrLabel: label }; OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL); CFRelease(items); if (addStatus == errSecSuccess) { NSLog(@"✅ 成功导入客户端证书到钥匙串。"); //导入成功,移除掉存储的证书和密码,预防潜在风险(如果存储过) } else if (addStatus == errSecDuplicateItem) { NSLog(@"⚠️ 证书已存在于密钥链中。"); [self showResponseMessage:@"The certificate already exists in the keychain."]; } else { NSLog(@"❌ 将身份标识添加到钥匙串失败,错误码: %d", (int)addStatus); } } /// 删除掉特定标签的证书身份 /// - Parameter label: 标签名字 - (void)deleteCertificateWithLabel:(NSString *)label { NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, (__bridge id)kSecAttrLabel: label, (__bridge id)kSecReturnRef: @NO }; OSStatus deleteStatus = SecItemDelete((__bridge CFDictionaryRef)query); if (deleteStatus == errSecSuccess) { NSLog(@"成功删除标签为 %@ 的证书", label); } else if (deleteStatus == errSecItemNotFound) { NSLog(@"未找到标签为 %@ 的证书", label); } else { NSLog(@"⚠️ 删除操作遇到其他错误,错误码: %d。将继续尝试导入。", (int)deleteStatus); } }3)NSURLSession使用:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler { NSString *authMethod = challenge.protectionSpace.authenticationMethod; //NSLog(@"接收到认证挑战: %@", authMethod); // 1. 处理服务器信任挑战(验证服务器证书) if ([authMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { NSLog(@"检测到服务器信任认证挑战,进行验证"); [self handleServerTrustChallenge:challenge completionHandler:completionHandler]; } // 2. 处理客户端证书挑战(向服务器证明客户端身份) else if ([authMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]) { NSLog(@"检测到客户端证书认证挑战,进行验证"); [self handleClientCertificateChallenge:challenge completionHandler:completionHandler]; } else { // 3. 对于其他未知类型的挑战,使用默认处理 NSLog(@"检测到其他认证挑战: %@,使用默认处理", authMethod); completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } } //服务器认证挑战 - (void)handleServerTrustChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; if (!serverTrust) { completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); return; } //NSString *host = challenge.protectionSpace.host; // 可根据需要实现自定义的服务器证书验证逻辑 // 例如,检查证书是否过期、域名是否匹配等 // 此处示例选择信任一个有效的服务器证书 SecTrustResultType trustResult; OSStatus status = SecTrustEvaluate(serverTrust, &trustResult); if (status == errSecSuccess && (trustResult == kSecTrustResultProceed || trustResult == kSecTrustResultUnspecified)) { NSLog(@"✅ 服务器证书验证通过"); // 服务器可信,创建信任凭证 NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; completionHandler(NSURLSessionAuthChallengeUseCredential, credential); } else { // 服务器不可信,取消挑战 NSLog(@"❌ 服务器证书验证失败,错误状态: %d", (int)status); completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } } //客户端证书挑战 - (void)handleClientCertificateChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler { // 1. 尝试从钥匙串获取客户端证书 SecIdentityRef identity = [self loadClientIdentityFromKeychain]; if (identity) { // 2. 成功获取,创建身份凭证 SecCertificateRef certificate = NULL; OSStatus status = SecIdentityCopyCertificate(identity, &certificate); if (status == errSecSuccess && certificate) { // 将证书添加到数组,创建凭证 const void *certs[] = { certificate }; CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL); NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray *)certArray persistence:NSURLCredentialPersistenceForSession]; // 通常一次会话即可 // 释放资源 CFRelease(certArray); CFRelease(certificate); CFRelease(identity); // 因为之前在查询时使用了 kSecReturnRef, 需要释放 NSLog(@"✅ 使用钥匙串中的客户端证书响应挑战"); completionHandler(NSURLSessionAuthChallengeUseCredential, credential); return; } if (certificate) { CFRelease(certificate); } CFRelease(identity); // 确保在失败路径下也释放 identity } // 3. 无法提供有效的客户端证书 NSLog(@"❌ 无法从钥匙串获取有效的客户端证书,取消认证挑战"); completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } /// 从钥匙串获取客户端身份 (SecIdentityRef) - (SecIdentityRef)loadClientIdentityFromKeychain { SecIdentityRef identity = NULL; // 构建查询条件 NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, (__bridge id)kSecReturnRef: @YES, //(__bridge id)kSecAttrLabel: @"", // 使用描述文件中的名称 (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, // 可以添加更多过滤条件来精确匹配你的证书 // kSecAttrLabel: (NSString *) 证书标签 // kSecAttrSubject: (NSData *) 主题信息 }; // 执行钥匙串查询 OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&identity); if (status != errSecSuccess) { NSLog(@"钥匙串查询失败,错误码: %d", (int)status); // 打印所有可用的证书,帮助调试 [self listAllIdentitiesInKeychain]; return NULL; } return identity; } /// 列出所有证书进行调试 - (void)listAllIdentitiesInKeychain { NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassIdentity, (__bridge id)kSecReturnRef: @YES, (__bridge id)kSecReturnAttributes: @YES, (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll, }; CFArrayRef results = NULL; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&results); if (status == errSecSuccess && results != NULL) { NSArray *identities = (__bridge NSArray *)results; NSLog(@"找到 %ld 个证书:", (long)identities.count); for (NSDictionary *item in identities) { SecIdentityRef identity = (__bridge SecIdentityRef)item[(__bridge id)kSecValueRef]; // 获取证书 SecCertificateRef certificate; SecIdentityCopyCertificate(identity, &certificate); if (certificate) { CFStringRef summary = SecCertificateCopySubjectSummary(certificate); NSLog(@"证书: %@", summary); if (summary) CFRelease(summary); CFRelease(certificate); } // 打印标签等其他信息 NSString *label = item[(__bridge id)kSecAttrLabel]; NSLog(@"标签: %@", label); } CFRelease(results); } }