iOS 证书、密钥及信任服务
——翻译自Apple Reference《Certificate,Key,and Trust Services Programming Guide》
本章描述并演示了如何使用证书、密钥和信任服务去导入一个indentity,评估证书是否可信,判断证书失效的原因,以及失效证书的恢复。
本章按如下顺序分别演示了:
导入一个 identity.
从导入的数据中获得证书.
获得用于证书评估的策略.
校验证书,根据指定策略评估证书是否可信.
测试证书中的可恢复错误.
判断证书是否过期.
改变评估条件,忽略过期证书.
重新评估证书.
“ 第2章,Certificate,Key,and Trust Services Concepts”,介绍了证书,密钥和信任服务的概念和术语。关于证书,密钥和信任服务的细节内容,请参考Certificate,Key,and Trust Services Reference.
如果你需要在iOS设备上使用加密过的identity(一个密钥及其关联的证书)进行客户端认证,例如——你可以把PKCS#12数据以受密码保护的文件的方式安全地传输到这个设备上。本节显示如何从PKCS#12数据中提取identity和trust objects(可信任对象),并评估其可信度。
列表 2-1 显示了用SecPKCS12Import函数从.p12文件中提取identity和可信任对象,以及评估其可信度。
列表 2-2 显示如何从identity中获取证书并显示证书信息。每个列表后都对代码进行了解释。
在编译这段代码时,请确认在Xcode工程中加入了Security.framework。
列表 2-1 从PKCS#12数据中提取identity和trust对象
#import <UIKit/UIKit.h>
#import <Security/Security.h>
#import <CoreFoundation/CoreFoundation.h>
NSString *thePath = [[NSBundle mainBundle]
pathForResource:@"MyIdentity" ofType:@"p12"];
NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];
CFDataRef inPKCS12Data = (CFDataRef)PKCS12Data; // 1
OSStatus status = noErr;
SecIdentityRef myIdentity;
SecTrustRef myTrust;
status = extractIdentityAndTrust(
inPKCS12Data,
&myIdentity,
&myTrust); // 2
if status != 0 ... //Do some error checking here
SecTrustResultType trustResult;
if (status == noErr) { // 3
status = SecTrustEvaluate(myTrust, &trustResult);
}
... // 4
if (trustResult == kSecTrustResultRecoverableTrustFailure) {
...;
}
OSStatus extractIdentityAndTrust(CFDataRef inPKCS12Data, // 5
SecIdentityRef *outIdentity,
SecTrustRef *outTrust)
{
OSStatus securityError = errSecSuccess;
CFStringRef password = CFSTR("Password");
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password };
CFDictionaryRef optionsDictionary = CFDictionaryCreate(
NULL, keys,
values, 1,
NULL, NULL); // 6
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import(inPKCS12Data,
optionsDictionary,
&items); // 7
//
if (securityError == 0) { // 8
CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
const void *tempIdentity = NULL;
tempIdentity = CFDictionaryGetValue (myIdentityAndTrust,
kSecImportItemIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
const void *tempTrust = NULL;
tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
}
if (optionsDictionary)
CFRelease(optionsDictionary); // 9
[PKCS12Data release];
在这段代码中 :
检索PKCS#12文件并获得数据。本例中,该文件位于application bundle中。你也可以通过网络方式把文件传输给你的应用程序。
调用该函数获得identity和trust对象(见步骤5)。
评估证书。这里的信任对象(trust object),包括信任策略和其他用于判断证书是否可信的信息,都已经含在了PKCS数据中。要单独评估一个证书是否可信,见列表 2-6。
处理信任结果。如果信任结果是kSecTrustResultInvalid,kSecTrustResultDeny,kSecTrustResultFatalTrustFailure,你无法进行处理。如果评估结果是kSecTrustResultRecoverableTrustFailure,你可以从信任失败中恢复。参见“从信任失败中恢复”。
第2步中调用的函数的具体实现。
构造包含了密码的dictionary,用于传递给SecPKCS12Import函数。注意这里使用的是core foundation中的CFDictionaryRef,与NSDictionary完全等价。列表 2-9 则是一个使用NSDictionary的例子。
从PKCS#12数据中提取证书、密钥和trust并将其放到数组中。
从数组中取出第1个dictionary,并从dictionary中取出identity和trust。SecPKCS12Import函数将PKCS数据中的每一个条目返回为一个dictionary。本例中,identity被提取到数组的第1个元素。
释放dictionary和PKCS12Data,他们不再被使用。
以下列表显示如何从identity中获取证书以及显示证书信息。编译本段代码前请确保在工程中导入了Security.framework。
列表 2-2 显示证书信息
// 从identity获取证书 .
SecCertificateRef myReturnedCertificate = NULL;
status = SecIdentityCopyCertificate (myReturnedIdentity,
&myReturnedCertificate); // 1
CFStringRef certSummary = SecCertificateCopySubjectSummary
(myReturnedCertificate); // 2
NSString* summaryString = [[NSString alloc]
initWithString:(NSString*)certSummary]; // 3
//Display the string
...
[summaryString release]; // 4
在这段代码中 :
从identity中提取证书。
从证书中获取summary摘要信息。
string 转换为NSString。
释放NSString。
当你在钥匙串中添加或查找一个条目时,你需要有一个持久化的引用。因为持久化引用能保证在程序从启动到能写入磁盘这段时间内,始终可用。当需要反复在钥匙串中查找条目时,使用持久化引用更加容易。以下代码演示如何获取一个 identity 的持久化引用。
列表 2-3 获取identity的持久化引用
CFDataRef persistentRefForIdentity(SecIdentityRef identity)
{
OSStatus status;
CFTypeRef identity_handle = 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;
}
下面演示使用持久化引用从钥匙串中检索identity对象。
列表 2-4 用持久化引用获取identity对象
SecIdentityRef identityForPersistentRef(CFDataRef persistent_ref)
{
CFTypeRef identity_ref = NULL;
const void *keys[] = { kSecReturnRef, kSecValuePersistentRef };
const void *values[] = { kCFBooleanTrue, persistent_ref };
CFDictionaryRef dict = CFDictionaryCreate(NULL, keys, values,
2, NULL, NULL);
SecItemCopyMatching(dict, &identity_ref);
if (dict)
CFRelease(dict);
return (SecIdentityRef)identity_ref;
}
以下代码演示如何使用证书名(在钥匙串中用证书名标识证书)查找证书。要用持久化引用在钥匙串中找到一个条目,参考列表 2-4。要用一个id字串查找一个条目,参考“数据加密和解密”。
列表 2-5 在钥匙串中查找证书
CFTypeRef certificateRef = NULL; // 1
const char *certLabelString = "Romeo Montegue";
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 (dict)
CFRelease(dict);
在这段代码中 :
定义变量,存储证书对象。
定义字符串,存储证书名。
定义 dictionary,存储证书查找条件。键-值序列中的键 kSecReturnRef表明,函数调用结束时应返回一个钥匙串条目的引用(当查找有结果时)。
在钥匙串中查找证书。
评估证书可信度之前,必需获取到一个证书对象的引用。你可以从一个identity中提取一个证书对象(列表 2-2),也可以从DER证书数据中创建证书对象(使用SecCertificateCreateWithData函数,见列表 2-6),或者从钥匙串中查找证书(列表 2-5)。
评估信任度的标准由信任策略(trust policy)指定。列表 3-2 显示如何获得用于评估的策略对象。在iOS中有两种策略可用:Basic X509和SSL(参考AppleX509TP 信任策略)。可以用SecPolicyCreateBasicX509或者SecPolicyCreateSSL函数获取策略对象。
下列代码显示了获取策略对象并用于评估证书是否可信。
列表 2-6 获取策略对象用于评估
NSString *thePath = [[NSBundle mainBundle]
pathForResource:@"Romeo Montegue" ofType:@"cer"];
NSData *certData = [[NSData alloc]
initWithContentsOfFile:thePath];
CFDataRef myCertData = (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); // 7
在这段代码中 :
查找证书文件并获取数据。本例中,该文件位于应用程序束。但你也可以从网络获取证书。如果证书存在于钥匙串中,参考“在钥匙串中查找证书”。
从证书数据中创建certificate引用。
创建用于评估证书的策略。
用证书和策略创建信任对象(trust)。如果存在中间证书或者锚证书,应把这些证书都包含在certificate数组中并传递给SecTrustCreateWithCertificates函数。这样会加快评估的速度。
评估一个信任对象。
处理信任结果(trust result)。如果信任结果是kSecTrustResultInvalid,kSecTrustResultDeny,kSecTrustResultFatalTrustFailure,你无法进行处理。如果信任结果是kSecTrustResultRecoverableTrustFailure,你可以恢复这个错误。参考“从信任失败中恢复”。
释放策略对象。
信任评估的结果有多个,这取决于:是否证书链中的所有证书都能找到并全都有效,以及用户对这些证书的信任设置是什么。信任结果怎么处理则由你的程序来决定。例如,如果信任结果是kSecTrustResultConfirm,你可以显示一个对话框,询问用户是否允许继续。
信任结果kSecTrustResultRecoverableTrustFailure的意思是:信任被否决,但可以通过改变设置获得不同结果。例如,如果证书签发过期,你可以改变评估日期以判断是否证书是有效的同时文档是已签名的。列表 3-4 演示如何改变评估日期。注意 CFDateCreate函数使用绝对时间(从2001年1月1日以来的秒数)。你可以用CFGregorianDateGetAbsoluteTime函数把日历时间转换为绝对时间。
列表 2-7 设置评估时间
SecTrustResultType trustResult;
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年内)导致前次评估失败,那么这次评估应该成功。
再次检查评估结果。如果仍不成功,则需要做更进一步的操作,比如提示用户安装中间证书,或则友好地告知用户证书校验失败。
证书,密钥和信任API包含了生产不对称密钥对并用于数据加密/解密的函数集。例如,你可能想加密数据,这些数据的备份不能被访问。或者,你可能想在你的iOS应用和桌面应用间共享公钥/私钥对,以通过网络发送加密数据。列表 2-8 显示如何产生可用于手机的公/私钥对。列表 2-9 显示如何用公钥加密数据,列表 2-10 显示如何用私钥解密数据。注意,这几个示例都使用了cocoa对象(如NSMutableDictionary),而本章其他示例使用了core framework对象(如CFMutableDictionaryRef),二者是等价的。
列表 2-8 生成密钥对
static const UInt8 publicKeyIdentifier[] = "com.apple.sample.publickey/0";
static const UInt8 privateKeyIdentifier[] = "com.apple.sample.privatekey/0";
// 1
- (void)generateKeyPairPlease
{
OSStatus status = noErr;
NSMutableDictionary *privateKeyAttr = [[NSMutableDictionary alloc] init];
NSMutableDictionary *publicKeyAttr = [[NSMutableDictionary alloc] init];
NSMutableDictionary *keyPairAttr = [[NSMutableDictionary alloc] init];
// 2
NSData * publicTag = [NSData dataWithBytes:publicKeyIdentifier
length:strlen((const char *)publicKeyIdentifier)];
NSData * privateTag = [NSData dataWithBytes:privateKeyIdentifier
length:strlen((const char *)privateKeyIdentifier)];
// 3
SecKeyRef publicKey = NULL;
SecKeyRef privateKey = NULL; // 4
[keyPairAttr setObject:(id)kSecAttrKeyTypeRSA
forKey:(id)kSecAttrKeyType]; // 5
[keyPairAttr setObject:[NSNumber numberWithInt:1024]
forKey:(id)kSecAttrKeySizeInBits]; // 6
[privateKeyAttr setObject:[NSNumber numberWithBool:YES]
forKey:(id)kSecAttrIsPermanent]; // 7
[privateKeyAttr setObject:privateTag
forKey:(id)kSecAttrApplicationTag]; // 8
[publicKeyAttr setObject:[NSNumber numberWithBool:YES]
forKey:(id)kSecAttrIsPermanent]; // 9
[publicKeyAttr setObject:publicTag
forKey:(id)kSecAttrApplicationTag]; // 10
[keyPairAttr setObject:privateKeyAttr
forKey:(id)kSecPrivateKeyAttrs]; // 11
[keyPairAttr setObject:publicKeyAttr
forKey:(id)kSecPublicKeyAttrs]; // 12
status = SecKeyGeneratePair((CFDictionaryRef)keyPairAttr,
&publicKey, &privateKey); // 13
// error handling...
if(privateKeyAttr) [privateKeyAttr release];
if(publicKeyAttr) [publicKeyAttr release];
if(keyPairAttr) [keyPairAttr release];
if(publicKey) CFRelease(publicKey);
if(privateKey) CFRelease(privateKey); // 14
}
在这段代码中 :
定义公/私钥id的字符串变量,以便后面使用。
定义dictionary,用于传递SecKeyGeneratePair函数中的第1个参数。
把第1步中定义的字符串转换为NSData对象。
为公/私钥对准备SecKeyRef对象。
设置密钥对的密钥类型为RSA。
设置密钥对的密钥长度为1024。
设置私钥的持久化属性(即是否存入钥匙串)为YES。
把1-3步中的identifier放到私钥的dictionary中。
设置公钥的持久化属性(即是否存入钥匙串)为YES。
把1-3步中的identifier放到公钥的dictionary中。
把私钥的属性集(dictionary)加到密钥对的属性集(dictionary)中。
把公钥的属性集(dictionary)加到密钥对的属性集(dictionary)中。
产生密钥对。
释放无用对象。
你可以把公钥发送给任何人,他们可以用它来加密数据。假设你安全地保存了私钥,则只有你能解密这些数据。以下代码演示如何用公钥加密数据。可以用从设备上产生的公钥(见后面代码),或者从证书中提取的公钥(发送给你的证书或者已经在钥匙串中的证书)。用SecTrustCopyPublicKey函数可以从证书中提取公钥。下面假设这个密钥由设备产生并已放到钥匙串中。
列表 2-9 用公钥加密数据
- (void)encryptWithPublicKey
{
OSStatus status = noErr;
size_t cipherBufferSize;
uint8_t *cipherBuffer; // 1
// [cipherBufferSize]
const uint8_t nonce[] = "the quick brown fox jumps
over the lazy dog/0"; // 2
SecKeyRef publicKey = NULL; // 3
NSData * publicTag = [NSData dataWithBytes:publicKeyIdentifier
length:strlen((const char *)publicKeyIdentifier)]; // 4
NSMutableDictionary *queryPublicKey =
[[NSMutableDictionary alloc] init]; // 5
[queryPublicKey setObject:(id)kSecClassKey forKey:(id)kSecClass];
[queryPublicKey setObject:publicTag forKey:(id)kSecAttrApplicationTag];
[queryPublicKey setObject:(id)kSecAttrKeyTypeRSA forKey:(id)kSecAttrKeyType];
[queryPublicKey setObject:[NSNumber numberWithBool:YES] forKey:(id)kSecReturnRef];
// 6
status = SecItemCopyMatching
((CFDictionaryRef)queryPublicKey, (CFTypeRef *)&publicKey); // 7
// Allocate a buffer
cipherBufferSize = cipherBufferSize(publicKey);
cipherBuffer = malloc(cipherBufferSize);
// Error handling
if (cipherBufferSize < sizeof(nonce)) {
// 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;
}
// Encrypt using the public.
status = SecKeyEncrypt( publicKey,
kSecPaddingPKCS1,
nonce,
(size_t) sizeof(nonce)/sizeof(nonce[0]),
cipherBuffer,
&cipherBufferSize
); // 8
// Error handling
// Store or transmit the encrypted text
if(publicKey) CFRelease(publicKey);
if(queryPublicKey) [queryPublicKey release]; // 9
free(cipherBuffer);
}
在这段代码中 :
定义缓存,用于放入加密文本。
指定要加密的文本。
定义SecKeyRef,用于公钥。
定义NSData对象,存储公钥的identifier(见列表 2-8 的第1、3、8步),该id在钥匙串中唯一。
定义dictionary,用于从钥匙串中查找公钥。
设置dictionary的键-值属性。属性中指定,钥匙串条目类型为“密钥”,条目identifier为第4步中指定的字符串,密钥类型为RSA,函数调用结束返回查找到的条目引用。
调用SecItemCopyMatching函数进行查找。
加密数据, 返回结果用PKCS1格式对齐。
释放不用的变量。
The following code sample shows how to decrypt data. This sample uses the private key corresponding to the public key used to encrypt the data, and assumes you already have the cipher text created in the preceding example. It gets the private key from the keychain using the same technique as used in the preceding example to get the public key.
下面代码演示如何解密。本例采用与加密数据的公钥对的私钥进行解密,并且密文为上面例子中的加密结果。从钥匙串中获取私钥,采用与上例相同的技术。
列表 2-10 用私钥解密
- (void)decryptWithPrivateKey
{
OSStatus status = noErr;
size_t plainBufferSize;;
uint8_t *plainBuffer;
SecKeyRef privateKey = NULL;
NSData * privateTag = [NSData dataWithBytes:privateKeyIdentifier
length:strlen((const char *)privateKeyIdentifier)];
NSMutableDictionary *queryPrivateKey = [[NSMutableDictionary alloc] init];
// Set the private key query dictionary.
[queryPrivateKey setObject:(id)kSecClassKey forKey:(id)kSecClass];
[queryPrivateKey setObject:privateTag forKey:(id)kSecAttrApplicationTag];
[queryPrivateKey setObject:(id)kSecAttrKeyTypeRSA forKey:(id)kSecAttrKeyType];
[queryPrivateKey setObject:[NSNumber numberWithBool:YES] forKey:(id)kSecReturnRef];
// 1
status = SecItemCopyMatching
((CFDictionaryRef)queryPrivateKey, (CFTypeRef *)&privateKey); // 2
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;
}
// Allocate the buffer
plainBufferSize = SecKeyGetBlockSize(privateKey);
plainBuffer = malloc(plainBufferSize)
// Error handling
status = SecKeyDecrypt( privateKey,
kSecPaddingPKCS1,
cipherBuffer,
cipherBufferSize,
plainBuffer,
&plainBufferSize
); // 3
// Error handling
// Store or display the decrypted text
if(publicKey) CFRelease(publicKey);
if(privateKey) CFRelease(privateKey);
if(queryPublicKey) [queryPublicKey release];
if(queryPrivateKey) [queryPrivateKey release]; // 4
}
在这段代码中 :
准备dictionary,用于从钥匙串查找私钥。
在钥匙串中找到私钥。
解密数据。
释放无用的变量。