12

我最近经历了一个极其艰巨的过程来构建一些应该非常简单但似乎在任何地方基本上都找不到的东西。我想尝试将所有内容都放在此处,以询问我是否做错了什么,如果没有,则可以帮助需要此信息的任何人。

背景:我试图为其提供安全性的产品/服务是围绕 Windows 服务器上的 WCF 服务构建的,这些服务只能通过 PC 或 iPad 上的自定义客户端应用程序访问。每个客户一台服务器,无浏览器访问。一切都已经通过使用 Windows 标准机制和来自商业 CA 的证书的身份验证和授权进行了 TLS 保护。

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

让所有这些都为 iPad 工作是一场可怕的噩梦,其中包含大量虚假信息或部分正确的建议。在接下来的内容中,我试图链接到最好的来源,但如果我无意中错过了归因,我深表歉意。如果这篇文章有任何错误/误导,请发表评论。

谢谢

4

2 回答 2

22

主要步骤是:

  1. 创建一个生成证书的系统(如果它是一个生产系统,那么简单但不重要)
  2. 将证书传输到 iPad(未嵌入应用商店捆绑包中!)
  3. 将所有收到的凭据保存在应用程序钥匙串中(Apple 表示它们属于)
  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] 非常棒,除了标准接口是 cmdline 并且文档很难遵循。

我希望有人事先向我解释的内容是显而易见的,但不是:[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

以下是 cfg 文件: 根

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 文件

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

这在 v8 或 9 中有所改变,但我需要支持 7,所以这适用于已弃用的处理程序。相同的解决方案,它首先添加到应用 plist 文件,如下面的屏幕截图所示。

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

在此处输入图像描述

在此处输入图像描述

接下来,您需要不言自明的委托/处理程序。由于这部分与正常的控制流无关,我正在处理 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 数据,应该很容易弄清楚下一步该做什么......不是。所有文档似乎都用于名称/密码存储,并且大量海报建议将服务器证书保存到文件系统,这没关系,但是当您拥有钥匙串并且 Apple 说这就是它的用途时,这绝对没有意义。最后但同样重要的是,您如何区分存储的证书以及如何更新它们?

长话短说,在尝试了各种不起作用的事情以检查它是否应该是更新或初始加载之后,我决定进行完整的删除/重新保存——除此之外,这是我首先想做的,因为这是我的应用链。所有这些都是 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.

总结是(duh)最简单的事情是为证书分配一个标签,以便您可以唯一地查找它并意识到如果您保存身份,它将自动拆分为密钥和证书,这可能 - 不是当然 - 导致更换时遇到一些困难。

代码(解释如下):

- (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. 验证服务器证书并返回客户端凭据

呼男孩。这是一个解释如何实际使用检索到的证书的编辑(它应该是显而易见的部分......)

首先,Apple 已经可疑的文档被新的应用程序传输安全框架淘汰(例如,参见:http: //useyourloaf.com/blog/app-transport-security/)。我不打算在这里讨论它,但这个想法是强制每个人在默认情况下始终使用 https 和受信任的证书。对于我的场景,通过专用客户端和私有服务器之间的证书固定和相互身份验证,您可以通过向 plist 添加字典来安全地关闭此功能,如下所示:

在此处输入图像描述

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

事实证明,您应该做的是实现“X.509 标准”(https://www.rfc-editor.org/rfc/rfc5280)第 6 节中的算法。幸运的是,这是由 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

于 2016-03-08T14:52:11.787 回答
3

[我只是(!)意识到我可以添加另一个答案,因为继续链接被否决并关闭,并且有两个请求不适合上面的附加信息。下面的答案从已删除帖子中的问题开始]

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

如果我只是将锚添加到从服务器接收到的信任中,我无法成功返回指向从服务器接收到的 NSURLCredential 的指针,它似乎被发送者修改和拒绝(?)。

问题是,这真的是适当的处理还是可以浓缩?这有点令人厌烦,但我不想仅仅因为它“有效”而接受某些东西。我目前的解决方案如下所示。

在第 4 步中,您已经拥有客户端凭据来响应该类型的质询而无需进行操作,但服务器证书作为由 SecCertificateCopyData 创建的 DER 格式的 NSData 浮动,并且不清楚当质询到达时会发生什么。

事实证明,您应该做的是实现“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 方法进行其他处理)。

接下来选择要评估的“信任对象”。如果我没有制作我通过线路从服务器 b/c 收到的信任对象的工作副本,我就无法做到这一点,如果我直接使用它,它会以某种方式弄乱“NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]”引用。尽管从真正可怕的 Apple 文档中看来,这是一种犹太方法(我,例如,如果您想了解其中的任何内容,我建议您略读 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 颁发,并且传入服务器name 应该被忽略(以允许一些基础设施的灵活性)。

接下来,您使用新策略和要验证的证书数组创建新信任。然后设置锚点并确保仅根据您的锚点证书评估链,而不仅仅是 iOS 钥匙串中的任何内容。

当您评估信任时,您接受 kSecTrustResultUnspecified 并且不继续或更听起来更积极的事情可能看起来很奇怪。实际上,继续意味着您正在从用户界面进行覆盖,这实际上很糟糕;未指定只是意味着根据指定的策略没有任何问题。

最后,您从传入的信任对象(不是新的)返回凭证,一切都应该是金色的......

于 2018-03-24T12:38:30.763 回答