【问题标题】:How can I get SecKeyRef from DER/PEM file如何从 DER/PEM 文件中获取 SecKeyRef
【发布时间】:2012-05-14 08:41:23
【问题描述】:

我需要将我的 iPhone 应用程序与系统集成,他们需要通过给定的公钥加密数据,有 3 种不同格式的 .xml .der 和 .pem 的 3 个文件,我研究并找到了一些关于从 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 签名。

【问题讨论】:

标签: iphone rsa public-key pem der


【解决方案1】:

我为同样的问题苦苦挣扎,终于找到了解决方案。我的问题是我需要同时使用外部私钥和公钥来加密/解密 iOS 应用程序中的数据,并且不想使用钥匙串。 事实证明,您还需要 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;
}

【讨论】:

  • 我发现的第一个解决方案是在不使用钥匙串的情况下设法做到这一点。很棒。
  • 现在不正确了。 iOS 9.3 现在支持直接引入 PEM 密钥。虽然它可能需要使用钥匙串,但您可以立即将其从钥匙串中删除。
  • @CommaToast - 自 9.3 以来如何做到这一点?
  • 我无法让私钥导出工作。 SecPKCS12Import 工作正常,没有错误,但 SecIdentityCopyPrivateKey 会崩溃(在 CFDataGetLength 中)。经过数小时的调试后,我一时兴起使用 exact same 命令行(bash 历史记录)重新生成了 .p12 文件,然后一切正常。未触及的私钥和证书文件。真希望我保留旧的 .p12 文件以证明我的理智。
【解决方案2】:

从 iOS 10 开始,实际上可以导入 PEM 私钥而不将它们转换为 PKCS#12(这是一种非常通用的容器格式,适用于与密码学相关的所有内容),因此也w/o 在命令行上使用 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() 即可获得SecKeyRefattributes 字典只需要以下键/值对:

  • kSecAttrKeyType: kSecAttrKeyTypeRSA
  • kSecAttrKeyClass: kSecAttrKeyClassPrivate
  • kSecAttrKeySizeInBits: CFNumberRef with then 密钥中的位数(例如 1024、2048 等)如果不知道,实际上可以从原始密钥数据中读取此信息,即 ASN.1 数据(有点超出了这个答案的范围,但我将在下面提供一些关于如何解析该格式的有用链接)。 这个值可能是可选的!在我的测试中,实际上没有必要设置这个值;如果不存在,API 会自行确定该值,并在以后始终正确设置。

如果私钥被-----BEGIN PRIVATE KEY-----包裹,那么base64编码的数据不是PKCS#1格式,而是PKCS#8格式,但是,这只是一个更通用的容器,它也可以保存非 RSA 密钥,但对于 RSA 密钥,该容器的内部数据等于 PKCS#1,因此可以说对于 RSA 密钥 PKCS#8PKCS#1 带有一个额外的标头,您需要做的就是剥离该额外的标头。只需剥离 base64 解码数据的前 26 个字节,您就会再次拥有 PKCS#1。是的,就是这么简单。

要详细了解 PEM 编码中的 PKCS#x 格式,have a look at this site。要了解有关 ASN.1 格式的更多信息,请here's a good site for that。如果您需要一个简单但功能强大且交互式的在线 ASN.1 解析器来处理不同的格式,可以直接读取 PEM 数据以及 base64 和 hexdump 中的 ASN.1,try this site

非常重要:当您向上面创建的钥匙串添加私钥时,请注意这样的私钥不包含公钥哈希,但公钥哈希是对于他们形成身份的钥匙串 API (SecIdentityRef) 很重要,因为使用公钥哈希是 API 如何找到属于导入证书的正确私钥的方式(SecIdentityRef 只是私人的SecKeyRef密钥和证书的SecCertificateRef 形成一个组合对象,它是将它们绑定在一起的公钥哈希)。因此,当您计划将私钥添加到钥匙串时,请务必手动设置公钥哈希,否则您将永远无法获得它的身份,否则您将无法使用钥匙串 API 执行签名或解密等任务数据。公钥哈希必须存储在一个名为kSecAttrApplicationLabel 的属性中(我知道这个名字很愚蠢,但它真的不是标签,用户也看不到任何东西,请查看文档)。例如:

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 每个符合标准的 base64 解码器都必须能够按照标准的要求忽略换行符,否则它甚至无法解码自己的编码输出,该输出经常包含换行符。使用initWithBase64EncodedString:options: 时,只需使用选项NSDataBase64DecodingIgnoreUnknownCharacters。大多数第 3 方 base64 解码器默认忽略换行符,并且只能通过选项关闭。
  • 你说它足以剥离 BEGIN CERTIFICATE 和 END... 但是我如何获得该证书来做到这一点?
  • @Mike 您将证书作为字符串加载,然后删除这两个子字符串。剩下的是 base64 编码的证书数据。 PEM 只是人类可读的文本,PEM 文件可以作为字符串加载。
  • 好的,所以我想我的下一个问题是如何获取/创建证书...我对“导入导入证书”感到困惑
【解决方案3】:

在这篇文章的帮助下,经过数小时的在线研究,我终于让它完美运行。这是最新版本的工作 Swift 代码的注释。我希望它可以帮助别人!

  1. 收到一个包含在头部和尾部之间的 base64 编码字符串的证书,如下所示(PEM 格式):

    -----BEGIN CERTIFICATE-----
    -----END CERTIFICATE-----
    
  2. 去掉header和tail,比如

    // 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. 将base64字符串解码为NSData:

    let data = NSData(base64Encoded: cerStr, 
       options:NSData.Base64DecodingOptions.ignoreUnknownCharacters)!  
    
  4. 将其从 NSdata 格式转换为 SecCertificate:

    let cert = SecCertificateCreateWithData(kCFAllocatorDefault, data)
    
  5. 现在,此证书可用于与从 urlSession 信任收到的证书进行比较:

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

【讨论】:

  • 如果 PEM 是多个键的链,即多个“BEGIN / END”怎么办?
  • NSData.Base64DecodingOptions.ignoreUnknownCharacters 似乎解决了我的转换问题。
  • 您是如何收到该证书的?
  • 不错的一个。另外,请记住,您可能有 URI 格式的 base64,您需要将“-”替换为“+”,将“_”替换为“/”。在 iOS 上,您还需要使用 '=' 添加填充。字符串必须能被 4 整除而没有分数。否则你会得到错误的数据对象。
猜你喜欢
  • 2010-12-08
  • 1970-01-01
  • 2022-01-10
  • 2021-12-30
  • 1970-01-01
  • 2020-03-21
  • 2010-11-02
相关资源
最近更新 更多