iOS安全-证书、密钥及信任服务(Certificate, Ke
本章描述并演示了如何使用证书、密钥和信任服务去导入一个身份(identity),评估证书是否可信,判断信任失败的原因,以及信任失败后的恢复。
本章按如下顺序分别演示了:
- 导入一个 identity.
- 从导入的数据中获得证书.
- 获得用于证书评估的策略.
- 校验证书,根据指定策略评估证书是否可信.
- 测试证书中的可恢复错误.
- 判断证书是否过期.
- 改变评估条件,忽略过期证书.
- 重新评估证书.
Certificate, Key, and Trust Services Concepts提供了证书,密钥,信任服务的概念和术语。证书,密钥,信任服务函数的详细信息参见Certificate, Key, and Trust Services Reference.
文章中的代码片段假设你已经引入了以下头文件。
#import <UIKit/UIKit.h>
#import <Security/Security.h>
#import <CoreFoundation/CoreFoundation.h>
1. 从一个.p12文件中提取、评估身份
如果你需要在iOS设备上使用加密过的identity(一个密钥及其关联的证书)进行客户端认证,例如:你可以把PKCS#12数据以受密码保护的文件的方式安全地传输到这个设备上。本节显示如何从PKCS#12数据中提取identity和trust objects(可信任对象),并评估其可信度。
列表2-1 显示了用SecPKCS12Import函数从.p12文件中提取identity和可信任对象,以及评估其可信度。
列表 2-2 显示如何从identity中获取证书并显示证书信息。每个列表后都对代码进行了解释。
在编译这段代码时,请确认在Xcode工程中加入了Security.framework。
Listing 2-1 Extractingidentity and trust objects from PKCS #12 Data
从PKCS#12数据中提取identity和trust对象
OSStatus extractIdentityAndTrust(CFDataRef inPKCS12Data,
SecIdentityRef *outIdentity,
SecTrustRef *outTrust,
CFStringRef keyPassword)
{
OSStatus securityError = errSecSuccess;
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { keyPassword };
CFDictionaryRef optionsDictionary = NULL;
/* Create a dictionary containing the passphrase if one
was specified. Otherwise, create an empty dictionary. */
optionsDictionary = CFDictionaryCreate(
NULL, keys,
values, (keyPassword ? 1 : 0),
NULL, NULL); // 1 创建要传给 SecPKCS12Import的包含密码的字典
CFArrayRef items = NULL;
securityError = SecPKCS12Import(inPKCS12Data,
optionsDictionary,
&items); // 2 从PKCS #12数据中导出证书、密钥、信任,放到数组中。
//
if (securityError == 0) {
// 3 从数组中取出第一个字典,并从这个字典中取出身份和信任。
// SecPKCS12Import方法为PKCS #12数据中的每一个条目(身份或证书)返回一个字典。
// 在这个例子中被导出的身份是数组中的第一个(item #0)。
CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
const void *tempIdentity = NULL;
tempIdentity = CFDictionaryGetValue (myIdentityAndTrust,
kSecImportItemIdentity);
CFRetain(tempIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
const void *tempTrust = NULL;
tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
CFRetain(tempTrust);
*outTrust = (SecTrustRef)tempTrust;
}
if (optionsDictionary) // 4 释放这些不用的字典和数组
CFRelease(optionsDictionary);
if (items)
CFRelease(items);
return securityError;
}
这段代码假设:
- 你已经加载PKCS#12文件为NSData或CFDataRef对象。
- 已经得到了密钥的密码。
这里是这段代码做的内容:
- 创建要传给 SecPKCS12Import的包含密码的字典
- 从PKCS #12数据中导出证书、密钥、信任,放到数组中。
- 从数组中取出第一个字典,并从这个字典中取出身份和信任。SecPKCS12Import方法为PKCS #12数据中的每一个条目(身份或证书)返回一个字典。在这个例子中被导出的身份是数组中的第一个(item #0)。
- 释放这些不用的字典和数组
完成这些步骤,你应该:
- 释放包含新数据的CFDataRef对象
- 返回的信任对象通过调用SecTrustEvaluate 或 SecTrustEvaluateAsync评估信任。
- 处理信任结果。
- 如果信任结果是kSecTrustResultInvalid, kSecTrustResultDeny, kSecTrustResultFatalTrustFailure,你不能继续,以失败结束。
如果信任结果是kSecTrustResultRecoverableTrustFailure,你应该从信任失败中恢复。
下面的代码清单显示了如何从身份中获取证书,如何展示证书的信息。在编译这段代码时,请确认在Xcode工程中加入了Security.framework。
Listing 2-2 Displaying information from the certificate
显示证书信息
NSString *copySummaryString(SecIdentityRef identity)
{
// Get the certificate from the identity.
SecCertificateRef myReturnedCertificate = NULL;
OSStatus status = SecIdentityCopyCertificate (identity,
&myReturnedCertificate); // 1 从证书中提取身份
if (status) {
NSLog(@"SecIdentityCopyCertificate failed.\n");
return NULL;
}
CFStringRef certSummary = SecCertificateCopySubjectSummary
(myReturnedCertificate); // 2 从证书中获取概要信息。
NSString* summaryString = [[NSString alloc]
initWithString:(__bridge NSString *)certSummary]; // 3转换string为NSString对象
CFRelease(certSummary); //4 释放NSString对象
return summaryString;
}
2. 获取和使用持久化的钥匙串
当你在钥匙串中添加或查找一个条目时,你需要有一个持久化的引用。因为持久化引用能保证在程序从启动到能写入磁盘这段时间内,始终可用。当需要反复在钥匙串中查找条目时,使用持久化引用更加容易。以下代码演示如何获取一个identity 的持久化引用。
Listing 2-3 Gettinga persistent reference for an identity
CFDataRef persistentRefForIdentity(SecIdentityRef identity)
{
OSStatus status = errSecSuccess;
CFTypeRef persistent_ref = NULL;
const void *keys[] = { kSecReturnPersistentRef, kSecValueRef };
const void *values[] = { kCFBooleanTrue, identity };
CFDictionaryRef dict = CFDictionaryCreate(NULL, keys, values,
2, NULL, NULL);
status = SecItemAdd(dict, &persistent_ref);
if (dict)
CFRelease(dict);
return (CFDataRef)persistent_ref;
}
下面的示例展示了如何从持久化引用的钥匙串中检索身份对象。
Listing 2-4 Gettingan identity using a persistent reference
SecIdentityRef identityForPersistentRef(CFDataRef persistent_ref)
{
CFTypeRef identity_ref = NULL;
const void *keys[] = { kSecClass, kSecReturnRef, kSecValuePersistentRef };
const void *values[] = { kSecClassIdentity, kCFBooleanTrue, persistent_ref };
CFDictionaryRef dict = CFDictionaryCreate(NULL, keys, values,
3, NULL, NULL);
SecItemCopyMatching(dict, &identity_ref);
if (dict)
CFRelease(dict);
return (SecIdentityRef)identity_ref;
}
3. 从钥匙串中查找证书
以下代码演示如何在要是串中查找使用名称识别的证书。在钥匙串中找到一个持久化引用的条目,参考列表2-4。要用一个id字串查找一个条目,参考“数据加密和解密”。
Listing 2-5 Findinga certificate In the Keychain
void certificateInKeychain(){
OSStatus status = errSecSuccess;
CFTypeRef certificateRef = NULL; // 1
const char *certLabelString = "Romeo Montague";
CFStringRef certLabel = CFStringCreateWithCString(
NULL, certLabelString,
kCFStringEncodingUTF8); // 2
const void *keys[] = { kSecClass, kSecAttrLabel, kSecReturnRef };
const void *values[] = { kSecClassCertificate, certLabel, kCFBooleanTrue };
CFDictionaryRef dict = CFDictionaryCreate(NULL, keys,
values, 3,
NULL, NULL); // 3
status = SecItemCopyMatching(dict, &certificateRef); // 4
if (status == errSecSuccess) {
CFRelease(certificateRef);
certificateRef = NULL;
}
/* Do something with certificateRef here */
if (dict)
CFRelease(dict);
}
4. 获取策略对象并评估可信度
评估证书可信度之前,必需获取到一个证书对象的引用。你可以从一个identity中提取一个证书对象(列表 2-2),也可以从DER证书数据中创建证书对象(使用SecCertificateCreateWithData函数,见列表2-6),或者从钥匙串中查找证书(列表 2-5)。
评估信任度的标准由信任策略(trust policy)指定。列表3-2 显示如何获得用于评估的策略对象。在iOS中有两种策略可用:Basic X509和SSL(参考AppleX509TP信任策略)。可以用SecPolicyCreateBasicX509或者SecPolicyCreateSSL函数获取策略对象。
下列代码显示了获取策略对象并用于评估证书是否可信。
Listing 2-6 Obtaining a policy reference object and evaluating trust
NSString *thePath = [[NSBundle mainBundle]
pathForResource:@"Romeo Montague" ofType:@"cer"];
NSData *certData = [[NSData alloc]
initWithContentsOfFile:thePath];
CFDataRef myCertData = (__bridge CFDataRef)certData; // 1
SecCertificateRef myCert;
myCert = SecCertificateCreateWithData(NULL, myCertData); // 2
SecPolicyRef myPolicy = SecPolicyCreateBasicX509(); // 3
SecCertificateRef certArray[1] = { myCert };
CFArrayRef myCerts = CFArrayCreate(
NULL, (void *)certArray,
1, NULL);
SecTrustRef myTrust;
OSStatus status = SecTrustCreateWithCertificates(
myCerts,
myPolicy,
&myTrust); // 4
SecTrustResultType trustResult;
if (status == noErr) {
status = SecTrustEvaluate(myTrust, &trustResult); // 5
}
//... // 6
if (trustResult == kSecTrustResultRecoverableTrustFailure) {
// ...;
}
// ...
if (myPolicy)
CFRelease(myPolicy);
在这段代码中:
- 查找证书文件并获取数据。本例中,该文件位于应用程序束。但你也可以从网络获取证书。如果证书存在于钥匙串中,参考“在钥匙串中查找证书”。
- 从证书数据中创建certificate引用。
- 创建用于评估证书的策略。
- 用证书和策略创建信任对象(trust)。如果存在中间证书或者锚证书,应把这些证书都包含在certificate数组中并传递给SecTrustCreateWithCertificates函数。这样会加快评估的速度。
- 评估一个信任对象。
- 处理信任结果(trust result)。如果信任结果是kSecTrustResultInvalid,kSecTrustResultDeny,kSecTrustResultFatalTrustFailure,你无法进行处理。如果信任结果是kSecTrustResultRecoverableTrustFailure,你可以恢复这个错误。参考“从信任失败中恢复”。
- 释放策略对象。
5. 从信任失败中恢复
信任评估的结果有多个,这取决于:是否证书链中的所有证书都能找到并全都有效,以及用户对这些证书的信任设置是什么。信任结果怎么处理则由你的程序来决定。例如,如果信任结果是kSecTrustResultConfirm,你可以显示一个对话框,询问用户是否允许继续。
信任结果kSecTrustResultRecoverableTrustFailure的意思是:信任被否决,但可以通过改变设置获得不同结果。例如,如果证书签发过期,你可以改变评估日期以判断是否证书是有效的同时文档是已签名的。列表2-7 演示如何改变评估日期。注意 CFDateCreate函数使用绝对时间(从2001年1月1日以来的秒数)。你可以用CFGregorianDateGetAbsoluteTime函数把日历时间转换为绝对时间。
void recoverFromTrustFailure(SecTrustRef myTrust)
{
SecTrustResultType trustResult;
OSStatus status = SecTrustEvaluate(myTrust, &trustResult); // 1
//Get time used to verify trust
CFAbsoluteTime trustTime,currentTime,timeIncrement,newTime;
CFDateRef newDate;
if (trustResult == kSecTrustResultRecoverableTrustFailure) {// 2
trustTime = SecTrustGetVerifyTime(myTrust); // 3
timeIncrement = 31536000; // 4
currentTime = CFAbsoluteTimeGetCurrent(); // 5
newTime = currentTime - timeIncrement; // 6
if (trustTime - newTime){ // 7
newDate = CFDateCreate(NULL, newTime); // 8
SecTrustSetVerifyDate(myTrust, newDate); // 9
status = SecTrustEvaluate(myTrust, &trustResult); // 10
}
}
if (trustResult != kSecTrustResultProceed) { // 11
//...
}
}
在这段代码中:
-
评估证书可信度。参考“获取策略对象并评估可信度”。
-
检查信任评估结果是否是可恢复的失败( kSecTrustResultRecoverableTrustFailure)。
-
取得证书的评估时间(绝对时间)。如果证书在评估时已经过期了,则被认为无效。
-
设置时间的递增量为1年(以秒计算)。
-
取得当前时间的绝对时间。
-
设置新时间(第2次评估的时间)为当前时间减一年。
-
检查评估时间是否大于1年前(最近一次评估是否1年前进行的)。如果是,使用新时间(1年前的时间)进行评估,看证书是否在1年前就已经过期。
-
把新时间转换为CFDateRef。也可以用NSDate,二者是完全互通的,方法中的NSDate*参数,可以用CFDateRef进行传递;反之亦可。
-
设置信任评估时间为新时间(1年前)。
-
再次进行信任评估。如果证书是因为过期(到期时间在1年内)导致前次评估失败,那么这次评估应该成功。
-
再次检查评估结果。如果仍不成功,则需要做更进一步的操作,比如提示用户安装中间证书,或则友好地告知用户证书校验失败。
6.加密和解密数据(Encrypting and Decrypting Data)
证书,密钥和信任API包含了生产不对称密钥对并用于数据加密和解密的函数。例如,您可能想要使用此功能来对你不想在备份数据中访问的数据进行加密。或者,你可能想使用公钥/私钥在你的iOS应用和桌面应用间通过网络发送加密数据。列表2-8 显示如何生成可用于手机的公/私钥对。列表2-9 显示如何用公钥加密数据,列表2-10 显示如何用私钥解密数据。注意,这几个示例都使用了cocoa对象(如NSMutableDictionary),而不是像本章其他示例那样使用了core foundation对象(如CFMutableDictionaryRef),Cocoa对象和对应的Core Foundation完全相同,免费桥接。例如:在方法中有个NSMutableDictionary *参数,你可以转化为CFMutableDictionaryRef,方法中的CFMutableDictionaryRef参数,你可以转换为NSMutableDictionary实例。
Listing 2-8 Generatinga key pair
生成密钥对
//1 定义独特的字符串作为属性添加到私钥和公钥密钥链项,来让他们以后更容易找到。
static const UInt8 publicKeyIdentifier[] = "com.apple.sample.publickey\0";
static const UInt8 privateKeyIdentifier[] = "com.apple.sample.privatekey\0";
- (void)generateKeyPairPlease
{
OSStatus status = noErr;
NSMutableDictionary *privateKeyAttr = [[NSMutableDictionary alloc] init];
NSMutableDictionary *publicKeyAttr = [[NSMutableDictionary alloc] init];
NSMutableDictionary *keyPairAttr = [[NSMutableDictionary alloc] init];
// 2 为SecKeyGeneratePair方法中的属性分配字典
NSData * publicTag = [NSData dataWithBytes:publicKeyIdentifier
length:strlen((const char *)publicKeyIdentifier)];
NSData * privateTag = [NSData dataWithBytes:privateKeyIdentifier
length:strlen((const char *)privateKeyIdentifier)];
// 3 创建包含步骤1中定义的标识符字符串的NSData对象
SecKeyRef publicKey = NULL;
SecKeyRef privateKey = NULL; // 4 为公钥和私钥创建SecKeyRef对象
[keyPairAttr setObject:(__bridge id)kSecAttrKeyTypeRSA
forKey:(__bridge id)kSecAttrKeyType]; // 5 设置密钥对类型属性为RSA
[keyPairAttr setObject:[NSNumber numberWithInt:1024]
forKey:(__bridge id)kSecAttrKeySizeInBits]; // 6 设置密钥对长度为1024字节
[privateKeyAttr setObject:[NSNumber numberWithBool:YES]
forKey:(__bridge id)kSecAttrIsPermanent]; // 7 设置私钥的持久化属性(即是否存入钥匙串)为YES
[privateKeyAttr setObject:privateTag
forKey:(__bridge id)kSecAttrApplicationTag]; // 8 把1-3步中的identifier放到私钥的dictionary中
[publicKeyAttr setObject:[NSNumber numberWithBool:YES]
forKey:(__bridge id)kSecAttrIsPermanent]; // 9 设置公钥的持久化属性(即是否存入钥匙串)为YES
[publicKeyAttr setObject:publicTag
forKey:(__bridge id)kSecAttrApplicationTag]; // 10 把1-3步中的identifier放到公钥的dictionary中
[keyPairAttr setObject:privateKeyAttr
forKey:(__bridge id)kSecPrivateKeyAttrs]; // 11 把私钥的属性集(dictionary)加到密钥对的属性集(dictionary)中
[keyPairAttr setObject:publicKeyAttr
forKey:(__bridge id)kSecPublicKeyAttrs]; // 12 把公钥的属性集(dictionary)加到密钥对的属性集(dictionary)中
status = SecKeyGeneratePair((__bridge CFDictionaryRef)keyPairAttr,
&publicKey, &privateKey); // 13 产生密钥对
// error handling...
if(publicKey) CFRelease(publicKey);
if(privateKey) CFRelease(privateKey); // 14 释放不用的对象内存
}
你可以将您的公钥发送给任何人,谁可以使用它来加密数据。如果你保持你的私钥安全,那么只有你能够解密数据。以下代码示例展示了如何使用公钥加密数据。这可以在设备上生成一个公钥(参见前面的代码示例)或者从发送给你或钥匙链中的证书中提取公钥。您可以使用SecTrustCopyPublicKey函数来从证书中提取公钥。在下面的代码示例中,假定密钥已经在设备上生成并放置在钥匙串中。
Listing 2-9 Encryptingdata with a public key
用公钥加密数据
- (NSData *)encryptWithPublicKey
{
OSStatus status = noErr;
size_t cipherBufferSize;
uint8_t *cipherBuffer; // 1 为加密文本分配缓冲区
// [cipherBufferSize]
const uint8_t dataToEncrypt[] = "the quick brown fox jumps "
"over the lazy dog\0"; // 2 指定要加密的文本
size_t dataLength = sizeof(dataToEncrypt)/sizeof(dataToEncrypt[0]);
SecKeyRef publicKey = NULL; // 3 定义SecKeyRef,用于公钥。
NSData * publicTag = [NSData dataWithBytes:publicKeyIdentifier
length:strlen((const char *)publicKeyIdentifier)]; // 4 定义NSData对象,存储公钥的identifier(见列表 2-8 的第1、3、8步),该id在钥匙串中唯一。
NSMutableDictionary *queryPublicKey =
[[NSMutableDictionary alloc] init]; // 5 定义dictionary,用于从钥匙串中查找公钥。
[queryPublicKey setObject:(__bridge id)kSecClassKey forKey:(__bridge id)kSecClass];
[queryPublicKey setObject:publicTag forKey:(__bridge id)kSecAttrApplicationTag];
[queryPublicKey setObject:(__bridge id)kSecAttrKeyTypeRSA forKey:(__bridge id)kSecAttrKeyType];
[queryPublicKey setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecReturnRef];
// 6 设置dictionary的键-值属性。属性中指定,钥匙串条目类型为“密钥”,条目identifier为第4步中指定的字符串,密钥类型为RSA,函数调用结束返回查找到的条目引用。
status = SecItemCopyMatching
((__bridge CFDictionaryRef)queryPublicKey, (CFTypeRef *)&publicKey); // 7 调用SecItemCopyMatching函数进行查找。
// Allocate a buffer
cipherBufferSize = SecKeyGetBlockSize(publicKey);
cipherBuffer = malloc(cipherBufferSize);
// Error handling
if (cipherBufferSize < sizeof(dataToEncrypt)) {
// Ordinarily, you would split the data up into blocks
// equal to cipherBufferSize, with the last block being
// shorter. For simplicity, this example assumes that
// the data is short enough to fit.
printf("Could not decrypt. Packet too large.\n");
return NULL;
}
// Encrypt using the public.
status = SecKeyEncrypt( publicKey,
kSecPaddingPKCS1,
dataToEncrypt,
(size_t) dataLength,
cipherBuffer,
&cipherBufferSize
); // 8 加密数据, 返回结果用PKCS1格式对齐。
// Error handling
// Store or transmit the encrypted text
if (publicKey) CFRelease(publicKey);
NSData *encryptedData = [NSData dataWithBytes:cipherBuffer length:dataLength];
free(cipherBuffer);
return encryptedData;
}
下面的代码示例显示了如何解密数据。这个示例使用用来加密数据的公钥对应的私钥,并假设您已经在前面的示例中创建的密文。从钥匙串中获取私钥的技术跟前面的示例中获取公钥的技术相同。
Listing 2-10 Decryptingwith a private key
私钥解密
- (void)decryptWithPrivateKey: (NSData *)dataToDecrypt
{
OSStatus status = noErr;
size_t cipherBufferSize = [dataToDecrypt length];
uint8_t *cipherBuffer = (uint8_t *)[dataToDecrypt bytes];
size_t plainBufferSize;
uint8_t *plainBuffer;
SecKeyRef privateKey = NULL;
NSData * privateTag = [NSData dataWithBytes:privateKeyIdentifier
length:strlen((const char *)privateKeyIdentifier)];
NSMutableDictionary *queryPrivateKey = [[NSMutableDictionary alloc] init];
// 1 设置放钥匙串中私钥的字典
[queryPrivateKey setObject:(__bridge id)kSecClassKey forKey:(__bridge id)kSecClass];
[queryPrivateKey setObject:privateTag forKey:(__bridge id)kSecAttrApplicationTag];
[queryPrivateKey setObject:(__bridge id)kSecAttrKeyTypeRSA forKey:(__bridge id)kSecAttrKeyType];
[queryPrivateKey setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecReturnRef];
status = SecItemCopyMatching
((__bridge CFDictionaryRef)queryPrivateKey, (CFTypeRef *)&privateKey); // 2 从钥匙串中找到私钥
// Allocate the buffer
plainBufferSize = SecKeyGetBlockSize(privateKey);
plainBuffer = malloc(plainBufferSize);
if (plainBufferSize < cipherBufferSize) {
// Ordinarily, you would split the data up into blocks
// equal to plainBufferSize, with the last block being
// shorter. For simplicity, this example assumes that
// the data is short enough to fit.
printf("Could not decrypt. Packet too large.\n");
return;
}
// Error handling
status = SecKeyDecrypt( privateKey,
kSecPaddingPKCS1,
cipherBuffer,
cipherBufferSize,
plainBuffer,
&plainBufferSize
); // 3 解密数据
// Error handling
// Store or display the decrypted text
if(privateKey) CFRelease(privateKey); // 4 释放内存
}