iOS如何实现客户端证书和服务器身份验证

13

我最近经历了一个非常辛苦的过程,构建了一些本应该非常简单但似乎在任何一个地方都难以找到的东西。我想在这里尝试把所有内容放在这里,问问自己是否做错了什么,如果没有的话,帮助需要这些信息的人。

背景:我试图为需要提供安全性的产品/服务建立在 Windows 服务器上的 WCF 服务上提供安全性,这些服务仅通过 PC 或 iPad 上的自定义客户端应用程序访问。每个客户一个服务器,没有浏览器访问权限。所有内容已经使用 Windows 标准机制和商业 CA 的证书进行了 TLS 安全认证和授权。

为了进一步限制访问,已经实现了 Windows 平台的客户端/服务器证书,使用自签名证书(在存在相互验证且不存在公共/浏览器访问的情况下,不需要商业 CA - 尽管有相反的说法 - 而且更难管理)。

使这一切工作于 iPad 上是一个文档可怕的噩梦,有大量的虚假信息或部分正确的推荐。在接下来的内容中,我试图链接到最好的来源,但如果我无意中错过了某个归属权,请谅解。如果这篇文章有任何错误/误导之处,请评论。

谢谢

2个回答

23
主要步骤如下:
  1. 创建用于生成证书的系统(如果是生产系统,则容易但不平凡)
  2. 将证书传输到iPad上(不要嵌入在应用商店捆绑包中!)
  3. 将所有接收到的凭据保存在应用程序密钥链中(苹果指定的位置)
  4. 从密钥链检索保存的凭据以在NSURLConnections中使用
  5. 实际认证服务器证书并返回客户端凭据
第1步:生成证书 参考:http://developer-should-know.tumblr.com/post/127063737582/how-to-create-your-own-pki-with-openssl 您可以使用其他方法,但是Windows的OpenSSL [http://slproweb.com/products.html]非常出色,除了标准界面是命令行界面,文档难以理解之外。
我希望有人能事先向我解释明显但不明显的内容: [a] 应用程序安装到根级目录,并包括默认情况下用于未在命令行中指定的设置的配置文件 [b] 中间和输出文件的位置应在配置文件中指定 [c] 某些文件需要手动创建才能运行命令 [d] 您应该构建适合您要执行的操作的文件/文件夹结构,然后相应地自定义cfg文件。
在我的情况下,这意味着为我的公司一个RootCA,每个客户一个中间证书(设置为只生成客户端证书),每个客户一个服务器证书和根据需要的客户端证书。(这是最小配置,请勿使用CA /客户对,请将根放在锁箱中) 下面是我的文件结构:
c:\sslcert
    root
    certs
        YourCompany (duplicate this structure as required)
             intermediate
             server
             client
             crl (optional)

在顶层sslcert文件夹中。
.rnd        (empty file)
certindex.txt   (empty file)
serial.txt  (Text file seeded with the text “01”, hold the quotes)

在根目录中

RootCA.cfg

在certs\template文件夹中。
IntermediateCA.cfg

设置工作目录并启动OpenSSL

cd \sslcert c:\OpenSSL-Win32\bin\openssl.exe

一步创建根密钥和证书

req -config ./root/RootCA.cfg -new -x509 -days 7300 -extensions v3_ca -keyout root/YourCompanyRootCAkey.pem -out root/YourCompanyRootCAcert.cer

对于初学者的注意事项:-extensions选项允许您在同一cfg文件中选择应用多个子部分中的一个。

检查密钥和证书(可选)

x509 -noout -text -in root/YourCompanyRootCAcert.cer

请求一个新的中间证书

req -config certs/YourCompany/IntermediateCA.cfg -new -keyout certs/YourCompany/intermediate/intermediateCAkey.pem -out certs/YourCompany/intermediate/intermediateCAreq.pem  

使用根配置中找到的根证书来签署中间证书。
ca -config root/RootCA.cfg -extensions v3_intermediate_ca -days 3650 -notext -in certs/YourCompany/intermediate/intermediateCAreq.pem -out certs/YourCompany/intermediate/YourCompanyIntermediateCAcert.cer

