如何从DER/PEM文件获取SecKeyRef

23

我需要将我的iPhone应用程序与一个系统集成,他们要求使用给定的公钥对数据进行加密,有三个不同格式的文件,分别是.xml .der 和.pem,我已经做了一些研究并找到了一些关于从DER/PEM获取SecKeyRef的文章,但它们总是返回nil。以下是我的代码:

NSString *pkFilePath = [[NSBundle mainBundle] pathForResource:@"PKFile" ofType:@"der"];
NSData *pkData = [NSData dataWithContentsOfFile:pkFilePath]; 

SecCertificateRef   cert; 
cert = SecCertificateCreateWithData(NULL, (CFDataRef) pkData);
assert(cert != NULL);

OSStatus err;

    if (cert != NULL) {
        err = SecItemAdd(
                         (CFDictionaryRef) [NSDictionary dictionaryWithObjectsAndKeys:
                                            (id) kSecClassCertificate,  kSecClass, 
                                            (id) cert,                  kSecValueRef,
                                            nil
                                            ], 
                         NULL
                         );
        if ( (err == errSecSuccess) || (err == errSecDuplicateItem) ) {
            CFArrayRef certs = CFArrayCreate(kCFAllocatorDefault, (const void **) &cert, 1, NULL); 
            SecPolicyRef policy = SecPolicyCreateBasicX509();
            SecTrustRef trust;
            SecTrustCreateWithCertificates(certs, policy, &trust);
            SecTrustResultType trustResult;
            SecTrustEvaluate(trust, &trustResult);
            if (certs) {
                CFRelease(certs);
            }
            if (trust) {
                CFRelease(trust);
            }
            return SecTrustCopyPublicKey(trust);
        }
    }
return NULL;

在SecCertificateCreateWithData处出现问题,即使读取文件正常也始终返回nil。如果有人完成了这个,请帮帮我,谢谢!

编辑:该证书文件具有MD5签名。


2
我认为你会在这里找到答案:https://dev59.com/MXI-5IYBdhLWcg3w9thw - Chris Marshall
3个回答

57

我曾经和你一样为同一个问题苦苦挣扎,最终找到了解决方案。我的问题是我需要在iOS应用程序中使用外部私钥和公钥来加密/解密数据,但不想使用Keychain。

结果证明,您还需要签署证书以使iOS安全库能够读取密钥数据,当然文件必须是正确的格式。

该过程基本上如下:

假设您有一个PEM格式的私钥(带有-----BEGIN RSA PRIVATE KEY-----和-----END RSA PRIVATE KEY-----标记):rsaPrivate.pem

//Create a certificate signing request with the private key
openssl req -new -key rsaPrivate.pem -out rsaCertReq.csr

//Create a self-signed certificate with the private key and signing request
openssl x509 -req -days 3650 -in rsaCertReq.csr -signkey rsaPrivate.pem -out rsaCert.crt

//Convert the certificate to DER format: the certificate contains the public key
openssl x509 -outform der -in rsaCert.crt -out rsaCert.der

//Export the private key and certificate to p12 file
openssl pkcs12 -export -out rsaPrivate.p12 -inkey rsaPrivate.pem -in rsaCert.crt

现在你有两个与iOS安全框架兼容的文件:rsaCert.der(公钥)和rsaPrivate.p12(私钥)。下面的代码假定将公钥文件添加到您的捆绑包中,并读取该公钥:

- (SecKeyRef)getPublicKeyRef {

    NSString *resourcePath = [[NSBundle mainBundle] pathForResource:@"rsaCert" ofType:@"der"];
    NSData *certData = [NSData dataWithContentsOfFile:resourcePath];
    SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef)certData);
    SecKeyRef key = NULL;
    SecTrustRef trust = NULL;
    SecPolicyRef policy = NULL;

    if (cert != NULL) {
        policy = SecPolicyCreateBasicX509();
        if (policy) {
            if (SecTrustCreateWithCertificates((CFTypeRef)cert, policy, &trust) == noErr) {
                SecTrustResultType result;
                OSStatus res = SecTrustEvaluate(trust, &result);

                //Check the result of the trust evaluation rather than the result of the API invocation.
                if (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified) {
                    key = SecTrustCopyPublicKey(trust);
                }
            }
        }
    }
    if (policy) CFRelease(policy);
    if (trust) CFRelease(trust);
    if (cert) CFRelease(cert);
    return key;
}

使用以下代码读取私钥:

