iOS 生成 SecKeyRef 的正规方式

前言

针对 macOS 的开发,多年以前苹果就弃用了 OpenSSL,转而推荐自有框架 Security 和 CommonCrypto。当然你仍然可以使用 OpenSSL,比如说在 iOS 上使用开源库 OpenSSL for iPhone

苹果有一套自己的方式来生成各种密钥(对称加密、非对称加密),你可以查看苹果的 sample code CryptoExercise,来了解如何在苹果自有平台(macOS、iOS、 tvOS 等等)上使用这一套机制。

OpenSSL 是被广泛使用的生成公私钥对以及各类证书等文件的方式,比如生成 PEM 或者 DER 后缀的文件(前者是 Base64 编码,或者则是 DER 编码的内容,如何包含的只是公钥或者私钥的话,本质上就没有区别)。但是在 iOS 并没有原生支持读取只包含公钥或者私钥的方法(iOS 10 之后可以使用 SecKeyCreateWithData 来生成) 。

在 iOS 上, SecKeyRef 对象是一个密码学角度的抽象的密钥对象(也就是说它可以代表一个公钥、私钥或者某种对称加密的密钥)。所以如何生成这样一个对象就显得格外重要,因为无论是加解密还是签名,都会需要这个对象

原生生成公私钥对象的一种通用方式 (仅限 iOS 10 及以上)

苹果从 iOS 10 开始支持直接从公私钥数据来生成 SecKeyRef。步骤如下:

  1. 对于 PEM 编码的数据,需要先将多余的信息给剔除,主要是头尾两行 (begin 和 end )以及去掉换行。
  2. 构造一个 attribute 属性字典,指定密钥算法(比如 RSA),密钥格式(公钥还是私钥),还有密钥大小
  3. 调用 SecKeyCreateWithData,返回一个 SecKeyRef

下面是具体代码:

SecKeyRef getPrivateKeyFromPem() {
    // 下面是对于 PEM 格式的密钥文件的密钥多余信息的处理,通常 DER 不需要这一步
    NSString *key = @"PEM 格式的密钥文件";
    NSRange spos;
    NSRange epos;
    spos = [key rangeOfString:@"-----BEGIN RSA PRIVATE KEY-----"];
    if(spos.length > 0){
        epos = [key rangeOfString:@"-----END RSA PRIVATE KEY-----"];
    }else{
        spos = [key rangeOfString:@"-----BEGIN PRIVATE KEY-----"];
        epos = [key rangeOfString:@"-----END PRIVATE KEY-----"];
    }
    if(spos.location != NSNotFound && epos.location != NSNotFound){
        NSUInteger s = spos.location + spos.length;
        NSUInteger e = epos.location;
        NSRange range = NSMakeRange(s, e-s);
        key = [key substringWithRange:range];
    }
    key = [key stringByReplacingOccurrencesOfString:@"\r" withString:@""];
    key = [key stringByReplacingOccurrencesOfString:@"\n" withString:@""];
    key = [key stringByReplacingOccurrencesOfString:@"\t" withString:@""];
    key = [key stringByReplacingOccurrencesOfString:@" "  withString:@""];
    
    // This will be base64 encoded, decode it.
    NSData *data = base64_decode(key);
    if(!data){
        return nil;
    }
    
    // 设置属性字典
    NSMutableDictionary *options = [NSMutableDictionary dictionary];
    options[(__bridge id)kSecAttrKeyType] = (__bridge id) kSecAttrKeyTypeRSA;
    options[(__bridge id)kSecAttrKeyClass] = (__bridge id) kSecAttrKeyClassPrivate;
    NSNumber *size = @2048;
    options[(__bridge id)kSecAttrKeySizeInBits] = size;
    NSError *error = nil;
    CFErrorRef ee = (__bridge CFErrorRef)error;
    
    // 调用接口获取密钥对象
    SecKeyRef ret = SecKeyCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)options, &ee);
    if (error) {
        return nil;
    }
    return ret;
}

原生生成公私钥对象的一种通用方式 (iOS 9 及以前)