检查密钥和证书(可选)

x509 -noout -text -in certs/YourCompany/intermediate/YourCompanyIntermediateCAcert.cer

通过将中间证书和根证书连接起来创建证书链文件(这只是从命令行进行的简单追加操作 - 新的链将被添加到最终的p12包中)。
c:\sslcert> type c:\sslcert\certs\YourCompany\intermediate\YourCompanyIntermediateCAcert.cer c:\sslcert\root\YourCompanyRootCAcert.cer > c:\sslcert\certs\YourCompany\intermediate\YourCompanyCAchain.cer

请求新的客户端密钥和证书

genrsa -aes256 -out certs/YourCompany/client/YourCompanyClientkey.pem 2048
req -config certs/YourCompany/IntermediateCA.cfg -key 
certs/YourCompany/client/YourCompanyClientkey.pem -new -sha256 -out         certs/YourCompany/client/YourCompanyClientreq.pem

使用中间颁发机构签署并测试客户端证书

ca -config certs/YourCompany/IntermediateCA.cfg -extensions usr_cert -days 1095 -notext -md sha256 -in certs/YourCompany/client/YourCompanyClientreq.pem -out certs/YourCompany/client/YourCompanyClientcert.cer
x509 -noout -text -in certs/YourCompany/client/YourCompanyClientcert.cer
verify -CAfile certs/YourCompany/intermediate/YourCompanyCAchain.cer certs/YourCompany/client/YourCompanyClientcert.cer

打包客户端证书

pkcs12 -export -in certs/YourCompany/client/YourCompanyClientcert.cer -name “YourCompany Smips Client” -inkey certs/YourCompany/client/YourCompanyClientkey.pem -certfile certs/YourCompany/intermediate/YourCompanyCAchain.cer -out certs/YourCompany/client/YourCompanyClientWithName.p12

将 PKCS 重命名以便从电子邮件/ iTunes 导入 iOS

c:\sslcert> copy c:\sslcert\certs\YourCompany\client\YourCompanyClient.p12 c:\sslcert\certs\YourCompany\client\YourCompanyClient.yourext12

请求一个新的服务器密钥和证书

genrsa -aes256 -out certs/YourCompany/server/YourCompanyServerkey.pem 2048
req -config certs/YourCompany/IntermediateCA.cfg -key certs/YourCompany/server/YourCompanyServerkey.pem -new -sha256 -out certs/YourCompany/server/YourCompanyServerreq.pem

使用中间机构签署和测试服务器证书

ca -config certs/YourCompany/IntermediateCA.cfg -extensions server_cert -days 1095 -notext -md sha256 -in certs/YourCompany/server/YourCompanyServerreq.pem -out certs/YourCompany/server/YourCompanyServercert.cer
x509 -noout -text -in certs/YourCompany/server/YourCompanyServercert.cer
verify -CAfile certs/YourCompany/intermediate/YourCompanyCAchain.cer certs/YourCompany/server/YourCompanyServercert.cer

打包服务器证书

pkcs12 -export -in certs/YourCompany/server/YourCompanyServercert.cer -name “YourCompany Smips Server” -inkey certs/YourCompany/server/YourCompanyServerkey.pem -certfile certs/YourCompany/intermediate/YourCompanyCAchain.cer -out certs/YourCompany/server/YourCompanyServer.p12

这里是配置文件: 根目录
dir                 = .

[ ca ]
default_ca              = CA_default
 
[ CA_default ]
serial              = $dir/serial.txt
database                = $dir/certindex.txt
new_certs_dir           = $dir/certs
certs                   = $dir/certs
private_key             = $dir/root/yourcompanyRootCAkey.pem
certificate             = $dir/root/yourcompanyRootCAcert.cer
default_days            = 7300
default_md              = sha256
preserve                = no
email_in_dn             = no
nameopt             = default_ca 
certopt             = default_ca 
policy              = policy_strict

[ policy_strict ]
countryName                 = match
stateOrProvinceName         = match
organizationName            = match
organizationalUnitName      = optional
commonName                  = supplied
emailAddress                = optional