SecKeyRef getPrivateKeyRef() {
    NSString *resourcePath = [[NSBundle mainBundle] pathForResource:@"rsaPrivate" ofType:@"p12"];
    NSData *p12Data = [NSData dataWithContentsOfFile:resourcePath];

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

    SecKeyRef privateKeyRef = NULL;

    //change to the actual password you used here
    [options setObject:@"password_for_the_key" forKey:(id)kSecImportExportPassphrase];

    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);

    OSStatus securityError = SecPKCS12Import((CFDataRef) p12Data,
                                             (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;
        }
    }
    [options release];
    CFRelease(items);
    return privateKeyRef;
}

我找到的第一个解决方案成功地做到了这一点,而不需要使用钥匙串。太棒了。 - fishinear
现在这是不正确的。iOS 9.3现在支持直接引入PEM密钥。虽然可能需要使用钥匙串,但您可以立即将其从钥匙串中删除。 - CommaToast
@CommaToast - 自9.3以来,该如何实现这个功能? - cober
我无法成功导出私钥。SecPKCS12Import 没有错误,但是 SecIdentityCopyPrivateKey 会在 CFDataGetLength 中崩溃。经过数小时的调试后,我决定使用相同的命令行(bash 历史记录)重新生成 .p12 文件,然后一切都正常了。未更改私钥和证书文件。真希望我保留了旧的 .p12 文件以证明自己的理智。 - FyKnight

17

iOS 10开始,可以直接导入PEM私钥而无需将它们转换为PKCS#12(这是与加密有关的通用容器格式),因此也不需要在命令行上使用OpenSSL或静态地将应用程序链接到它。在macOS上,甚至自10.7以来就可以使用与此处提到的不同函数(但到目前为止,在iOS上不存在)。如下所述的确切方式也适用于macOS 10.12及更高版本。

要导入证书,只需去掉

-----BEGIN CERTIFICATE-----

-----END CERTIFICATE-----

将这些行拼接在一起,然后对剩下的数据运行base64解码,得到的结果是一个标准DER格式的证书,可以直接提供给SecCertificateCreateWithData()来获取SecCertificateRef。这一直都有效,即使在iOS 10之前也是如此。

要导入私钥,可能需要额外的一些工作。如果私钥被包装在中...

-----BEGIN RSA PRIVATE KEY-----
如果是这种情况,那么很简单。再次去掉第一行和最后一行,对剩余数据进行 base64 解码,结果就是 PKCS#1 格式的 RSA 密钥。这种格式只能容纳 RSA 密钥,直接可读,只需要将解码后的数据传递给 SecKeyCreateWithData() 方法获取一个 SecKeyRef。属性字典需要以下键值对:
  • kSecAttrKeyType: kSecAttrKeyTypeRSA
  • kSecAttrKeyClass: kSecAttrKeyClassPrivate
  • kSecAttrKeySizeInBits: CFNumberRef 标识密钥位数(例如 1024,2048 等)。如果不知道,可以从 ASN.1 数据中读取此信息(这超出了本答案的范围,但下面提供了一些有用的链接,介绍如何解析该格式)。该值可能是可选项!在我的测试中,实际上不需要设置此值;如果缺少,则 API 会自动确定该值,并在以后始终正确地设置。

如果私钥被 -----BEGIN PRIVATE KEY----- 包装,那么 base64 编码的数据不是 PKCS#1 格式而是 PKCS#8 格式,但这只是一个更通用的容器,也可以存储非 RSA 密钥,但对于 RSA 密钥,该容器的内部数据等于 PKCS#1,因此可以说对于 RSA 密钥,PKCS#8 相当于带有额外标题的 PKCS#1,只需去掉那个额外头即可。只需去掉 base64 解码数据的前 26 个字节,就可以得到 PKCS#1。是的,就这么简单。

要了解有关 PEM 编码中 PKCS#x 格式的详细信息,请参阅此网站。要了解有关 ASN.1 格式的详细信息,请查看这个网站。如果您需要一个简单而强大且交互性的在线 ASN.1 解析器,可以尝试一下这个网站,它可以直接读取 PEM 数据以及 ASN.1 中的 base64 和十六进制转储。

