SSL:手动导入证书至应用钥匙串,代码进行获取

12 阅读7分钟

一、上篇总结

接上一篇:juejin.cn/post/759630…

关于上一篇说的,客户端证书能否让网络框架(NSURLSession、AFNetworking)从钥匙串直接获取问题,经查询资料,得出的资料是不能。

无论根 CA 是否为正规 CA,描述文件(.mobileconfig)安装的客户端证书,都会被 iOS 存入系统 / 托管钥匙串(System/Managed Keychain) ,而非 NSURLSession(第三方 APP)可访问的「用户钥匙串(User Keychain)」,这是苹果的安全设计,具体隔离细节如下:

  1. 存储区域的访问权限边界系统 / 托管钥匙串属于「Apple 专属访问组」(如com.apple.systemgroup.xxxcom.apple.managed.xxx),仅苹果自带系统进程(Safari、Mail、VPN、系统网络服务等)拥有访问权限,第三方 APP 的NSURLSession(包括你开发的 Objective-C APP)没有任何访问权限,哪怕配置了Keychain Sharing也无法突破这个限制。

  2. 「系统自动获取」的适用范围有严格前提,是有条件的,并非所有场景都适用:

    • 适用场景:仅针对「系统进程」(如 Safari 访问双向认证站点),系统进程可以直接访问系统 / 托管钥匙串中的证书,自动完成双向认证,无需用户干预。
    • 不适用场景:第三方 APP 的NSURLSession,无法访问系统 / 托管钥匙串,也无法让系统「自动提取」该区域的证书 / 身份,必须依赖「用户钥匙串」中的证书,且需要 APP 手动配置权限和提取。
  3. 根 CA 合规性仅影响「证书信任」,不影响「证书访问权限」根 CA 是正规 CA 颁发的,仅能保证:证书在 iOS 中无需手动开启「完全信任」(自签名证书需要),系统会自动信任该证书的有效性,但无法改变证书的存储区域,也无法赋予第三方 APP 访问隔离钥匙串的权限。简单说:「证书是否被信任」和「证书是否能被 APP 访问」是两个独立的问题,根 CA 合规解决了前者,解决不了后者。

如果想要使用这种方式(通过描述文件安装后可以直接访问的),需要使用移动设备管理(MDM)方式进行分发安装(前提是开发者开发账号,支持这种方式)。

二、可行方式

  1. 如果还是想从钥匙串获取有效身份,则需要手动将证书导入NSURLSession可访问到的应用钥匙串组,相对于把证书放到app包里面,还是前者安全性高一点。

  2. 执行过程:在项目里面,先通过证书地址:https://域名/clientcer.pfx, 下载下来,然后通过代码导入到钥匙串,最后通过代码进行读取使用。

  3. 示例:

    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);
        }
    }