[ req ]
default_bits            = 4096      # Size of keys
default_keyfile         = key.pem       # name of generated keys
default_md              = sha256        # message digest algorithm
string_mask             = nombstr       # permitted characters
distinguished_name      = req_distinguished_name
x509_extensions         = v3_ca

[ req_distinguished_name ]
0.organizationName           = Organization Name
organizationalUnitName       = Organizational Unit Name
emailAddress                 = Email Address
emailAddress_max            = 40
localityName            = Locality Name (city, district)
stateOrProvinceName     = State or Province Name (full name)
countryName             = Country Name (2 letter code)
countryName_min         = 2
countryName_max         = 2
commonName              = Common Name (hostname, IP, or your name)
commonName_max          = 64

0.organizationName_default  = yourcompany
organizationalUnitName_default  = yourcompanyRoot Certification
emailAddress_default        = info@yourcompany.com
localityName_default        = Okeefenokee
stateOrProvinceName_default = Wisconsin
countryName_default     = US

[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ crl_ext ]
authorityKeyIdentifier=keyid:always

中级

dir = .

# [For non-command-line folks, everything is keyed to the working directory here (.) so if your working prompt says c:\sslcerts> then the cfg will look for serial.txt at c:\sslcerts\serial.txt and bomb if it doesn’t find things laid out accordingly. Thats why you set up a directory structure to match these entries]

[ ca ]
default_ca              = CA_default

[ CA_default ]
serial                  = $dir/serial.txt
database                = $dir/certindex.txt
crl_dir                 = $dir/certs/yourcompany/crl
new_certs_dir               = $dir/certs
certs                   = $dir/certs
private_key             = $dir/certs/yourcompany/intermediate/IntermediateCAkey.pem
certificate             = $dir/certs/yourcompany/intermediate/yourcompanyIntermediateCAcert.cer
default_days                = 3650
default_md              = sha256
preserve                = no
email_in_dn             = no
nameopt                 = default_ca
certopt                 = default_ca 
crlnumber               = $dir/certs/yourcompany/crl/crlnumber
crl                 = $dir/certs/yourcompany/crl/crl.pem
crl_extensions              = crl_ext
default_crl_days            = 365
policy                  = policy_loose

[ policy_loose ]
countryName                     = optional
stateOrProvinceName             = optional
localityName                    = optional
organizationName                = optional
organizationalUnitName          = optional
commonName                      = supplied
emailAddress                    = optional

[ req ]
default_bits                = 4096              # Size of keys
default_keyfile             = $dir/certs/yourcompany/intermediate/IntermediateCAkey.pem
default_md              = sha256            # message digest 

# the old default was md1 - change this]

algorithm
string_mask             = nombstr           # permitted characters
distinguished_name          = req_distinguished_name
x509_extensions             = v3_intermediate_ca

[ req_distinguished_name ]
0.organizationName                  = Organization Name
organizationalUnitName              = Organizational Unit Name
emailAddress                        = Email Address
emailAddress_max            = 40
localityName                = Locality Name (city, district)
stateOrProvinceName         = State or Province Name (full name)
countryName             = Country Name (2 letter code)
countryName_min             = 2
countryName_max             = 2
commonName              = Common Name (hostname, IP, or your name)
commonName_max              = 64

0.organizationName_default      = yourcompany
organizationalUnitName_default      = yourcompany Intermediate Certification
emailAddress_default            = info@yourcompany.com
localityName_default            = Okeefenokee
stateOrProvinceName_default     = Wisconsin [should be spelled out]
countryName_default         = US

[ v3_intermediate_ca ]
subjectKeyIdentifier            = hash
authorityKeyIdentifier          = keyid:always,issuer
basicConstraints            = critical, CA:true, pathlen:0
keyUsage                = critical, digitalSignature, cRLSign, keyCertSign

# Important - the pathlen parameter prevents this cert from being used to create new intermediate certs. The subsequent subsections for server and client certs allows you to specify their type and intended usage, as distinct from the intermediate cert, in the same cfg file 