针对 iOS 10 以前的版本,想要获取私钥的正规途径是通过 P12(亦即 PKCS #12) 文件获取(P12 是同时包含公私钥的文件,同时需要一个对称密码来使用 p12 文件),步骤也很简单:

  1. 读取 p12 文件,当然我不推荐你直接将 p12 文件放在 app bundle 中。你可以硬编码在代码中,会安全一丢丢。
  2. 设置参数字典,主要是设置你在导出 p12 文件时候设置的密码。
  3. 调用 SecPKCS12Import 导出 p12 文件包含的 item 数组
  4. 获取 item 数组第一个元素的字典,其中 kSecImportItemIdentity 键对应的是值也就是 SecIdentityRef 对象
  5. 从 SecIdentityRef 中拷出私钥对象

如果要拷出公钥,稍微有点不一样:

  1. 上面步骤中获得 item 数组第一个元素的字典,其中 kSecImportItemTrust 键对应的是值也就是一个 Trust 对象。
  2. 调用 SecTrustCopyPublicKey 获取公钥对象

下面是代码解释:

NSString *resourcePath = [[NSBundle mainBundle] pathForResource:@"rsaPrivate" ofType:@"p12"];
NSData *p12Data = [NSData dataWithContentsOfFile:resourcePath];

NSMutableDictionary * options = [[NSMutableDictionary alloc] init];

SecKeyRef privateKeyRef = NULL;
id publicKey = NULL;

// 改成你设置的密码
[options setObject:@"" forKey:(id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = SecPKCS12Import((CFDataRef) p12Data, (CFDictionaryRef)options, &items);

if (securityError == noErr && CFArrayGetCount(items) > 0) {
    // 获取一个 Identity 对象
    CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
    
    // 获取私钥
    SecIdentityRef identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
    securityError = SecIdentityCopyPrivateKey(identityApp, &privateKeyRef);
    if (securityError != noErr) {
        privateKeyRef = NULL;
    }
    
    // 获取一个 Trust 对象
    SecTrustRef trustRef = (SecTrustRef)CFDictionaryGetValue(identityDict, kSecImportItemTrust);
    // 获取公钥
    publicKey = (__bridge_transfer id)SecTrustCopyPublicKey(trustRef);
}
CFRelease(items);

从证书文件读取公钥对象

从证书文件读取公钥对象步骤如下:

  1. 读取证书文件生成一个 Certificate 对象(SecCertificateRef 类型)
  2. 从 Certificate 对象获取一个 Trust 对象 (SecTrustRef 类型)
  3. 从 Trust 对象拷贝出公钥 (这一步可以先根据 Trust 对象来判断证书是否可信)

代码解释如下:

id publicKey = nil;
SecCertificateRef certificate;
SecCertificateRef certificates[1];
CFArrayRef tempCertificates = nil;
SecPolicyRef policy = nil;
SecTrustRef trust = nil;
SecTrustResultType result;

certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateDate);
if (certificate) {
    certificates[0] = certificate;
    tempCertificates = CFArrayCreate(NULL, (const void **)certificates, 1, NULL);
    policy = SecPolicyCreateBasicX509();
    SecTrustCreateWithCertificates(tempCertificates, policy, &trust);
    SecTrustEvaluate(trust, &result);
    // 获得公钥对象
    publicKey = (__bridge_transfer id)SecTrustCopyPublicKey(trust);
}

if (trust) {
    CFRelease(trust);
}

if (policy) {
    CFRelease(policy);
}

if (tempCertificates) {
    CFRelease(tempCertificates);
}

if (certificate) {
    CFRelease(certificate);
}

如何从密钥文件生成 P12 和证书等

你可以参考这个 SO How can I get SecKeyRef from DER/PEM file

开源库

Objective-C-RSA 这个开源库源码解释了如何自己处理 PEM 格式密钥文件的头,但是由于解析力不够强,经常会返回一个空的密钥对象。所以必要时候可以参考一下。但是不太推荐。笔者对 ASN.1 等概念不太熟悉,这里不过多讨论了。

使用 OpenSSL

使用 OpenSSL 无法生成 SecKeyRef 密钥对象,但是 OpenSSL 提供了完整的密码学各类操作支持(加密,加签,解密,验签等),所以你完全可以不需要苹果的 Security 框架。你可以参考 支付宝的 Demo,了解如何使用该库。开源代码地址是 OpenSSL for iPhone

iOS 上关于加密等密码学操作的建议

苹果的官方文档 苹果 Security 框架文档 完整的描述了如何在苹果自有平台使用 Security 框架。你可以参考它。

主要是理解几个对象:(文档地址 Certificate, Key, and Trust Services

  1. certificate 对象
  2. identity 对象
  3. trust 对象
  4. key 对象
  5. policy 对象

引用

  1. OpenSSL for iPhone
  2. 苹果 sample code CryptoExercise
  3. Swift 对称密码使用法
  4. Swift 非对称密码使用法
  5. iOS 上的公钥 VS OpenSSL 上的公钥对比
  6. 密钥文件等的区别和转换
  7. iOS 上的 SHA256 with RSA VS JAVA 平台
  8. P12、证书文件的生成
  9. 苹果 Security 框架文档
  10. Objective-C-RSA 开源库
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容