iOS保证下载资源的可靠性(二)

前言

前文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地址

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Pulsar-V

原 前后端密钥分配验证

16060
来自专栏進无尽的文章

精析-苹果开发者证书的实现机制

      在iOS开发过程中,不可避免的要和证书打交道,真机调试、App上架、打包给测试去测试等都需要搞证书。在此过程中我们会遇到很多的问题,但是如果掌握了真...

20820
来自专栏程序员Gank

【译】使用RxJava代替EventBus类库

如今的Android社区,人人都在讨论RxJava以及为什么我们应该在项目中使用RxJava。当我们开始在Android项目中使用RxJava的时候,就已经意识...

12920
来自专栏码农二狗

不使用smtp直接发送邮件

24710
来自专栏张善友的专栏

Jexus服务器SSL二级证书安装指南

申请获得服务器证书有三张,一张服务器证书,二张中级CA证书。在Android微信中访问Https,如果服务器只有一张CA证书,就无法访问。 获取服务器证书中级...

25180
来自专栏owent

接入letsencrypt+全面启用HTTP/2

之前我的域名只有owent.net和www.owent.net买了SSL证书,现在有letsencrypt可以拿到免费的SSL签证,就稍微花了点时间把我的域名的...

10720
来自专栏Zachary46

解决Charles https抓包显示<unknown>

用mac电脑开发安卓的都应该知道青花瓷吧~(不知道的都是小菜鸡,邪恶.jpg)

3.2K30
来自专栏移动端周边技术扩展

防抓包(证书攻击)策略-iOS

2.1把证书机构签完的公钥证书放到工程里名称为"server.cer" 2.2设置AFSSLPinningMode

49430
来自专栏从零开始学 Web 前端

github提交代码contributions不显示小绿块

最近发现一个问题就是不管是提交新增的代码还是修改后提交的代码在github的contributions上都不显示贡献小绿块。

13230
来自专栏编程

Nginx双证书ECC/RSA配置

大家好,我是你们的老朋友Alex。今天教大家使用nginx配置证书,双证书! Nginx1.11.0版本后提供了ESA/ECC双证书的支持,以下是参考链接: h...

76080

扫码关注云+社区

领取腾讯云代金券