[ usr_cert ]
basicConstraints            = CA:FALSE
nsCertType              = client, email
nsComment               = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier            = hash
authorityKeyIdentifier          = keyid,issuer
keyUsage                = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage            = clientAuth, emailProtection

[ server_cert ]
basicConstraints            = CA:FALSE
nsCertType              = server
nsComment               = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier            = hash
authorityKeyIdentifier          = keyid,issuer:always
keyUsage                = critical, digitalSignature, keyEncipherment
extendedKeyUsage            = serverAuth

[ crl_ext ]
authorityKeyIdentifier          = keyid:always

2. 将证书传输到iPad

参考: 如何在iPad上注册应用程序以打开PDF文件

苹果公司建议注册应用程序处理的新文件类型,并将重命名为新自定义扩展名的p12文件传输到设备(手动或电子邮件)以安装客户端证书。 p12文件应包括公共证书链以及在步骤1中定义的客户端证书信息。当您尝试打开此类文件时,设备会向您的应用程序委托发送启动/唤醒消息,您需要处理此消息(不要在didload中处理,因为它可能是唤醒消息)。

这在v8或9中有所改变,但我需要支持7,因此这是针对已弃用的处理程序。不过解决方案相同,并从添加到应用程序plist文件开始,如下面的屏幕截图所示。

请注意,您将需要两个新的图标和文件扩展名,这不太可能被其他应用程序使用。

enter image description here

enter image description here

接下来,您需要委托/处理程序,其应该是不言自明的。由于此部分与正常控制流程无关,因此我在AppDelegate.m中处理所有委托处理(这样做是否有问题?)根据需要设置方法/变量,并请忽略对文件存在性的过度检查...

参考: https://www.raywenderlich.com/6475/basic-security-in-ios-5-tutorial-part-1

- (BOOL) application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication
           annotation:(id)annotation {

    if (url) {

        self.p12Data = [NSData dataWithContentsOfFile:[url path]];
        
        if (!p12Data) {
            [self messageBox:@"Warning" : @"Failed to read data file, cancelling certificate import"];
        }
        else {
            [self presentAlertViewForPassPhrase];
        }
        
        NSFileManager * fileManager = [NSFileManager defaultManager];
        if ( [fileManager fileExistsAtPath:[url path]] ) {
            [fileManager removeItemAtPath:[url path] error:NULL];
        }
    }

    return YES;
}

- (void)presentAlertViewForPassPhrase {
    
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Certificate Credentials"
                                                    message:@"Please enter the passphrase for your certificate"
                                                   delegate:self
                                          cancelButtonTitle:@"Cancel"
                                          otherButtonTitles:@"Done", nil];
    [alert setAlertViewStyle:UIAlertViewStyleSecureTextInput];
    [alert show];
}

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
    
    if (buttonIndex == 1) {                                             // User selected "Done"
        UITextField *ppField = [alertView textFieldAtIndex:0];
        if ([ppField.text length] > 0) {
            [self loadCertificates:ppField.text];
        }
        //Handle Else
    }
    else
    {                                                                   // User selected "Cancel"
        [self messageBox:@"Information" : @"Certificate import cancelled"];
    }
}

3. 将接收到的凭据保存在应用程序钥匙串中

现在您已经获得了原始的p12数据,接下来应该很容易想出下一步要做什么...并不是这样。所有文档似乎都是关于名称/密码存储的,许多帖子建议将服务器证书保存到文件系统中,这样做还可以,但当你有钥匙串时绝对没有意义,而苹果也说这就是它的用途。最后但并非最不重要的是,如何区分已存储的证书,并如何更新它们?

长话短说,我决定在尝试各种行不通的方法后进行完全删除/重新保存,以检查应该是更新还是初始加载 - 因为这是我的应用程序链。所有这些都是CF内容,我没有使用ARC,因为我拒绝移植我不必要的任何东西。据我所知,只要分配CF、转换为NS,并在使用后CFRelease,就没有警告。

以下是关键参考:

枚举iOS应用程序中的所有钥匙串项

[有助于帮助可视化您的钥匙串是什么样子]

如何删除应用程序可以访问的所有钥匙串项?

