前言
前文iOS如何保证下载资源的可靠性介绍了基于RSA的下载资源验证方案,这次详细介绍开发过程中的问题。
iOS接入步骤
- 后台上传资源文件,配置平台对文件进行hash并用私钥进行签名得到签名串signature;
- 把文件和signature打包成zip包,下发到客户端;
- 客户端解压zip,得到文件和签名串signature,对文件进行hash,加载本地公钥,把hash值、signature、公钥传给Security.framework;
- 用Security.framework提供的
SecKeyRawVerify
方法对hash值、signature、公钥进行验证,如果通过则表示文件未修改。
1、zip解压
iOS平台上可以使用MiniZipArchive进行解压。
- (BOOL)unzipFile:(NSString *)file toFilePath:(NSString *)unZipFilePath overWrite:(BOOL)overWrite
{
MiniZipArchive *za = [[MiniZipArchive alloc] init];
BOOL success = NO;
if ([za UnzipOpenFile:file]) {
success = [za UnzipFileTo:unZipFilePath overWrite:overWrite];
[za UnzipCloseFile];
}
return success;
}
2、公钥和私钥的加载
.der格式和.pem格式:.der格式表示二进制编码,.pem格式表示Base64编码。
iOS的公钥需要用.der格式,私钥需要用.p12格式,这个可以用openssl的指令来转换。(指令见末尾)
加载的时候先用NSData加载密钥,再用下面的:
getPrivateKeyRefWithContentsOfFile: password:
方法加载密钥;
getPublicKeyRefrenceFromeData:
方法加载公钥;
//获取私钥
- (SecKeyRef)getPrivateKeyRefWithContentsOfFile:(NSData *)p12Data password:(NSString*)password {
if (!p12Data) {
return nil;
}
SecKeyRef privateKeyRef = NULL;
NSMutableDictionary * options = [[NSMutableDictionary alloc] init];
[options setObject: password forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = SecPKCS12Import((__bridge CFDataRef) p12Data, (__bridge CFDictionaryRef)options, &items);
if (securityError == noErr && CFArrayGetCount(items) > 0) {
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
SecIdentityRef identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
securityError = SecIdentityCopyPrivateKey(identityApp, &privateKeyRef);
if (securityError != noErr) {
privateKeyRef = NULL;
}
}
CFRelease(items);
return privateKeyRef;
}
- (SecKeyRef)getPublicKeyRefrenceFromeData:(NSData *)certData {
SecKeyRef publicKeyRef = NULL;
CFDataRef myCertData = (__bridge CFDataRef)certData;
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef)myCertData);
if (cert == nil) {
NSLog(@"Can not read certificate ");
return nil;
}
SecPolicyRef policy = SecPolicyCreateBasicX509();
SecCertificateRef certArray[1] = {cert};
CFArrayRef myCerts = CFArrayCreate(NULL, (void *)(void *)certArray, 1, NULL);
SecTrustRef trust;
OSStatus status = SecTrustCreateWithCertificates(myCerts, policy, &trust);
if (status != noErr) {
NSLog(@"SecTrustCreateWithCertificates fail. Error Code: %d", (int)status);
CFRelease(cert);
CFRelease(policy);
CFRelease(myCerts);
return nil;
}
SecTrustResultType trustResult;
status = SecTrustEvaluate(trust, &trustResult);
if (status != noErr) {
NSLog(@"SecTrustEvaluate fail. Error Code: %d", (int)status);
CFRelease(cert);
CFRelease(policy);
CFRelease(trust);
CFRelease(myCerts);
return nil;
}
publicKeyRef = SecTrustCopyPublicKey(trust);
CFRelease(cert);
CFRelease(policy);
CFRelease(trust);
CFRelease(myCerts);
return publicKeyRef;
}
3、私钥签名和公钥验证
加载完公钥和私钥之后,用私钥可以对原始数据进行签名,详见PKCSSignBytesSHA256withRSA
方法,返回的是签名串;
在用zip解压出来的签名串进行验证的时候,需要用本地的公钥、原始数据和签名串进行验签,详见PKCSVerifyBytesSHA256withRSA
方法;
注意的是,因为选择的算法是kSecPaddingPKCS1SHA256
,需要对原始数据进行一次SHA256的hash。(kSecPaddingPKCS1SHA256
只能用于SecKeyRawSign/SecKeyRawVerify
)
BOOL PKCSVerifyBytesSHA256withRSA(NSData* plainData, NSData* signature, SecKeyRef publicKey)
{
if (!plainData || !signature) { // 保护
return NO;
}
size_t signedHashBytesSize = SecKeyGetBlockSize(publicKey);
const void* signedHashBytes = [signature bytes];
size_t hashBytesSize = CC_SHA256_DIGEST_LENGTH;
uint8_t* hashBytes = malloc(hashBytesSize);
if (!CC_SHA256([plainData bytes], (CC_LONG)[plainData length], hashBytes)) {
return NO;
}
OSStatus status = SecKeyRawVerify(publicKey,
kSecPaddingPKCS1SHA256,
hashBytes,
hashBytesSize,
signedHashBytes,
signedHashBytesSize);
return status == errSecSuccess;
}
NSData* PKCSSignBytesSHA256withRSA(NSData* plainData, SecKeyRef privateKey)
{
size_t signedHashBytesSize = SecKeyGetBlockSize(privateKey);
uint8_t* signedHashBytes = malloc(signedHashBytesSize);
memset(signedHashBytes, 0x0, signedHashBytesSize);
size_t hashBytesSize = CC_SHA256_DIGEST_LENGTH;
uint8_t* hashBytes = malloc(hashBytesSize);
if (!CC_SHA256([plainData bytes], (CC_LONG)[plainData length], hashBytes)) {
return nil;
}
SecKeyRawSign(privateKey,
kSecPaddingPKCS1SHA256,
hashBytes,
hashBytesSize,
signedHashBytes,
&signedHashBytesSize);
NSData* signedHash = [NSData dataWithBytes:signedHashBytes
length:(NSUInteger)signedHashBytesSize];
if (hashBytes)
free(hashBytes);
if (signedHashBytes)
free(signedHashBytes);
return signedHash;
}
4、签名串的保存
签名串可以使用setxattrf
写入文件的扩展属性,保证签名串和资源的一一对应。
-(BOOL)setExtendValueWithPath:(NSString *)path key:(NSString *)key value:(NSData *)value {
ssize_t writelen = setxattr([path fileSystemRepresentation],
[key UTF8String],
[value bytes],
[value length],
0,
0);
return writelen == 0;
}
比较奇怪的是,比较写入扩展属性之后的文件大小,并没有发生较大变化。在特意查询文档之后,发现下面一句话:
Space consumed for extended attributes is counted towards the disk quotasof the file owner and file group
原来扩展属性并不是写入文件,而是由文件系统来保存。
遇到的问题
1、验证失败,SecKeyRawVerify返回-9809
经常遇到的问题是,配置平台的签名在iOS客户端验证不通过,可以按照下面的流程检测:
- 首先是确保两端的公钥和私钥是一对;
- 配置平台签名完之后,用iOS客户端的公钥在本地验证;
- 确认两边使用的签名算法设置参数一致;
- iOS客户端用配置平台的私钥进行签名,再用公钥进行验证;
- 对比配置平台的签名串和iOS的签名串;
openssl的验证命令
openssl dgst -sign private_key.pem -sha256 -out sign source
openssl dgst -verify rsa_public_key.pem -sha256 -signature sign source
如果验证通过会有文字提示:Verified OK
2、生成证书失败,openssl X509: 出现 Expecting: TRUSTED CERTIFICATE
的错误
参考这些公钥和密钥的openssl生成命令
openssl genrsa -out private_key.pem 1024
openssl req -new -key private_key.pem -out rsaCertReq.csr
openssl x509 -req -days 3650 -in rsaCertReq.csr -signkey private_key.pem -out rsaCert.crt
openssl x509 -outform der -in rsaCert.crt -out public_key.der
openssl pkcs12 -export -out private_key.p12 -inkey private_key.pem -in rsaCert.crt
参考自GithubGist
附录
Signing and Verifying on iOS using RSA
xattr manpages
demo地址
招聘信息
岗位职责:负责iOS平台的开发工作,与产品、测试及其他团队协作,分析产品需求,制定技术方案,攻克技术难题; 新技术研究和引入。
岗位要求:
本科以上学历,计算机相关专业;
2年以上相关工作经验; 英语良好,能阅读英文资料;
责任心强,积极向上、乐于学习新技术;
要求精通Objective-C,熟悉iOS平台并有良好的软件开发经验,基础扎实; 熟悉https及流媒体上传下载协议,精通TCP/IP协议;
熟悉音频/视频的编解码;
有音视频直播相关经验者优先;
良好的编码风格,以及足够的调试技术;
岗位信息:
地点深圳,待遇从优,国内一流互联网公司。
能看到这里的朋友,相信也是爱学习的iOS开发者。
最近组内有岗位空缺,正在求职或者有换工作意向的朋友千万别错过。
我用我的和Github做担保,这个职位非常棒。
如果有兴趣共事,简历发往loying#foxmail#com
(防垃圾软件用#)。