非常重要:将上述创建的私钥添加到密钥链时,请注意这样一个私钥不包含公钥哈希,但公钥哈希对于密钥链 API 形成身份(SecIdentityRef)非常重要,使用公钥哈希是 API 找到属于导入的证书的正确私钥的方式(SecIdentityRef 只是一个私钥的 SecKeyRef 和证书的 SecCertificateRef 组合对象,并且它们之间的绑定是通过公钥哈希实现的)。因此,在计划将私钥添加到密钥链时,请务必手动设置公钥哈希,否则您将永远无法获取该身份,而没有身份,您不能使用密钥链 API 进行签名或解密等任务。公钥

OSStatus error = SecItemAdd(
    (__bridge CFDictionaryRef)@{
        (__bridge NSString *)kSecClass: 
            (__bridge NSString *)kSecClassKey,
        (__bridge NSString *)kSecAttrApplicationLabel: 
             hashOfPublicKey, // hashOfPublicKey is NSData *
#if TARGET_OS_IPHONE
        (__bridge NSString *)kSecValueRef: 
            (__bridge id)privateKeyToAdd, // privateKeyToAdd is SecKeyRef
#else
        (__bridge NSString *)kSecUseItemList: 
              @[(__bridge id)privateKeyToAdd], // privateKeyToAdd is SecKeyRef
              // @[ ... ] wraps it into a NSArray object,
              // as kSecUseItemList expects an array of items
#endif
     },
     &outReference // Can also be NULL,
                   // otherwise reference to added keychain entry
                   // that must be released with CFRelease()
);

除了剥离 -----BEGIN 和 -----END 行之外,在进行base64解码之前,还应该剥离换行符。 - mbonness
@mbonness 每个符合标准的base64解码器都必须能够忽略换行符,因为这是标准要求的,否则它甚至无法解码自己的编码输出,而这种输出通常包含换行符。当使用initWithBase64EncodedString:options:时,只需使用选项NSDataBase64DecodingIgnoreUnknownCharacters即可。大多数第三方base64解码器默认情况下都会忽略换行符,并且只能通过选项关闭。 - Mecki
你说只需要去掉“BEGIN CERTIFICATE”和“END”的部分就可以了……但我怎么才能得到那个证书呢? - Mike
好的,我想我的下一个问题是如何获取/创建证书...我被“导入证书”一词所困惑了。 - Mike
@Mike 问题是如何从PEM文件获取SecKeyRef。答案是:你将PEM文件加载到一个字符串中,然后从该字符串中删除开始/结束标记,通过Base64解码其余内容,使用适当的属性将数据传递给SecKeyCreateWithData(),然后你就有了存储在PEM文件中的密钥的SekKeyRef - Mecki
显示剩余3条评论

9

在阅读了这篇文章的帮助下,我花费几个小时的时间进行在线研究,最终成功地使其完美运行。以下是最新版本的工作Swift代码和笔记。我希望它能对某些人有所帮助!

  1. Received a certificate in the base64 encoded string sandwiched between header and tail like this (PEM format):

    -----BEGIN CERTIFICATE-----
    -----END CERTIFICATE-----
    
  2. strip out the header and the tail, such as

    // remove the header string  
    let offset = ("-----BEGIN CERTIFICATE-----").characters.count  
    let index = certStr.index(cerStr.startIndex, offsetBy: offset+1)  
    cerStr = cerStr.substring(from: index)  
    
    // remove the tail string 
    let tailWord = "-----END CERTIFICATE-----"   
    if let lowerBound = cerStr.range(of: tailWord)?.lowerBound {  
    cerStr = cerStr.substring(to: lowerBound)  
    }
    
  3. decode base64 string to NSData:

    let data = NSData(base64Encoded: cerStr, 
       options:NSData.Base64DecodingOptions.ignoreUnknownCharacters)!  
    
  4. Convert it from NSdata format to SecCertificate:

    let cert = SecCertificateCreateWithData(kCFAllocatorDefault, data)
    
  5. Now, this cert can be used to compare with the certificate received from the urlSession trust:

    certificateFromUrl = SecTrustGetCertificateAtIndex(...)
    if cert == certificate {
    }
    

1
如果PEM是多个密钥的链,即多个“BEGIN/END”,该怎么办? - vikzilla
NSData.Base64DecodingOptions.ignoreUnknownCharacters 似乎已经解决了我的转换问题。 - Guven
你如何获得那个证书? - Mike
不错。还要记住,您可能会在URI格式中使用base64,需要将“-”替换为“+”,将“_”替换为“/”。在iOS上,您还需要使用“=”添加填充。字符串必须能够被4整除,不能有小数。否则,您将得到错误的数据对象。 - Juraj Antas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接