什么使iOS中的钥匙串项成为唯一的?

http://help.sap.com/saphelp_smp307sdk/helpdata/en/7c/03830b70061014a937d8267bb3f358/content.htm

[https://developer.apple.com/library/ios/samplecode/AdvancedURLConnections/Listings/Credentials_m.html,其中说:

// IMPORTANT: SecCertificateRef's are not uniqued (that is, you can get two
// different SecCertificateRef values that described the same fundamental
// certificate in the keychain), nor can they be compared with CFEqual. So
// we match up certificates based on their data values.

总结一下,最简单的方法就是给证书分配一个标签,这样你就可以独立查找它并意识到如果你保存一个身份,它将自动分成密钥和证书,这可能会导致替换时出现一些困难。

代码(解释如下):

- (void) loadCertificates:(NSString *)passPhrase {
    
    BOOL lastError = false;
    NSMutableDictionary * p12Options = [[NSMutableDictionary alloc] init];
    [p12Options setObject:passPhrase forKey:(id)kSecImportExportPassphrase];
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
    OSStatus err = SecPKCS12Import((CFDataRef)p12Data, (CFDictionaryRef)p12Options, &items);
    if (err != noErr) {
        [self messageBox:@"Error" : @"Unable to extract security information with the supplied credentials. Please retry"];
        lastError = true;
    }
    if (!lastError && err == noErr && CFArrayGetCount(items) > 0) {
        CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
        //Clean-up

        NSArray *secItemClasses = [NSArray arrayWithObjects:
                                   (id)kSecClassCertificate,
                                   (id)kSecClassKey,
                                   (id)kSecClassIdentity,
                                   nil];
        
        for (id secItemClass in secItemClasses) {
            NSDictionary *spec = @{(id)kSecClass: secItemClass};
            err = SecItemDelete((CFDictionaryRef)spec);
        }

        //Client Identity & Certificate
        
        SecIdentityRef clientIdentity = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
        
        NSDictionary *addIdentityQuery = [NSDictionary dictionaryWithObjectsAndKeys:
                                          kClientIdentityLabel, kSecAttrLabel,
                                          (id)clientIdentity, kSecValueRef,
                                          nil];
        err = SecItemAdd((CFDictionaryRef)addIdentityQuery, NULL);
        if (err == errSecDuplicateItem) {
            NSLog(@"Duplicate identity");
        }
        if (err != noErr) {
            [self messageBox:@"Warning" : @"Failed to save the new identity"];
            lastError = true;
        }
        //Server Certificate
        CFArrayRef chain = CFDictionaryGetValue(identityDict, kSecImportItemCertChain);
        CFIndex N = CFArrayGetCount(chain);
        BOOL brk = false;
        for (CFIndex i=0; (i < N) && (brk == false); i++) {
            SecCertificateRef cert = (SecCertificateRef)CFArrayGetValueAtIndex(chain, i);
            CFStringRef summary = SecCertificateCopySubjectSummary(cert);
            NSString* strSummary = [[NSString alloc] initWithString:(NSString *)summary];
            if ([strSummary containsString:@"Root"] || (i == N)) {
                
                NSDictionary *addCertQuery = [NSDictionary dictionaryWithObjectsAndKeys:
                                              kServerCertificateLabel, kSecAttrLabel,
                                              (id)cert, kSecValueRef,
                                              nil];
                err = SecItemAdd((CFDictionaryRef)addCertQuery, NULL);
                if (err == errSecDuplicateItem) {
                    NSLog(@"Duplicate root certificate");
            }
            if (err != noErr) {
                [self messageBox:@"Warning" : @"Failed to save the new server certificate"];
                lastError = true;
            }
            brk = true;
        }
        [strSummary release];
        CFRelease(summary);
    }
}
else {
    [self messageBox:@"Error" : @"Unable to extract security information with the supplied credentials. Please retry"];
    lastError = true;
}
    [p12Options release];
    CFRelease(items);
    if (!lastError) [self messageBox:@"Information" : @"Certificate import succeeded"];
}

其中kClientIdentityLabel和kServerCertificateLabel是任意的标签。

kSec函数太多/复杂,无法在此详细解释。只需说明一切都被清除,然后保存提取的客户端身份,接着提取根CA,然后分别保存。为什么要循环呢?因为我不知道是否正确假设根在链的末尾,但如果我生成p12文件,它将在末尾,所以代码现在就在那里。

请注意,来自kSec的错误是编码的,因此该网站是必不可少的:https://www.osstatus.com

4.从钥匙串中检索保存的凭据

一旦凭据在钥匙串中,您可以按如下方式提取它们(失败模式有待改进):

- (void) reloadCredentials {
    
    self.clientCredential = nil;
    self.serverCertificateData = nil;
    
    if (self.useClientCertificateIfPresent) {
        
        NSDictionary* idQuery = [NSDictionary dictionaryWithObjectsAndKeys:
                                 kClientIdentityLabel,            kSecAttrLabel,
                                 (id)kSecClassIdentity,           kSecClass,
                                 kCFBooleanTrue,                  kSecReturnRef,
                                 kSecMatchLimitAll,               kSecMatchLimit,
                                 nil];
        CFArrayRef result = nil;
        OSStatus err = SecItemCopyMatching((CFDictionaryRef)idQuery, (CFTypeRef*)&result);
        if (err == errSecItemNotFound) {
            [self messageBox:@"Warning" : @"Client credentials not found. Server connection may fail"];
        }
        else if (err == noErr && result != nil ) {
            
            SecIdentityRef clientIdentity = (SecIdentityRef)CFArrayGetValueAtIndex(result, 0);
            
            SecCertificateRef clientCertificate;
            SecIdentityCopyCertificate(clientIdentity, &clientCertificate);
            const void *certs[] = { clientCertificate };
            CFArrayRef certsArray = CFArrayCreate(NULL, certs, 1, NULL);
            self.clientCredential = [NSURLCredential credentialWithIdentity:clientIdentity certificates:(NSArray*)certsArray
                                                                      persistence:NSURLCredentialPersistenceNone];
            CFRelease(certsArray);
            CFRelease(clientCertificate);
            CFRelease(result);
        }
        else {
            [self messageBox:@"Warning" : @"Client or Server credentials not found. Server connection may fail"];
        }
        
        NSDictionary* serverCertQuery = [NSDictionary dictionaryWithObjectsAndKeys:
                                         kServerCertificateLabel,         kSecAttrLabel,
                                         (id)kSecClassCertificate,        kSecClass,
                                         kCFBooleanTrue,                  kSecReturnRef,
                                         kSecMatchLimitAll,               kSecMatchLimit,
                                         nil];
        CFArrayRef result1 = nil;
        err = SecItemCopyMatching((CFDictionaryRef)serverCertQuery, (CFTypeRef*)&result1);
        if (err == errSecItemNotFound) {
            [self messageBox:@"Warning" : @"Server certificate not found. Server connection may fail"];
        }
        else if (err == noErr && result1 != nil ) {
            
            SecCertificateRef certRef = (SecCertificateRef)CFArrayGetValueAtIndex(result1, 0);
            CFDataRef certRefData = SecCertificateCopyData(certRef);
            self.serverCertificateData = (NSData *)certRefData;
            CFRelease(certRefData);
            CFRelease(result1);
        }
        else {
            [self messageBox:@"Warning" : @"Client or Server credentials not found. Server connection may fail"];
        }
    }
}

5. 认证服务器证书并返回客户端凭据

哎呀。这是一个编辑,解释如何实际使用检索到的证书(它本来应该是容易明显的部分...)

首先,苹果已经可疑的文档被新的应用程序传输安全框架所取代(例如: http://useyourloaf.com/blog/app-transport-security/)。我不会在这里详细讨论,但其想法是通过默认强制每个人始终使用https和受信任的证书来强制执行。对于我的场景,使用证书固定和专用客户端与私有服务器之间的相互认证,您可以通过添加字典到您的plist中安全地关闭此功能,如下所示:

enter image description here

接下来,在第4步中,您已经拥有了客户端凭据以立即响应该挑战,但服务器证书作为DER格式的NSData漂浮在周围,由SecCertificateCopyData创建,不清楚该挑战到达时应该发生什么。

事实证明,您应该实现“X.509标准”第6节中的算法(https://www.rfc-editor.org/rfc/rfc5280)。幸运的是,iOS SecTrustEvaluate函数在幕后实现了这一点,但需要构建脚手架和理解奇怪的东西。

[轻微问题-空间不足!添加了一个新问题,包括此步骤的结尾。]

https://stackoverflow.com/questions/35964632/correctly-use-a-pinned-self-signed-certificate-in-ios-9-2

[从其他帖子继续]

就是这样。对于不太成熟的质量我很抱歉,但是我想在脑海中清晰时把这个东西拍起来。如果我发现错误,我会更新帖子。

希望这有所帮助,这里是一本非常好的书的最后链接,其中将让您感到不安的是信任商业CA...

https://www.cs.auckland.ac.nz/~pgut001/pubs/book.pdf


1
嗨,伙计,很棒的帖子。不过,你的后续帖子已被删除。你能在聊天中提供其余部分吗? - jlmurph
2
不幸的是,有人对此进行了负面评价:“我投票关闭此问题,因为这是一篇博客文章,而不是一个问题 - Paulw11 Mar 13 '16 at 1:14”。也许有人有权重新激活它,它还在那里吗? - saminpa
顺便说一句,谢谢。我很乐意发送剩下的内容,但是在这个论坛上没有私信功能。 - saminpa
我不认为你能把剩下的放在问题的第二个答案中,对吧? - cacsar
由于原始问题无法访问,因此我在下面添加了余数作为新答案。希望有所帮助。 - saminpa
你能提供源代码的Git仓库链接吗?我尝试了你的解决方案但没有成功。谢谢@saminpa。 - Krishna

3

[我刚意识到我可以添加另一个答案,因为延续链接被投票反对并关闭了,并且有两个请求获取无法适合上方的其他信息。下面的答案从已删除帖子中的问题开始]

...我仍然不清楚的部分是为什么我必须创建一个新的信任和策略来实现锚定证书和固定。

如果我只是将锚点添加到从服务器接收到的信任中,我不能成功地将指针返回给从服务器接收到的NSURLCredential,它似乎被发送者修改和拒绝了(?).

问题是,这真的是适当的处理方式吗?还是可以简化它?这有点令人疲倦,但我不想接受某些东西,只因为它“有效”。我的当前解决方案如下所示。

在第4步中,您已经有了客户端凭据来响应那种类型的挑战,而服务器证书以DER格式的NSData形式漂浮在周围,由SecCertificateCopyData创建,并且不清楚该挑战到达时应该发生什么。

事实证明,您应该执行“X.509标准”(https://www.rfc-editor.org/rfc/rfc5280)第6节中的算法。幸运的是,iOS SecTrustEvaluate函数在幕后实现了这一点,但是需要构建支架并理解奇怪的东西。首先是代码(向我的原始来源致以敬意):

SecTrustEvaluate总是返回带有SecPolicyCreateSSL的kSecTrustResultRecoverableTrustFailure

    - (void)_handleServerTrustChallenge {

    OSStatus status;
    BOOL trusted = false;
    SecTrustResultType trustResult;
    SecTrustRef serverTrust = self.challenge.protectionSpace.serverTrust;
    NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; //candidate credential for return to sender if valid

    if (svDelegate.serverCertificateData) {

        //locally stored information
        SecCertificateRef storedCertificate = SecCertificateCreateWithData(NULL, (CFDataRef)svDelegate.serverCertificateData);
        NSMutableArray *anchorCertArray = [NSMutableArray arrayWithCapacity:1];
        [anchorCertArray addObject:(id)storedCertificate];

        //incoming credentials from server
        NSMutableArray *receivedCertChain = [NSMutableArray array];
        for(int i = 0; i < SecTrustGetCertificateCount(serverTrust); i++)
            [receivedCertChain addObject:(id) SecTrustGetCertificateAtIndex(serverTrust,i))];

        //new custom policy object to use in creating new trust
        //YES indicates extendedKeyUsage is set to serverAuth; effectively ignore server name check by specifying incoming name
        SecPolicyRef newPolicyRef = SecPolicyCreateSSL(YES, (CFStringRef)self.challenge.protectionSpace.host);

        //create and evaluate new trust with pinned certificate
        SecTrustRef newTrustRef = NULL;
        SecTrustCreateWithCertificates((CFArrayRef) receivedCertChain, newPolicyRef, &newTrustRef);
        status = SecTrustSetAnchorCertificates(newTrustRef, (CFArrayRef) anchorCertArray);
        if (status == noErr) status = SecTrustSetAnchorCertificatesOnly(newTrustRef, TRUE);
        if (status == noErr) status = SecTrustEvaluate(newTrustRef, &trustResult);  

        //----- debug -------
        //CFShow(newPolicyRef);
        //NSLog(@"%@", receivedCertChain);     

        CFRelease(newTrustRef);
        CFRelease(newPolicyRef);
        CFRelease(storedCertificate);
    }
    else {  //Server certificate not stored, rely on standard trusted Root CA authorities

        status = SecTrustEvaluate(serverTrust, &trustResult);
    }

    trusted = (status == noErr) && (trustResult == kSecTrustResultUnspecified);

    if (!trusted) credential = nil;
    [self stopWithCredential:credential];
    [self.delegate challengeHandlerDidFinish:self];
}

首先,我检查服务器证书是否已加载(否则通过传统受信任的 CA 方法处理)。

接下来,您选择要评估的 "trust object"。如果直接使用从服务器收到的 trust object 进行操作,则可能会破坏 'NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]' 引用。从苹果文档中可以看出,这是一种合法的方法,但此处建议查阅 x.509 rfc 以便更好理解。

https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html

[https://developer.apple.com/library/ios/technotes/tn2232/_index.html#//apple_ref/doc/uid/DTS40012884-CH1-SECCUSTOMROOT][2]

信任需要一个“策略”、待评估的入站证书链和一个或多个“基准证书”,它们基本上定义了任意坐标系中的原点 - 即使不是根证书,所有东西都将在零点下游验证。

因此,您需要把入站链和存储的证书加载到数组中,供新的信任使用,并使用 SecPolicyCreateSSL 创建一个新策略 - 这会设置一个标志,表明应检查证书是否已发出用于 serverAuth,并忽略传入服务器名称(以允许某些基础设施的灵活性)。

接下来,使用新策略和要进行身份验证的证书数组创建新的信任。然后设置锚点并确保链只针对您的基准证书进行评估,而不只是 iOS 密钥链中的任何内容。

在评估信任时,您可能会觉得奇怪,为什么要接受 kSecTrustResultUnspecified 而不是继续或听起来更积极的其他操作。实际上,继续表示您正在遵循用户界面中的覆盖,因此实际上是有问题的;未指定只是表示根据指定的策略没有问题。

最后,从传入的 trust object(而不是新对象)返回凭据,然后一切都应该正常......


这不起作用...客户端证书没有被发送。 - Krishna
Krishna,非常抱歉,这是两年前的帖子,我无法猜测您的代码出了什么问题,或者是否因为操作系统升级而出现问题。尽管如此,我建议使用调试器和实体设备(而不是模拟器),以验证处理程序是否被执行,并且设备不会因为服务器证书不是来自公共CA(自签名)而拒绝接受传入的服务器证书。或者您可以发布一个带有您的代码的问题,也许有人可以帮助找出问题所在。 - saminpa
我正在使用WKWebview...服务器信任是有效的...但是当我查看tcp dump时,我发现没有发送客户端证书...你的帖子真的很有帮助...但我不知道在哪里找到答案... - Krishna
非常抱歉,我对WKWebview一无所知。但如果您可以确认在第4步中,在“self.clientCredential = [NSURLCredential credentialWithIdentity:clientIdentity certificates:(NSArray*)certsArray persistence:NSURLCredentialPersistenceNone];”对应的行上具有有效数据,那么我会发表一个问题,询问如何确保它在您的情况下添加到响应中。 - saminpa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接