【问题标题】:How to get a SecIdentityRef from a SecCertificateRef and a SecKeyRef如何从 SecCertificateRef 和 SecKeyRef 获取 SecIdentityRef
【发布时间】:2017-09-01 10:05:18
【问题描述】:

这是我已经尝试过的:

第一个想法是,只需将两者放入一个数组中,将该数组与 kSecUseItemList 一起使用,因此钥匙串调用将仅对该数组中的项目进行操作,而不是对真正的钥匙串进行操作,然后像这样获得身份:

NSDictionary * searchQuery = @{
    (__bridge id)kSecClass:(__bridge id)kSecClassIdentity,
    (__bridge id)kSecUseItemList:@[(__bridge id)key, (__bridge id)cert],
    (__bridge id)kSecReturnRef:@YES
};
CFTypeRef foundItem = NULL;
OSStatus copyStatus = SecItemCopyMatching(
    (__bridge CFDictionaryRef)searchQuery, &foundItem
);

原来这行不通。引用文档:

@常数 kSecUseItemList 指定一个字典键,其值为 CFArray 的项目。如果提供,则将此数组视为 如果调用的 API 是,所有可能要搜索或添加的项目 SecItemAdd。此数组中的项目可能是SecKeyRef 类型, SecCertificateRefSecIdentityRefCFDataRef(对于持久性 项目引用。) 数组中的项目必须全部相同 输入。提供此属性时,不会搜索钥匙串。

嗯,它们不是同一类型,所以这行不通。

我的第二次尝试是将这两个项目添加到钥匙串(使用SecItemAdd()),它按预期工作,然后找到证书(使用SecItemCopyMatching()),它也成功并最终使用:

SecIdentityRef identity = NULL;
OSStatus copyStatus = SecIdentityCreateWithCertificate(NULL, cert, &identity);

但是 errKCItemNotFound 失败了。

查看 Keychain Access 应用程序中的项目,证书和私钥都在那里,它们都是正确的,但它们没有显示为形成身份(它们没有在“我的证书”下列出,证书只是列在“证书”下,密钥在“密钥”下)。

好的,我做错了什么或者我错过了什么重要的步骤?

如果我将密钥导出为 PKCS#8 并将证书导出为 DER 表示法,然后在命令行上使用 openssl 将两者组合成一个 PKCS#12 文件并使用 Keychain Access 导入该文件,然后它们将显示为钥匙串访问中的身份和此身份也可以正常工作(因此私钥实际上是证书中公钥的正确密钥)。但这并不是一个真正的选择,因为我的代码不能依赖于 OpenSSL,并且理想情况下可以移植到 iOS。

据我了解文档,身份匹配是通过匹配公钥哈希完成的,因此这可能与我的问题有关。系统如何知道我的公钥 SecKeyRef 的哈希值,这只是一个原始的私有 RSA 密钥?

文档还说我可以直接用SecAddItem() 添加SecIdentityRef,在这种情况下,我想一切都可能按预期工作(无法添加身份本身,将添加证书和私钥,但我认为以这种方式添加它们时身份绑定会没问题),但这听起来像是一个先有鸡还是先有蛋的问题,因为我首先如何获得该身份引用?

我不明白为什么没有 SecCreateIdentity(...) 函数只在输入时接受 SecCertificateRefSecKeyRef 并在输出时返回 SecIdentityRef


更新

这是我在SecKey.h 找到的一些有趣信息:

@constant kSecKeyLabel 类型 blob,用于私钥和公钥 这包含公钥的哈希值。这是用来 关联证书和密钥。它的值与值匹配 证书的kSecPublicKeyHashItemAttr已使用 从证书和密钥构造身份。 对于对称密钥,这是密钥的创建者 在生成密钥调用期间传入。

此值设置不正确。证书中的公钥散列为0x966C57...,但我的私钥包含0x097EAD...,这看起来像私钥本身的散列。如果我能以某种方式将此值设置为正确的值,我会尝试。


更新 2

这似乎是另一个死胡同。当我尝试在将密钥添加到钥匙串之前将kSecAttrApplicationLabelSecKeyUpdate() 一起设置时,我得到errKCItemNotFound,这是预期的,正如文档所述:

SecKeyRef 实例表示存储在钥匙串中的钥匙 可以安全地转换为 SecKeychainItemRef 进行操作 钥匙扣项目。另一方面,如果密钥没有存储在 钥匙串,将对象转换为SecKeychainItemRef 并传递 它对 Keychain Services 函数返回错误。

很公平。所以我首先添加密钥,然后从钥匙串中取回它,最后尝试更新kSecAttrApplicationLabel,但这也失败了,这个错误是errKCNoSuchAttr

哦,如果有人想知道我为什么要更新 kSecAttrApplicationLabel,当我说属性在我的第一次更新中命名为 kSecKeyLabelkSecKeyLabel 是旧属性枚举的枚举值,Apple 用于各种已全部弃用的 API 调用。新的 API 调用(如 SecItemUpdate())与字典一起使用,并且由于使用枚举值作为字典键有点难看,Apple 定义了一组新的字典键 CFStringRef

@constant kSecAttrApplicationLabel 指定字典键 其值为键的应用程序标签属性。 这与 kSecAttrLabel 不同(旨在 人类可读)。该属性用于查找键 以编程方式;特别是对于类的键 kSecAttrKeyClassPublickSecAttrKeyClassPrivate, 该属性的值是公钥的哈希值。 此项目是CFDataRef 的类型。 旧版密钥可能在此字段中包含 UUID,如 CFStringRef

所以这似乎是要更新的正确属性,不是吗?除了错误暗示该项目不存在此类属性外。即使同一个头文件明确将此属性列为SecKeyRef 项目的可能属性:

kSecClassKey物品属性:
kSecAttrAccess(仅限 OS X)
kSecAttrAccessControl
kSecAttrAccessGroup (iOS;如果指定了 kSecAttrSynchronizable 也适用于 OS X)
kSecAttrAccessible (iOS;如果指定了 kSecAttrSynchronizable 也适用于 OS X)
kSecAttrKeyClass
kSecAttrLabel
kSecAttrApplicationLabel
[...等等...]


更新 3

我得到的第一个答案建议改用SecItemCopyMatching(),但是,请理解这段代码:

CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching(
    (__bridge CFDictionaryRef)@{
        (__bridge id)kSecClass:(__bridge id)kSecClassIdentity,
        (__bridge id)kSecMatchItemList:@[(__bridge id)cert],
        (__bridge id)kSecReturnRef:@YES
     }, &result
);

实际上与他的代码功能相同:

SecIdentityRef result = NULL;
OSStatus status = SecIdentityCreateWithCertificate(
    NULL, cert &result
);

后一个只是较旧的 API 调用(较旧,但未弃用),当时钥匙串访问仅限于使用 Sec...RefCoreFoundation“对象”(Apple 试图在纯 C 中模仿一点 OO) ,而第一个是较新的 API,您通常只使用钥匙串项目的字典表示形式(因为它将免费转换为 Obj-C,当您使用 ARC 时,您只需要一些桥接转换),但您也可以选择回退到CoreFoundation“对象”(使用kSecMatchItemListkSecUseItemListkSecReturnRef等属性时)。我实际上很确定实际上只有一个真正的 API,而另一个只是在另一个之上实现(取决于哪个在哪个之上,新的可能只是为了方便而存在,也可能是旧的只是为了向后兼容而保留的)。

【问题讨论】:

    标签: macos certificate keychain private-key


    【解决方案1】:

    诀窍是先导出内存中的密钥,然后将其直接重新导入钥匙串,而不是直接添加到那里。看下面的代码(小心,是C++):

    static OSStatus AddKeyToKeychain(SecKeyRef privateKey, SecKeychainRef targetKeychain)
    {
        // This is quite similar to pal_seckey's ExportImportKey, but
        // a) is used to put something INTO a keychain, instead of to take it out.
        // b) Doesn't assume that the input should be CFRelease()d and overwritten.
        // c) Doesn't return/emit the imported key reference.
        // d) Works on private keys.
        SecExternalFormat dataFormat = kSecFormatWrappedPKCS8;
        CFDataRef exportData = nullptr;
    
        SecItemImportExportKeyParameters keyParams = {};
        keyParams.version = SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION;
        keyParams.passphrase = CFSTR("ExportImportPassphrase");
    
        OSStatus status = SecItemExport(privateKey, dataFormat, 0, &keyParams, &exportData);
    
        SecExternalFormat actualFormat = dataFormat;
        SecExternalItemType actualType = kSecItemTypePrivateKey;
        CFArrayRef outItems = nullptr;
    
        if (status == noErr)
        {
            status =
                SecItemImport(exportData, nullptr, &actualFormat, &actualType, 0, &keyParams, targetKeychain, &outItems);
        }
    
        if (exportData != nullptr)
            CFRelease(exportData);
    
        CFRelease(keyParams.passphrase);
        keyParams.passphrase = nullptr;
    
        if (outItems != nullptr)
            CFRelease(outItems);
    
        return status;
    }
    

    代码已被from here.

    【讨论】:

    • 您所描述的正是SecIdentityCreateWithCertificate() 所做的。它将在所有钥匙串中搜索匹配的私钥,并从中构建一个SecIdentityRef(它甚至可以按照您的代码建议在内部实现)。就像SecIdentityCreateWithCertificate() 一样,第二个SecItemCopyMatching() 的结果是errKCItemNotFound。另请参阅我对该问题的第一次更新 - 我认为这与它无法找到密钥的原因有关。顺便说一句,所有代码都不是 Swift,都是 Obj-C 代码。
    • github.com/dotnet/corefx/blob/… 是它的一个有效版本,这是我的回答所依据的。我可能在简单的功能上遇到了一些微妙的问题,但 SecItemCopyMatching 有效(使用永久链接重新发布评论,因此行号不会随着时间的推移而错位)
    • 该代码实际上可能有效,但如果有效,则由于您在上面引用的部分原因,它不起作用,而是因为函数AddKeyToKeychain() 不只是将密钥添加到钥匙串(如SecAddItem() 可以),而是将其导出到 CFDataRef 并直接将其重新导入到钥匙串中。这可能是修复丢失的公钥哈希的技巧,我会检查并及时更新。另一方面,也许您曾经测试过此代码的所有密钥都已经正确设置了正确的公钥哈希。我的私钥来自一个文件,里面只有一个私钥。
    • 好的,AddKeyToKeychain() 函数确实有效,所以我会奖励你这个答案,但我必须先重写它,因为上面的答案不是问题的答案,您的评论链接将我指向正确的答案。
    • 好的,现在答案真的是一个答案。一天之内,我可以分配给你 50 声望的赏金。只是一点点说明:此代码CFRelease(keyParams.passphrase); 毫无意义。 CFSTR() 创建字符串 constants,你也不会在 const char * x = "Test"; 上调用 free(x),对吗?字符串常量上的CFRelease() 是 NOP。它不会崩溃,因为 Apple 希望允许您将字符串常量传递给期望动态字符串的函数,因此允许尝试释放常量 CFStringRef's 但它没有意义。
    【解决方案2】:

    @bartonjs 的回答确实解决了我的问题,我只想在这里提供一些额外的有用信息,因为它解决问题的原因并不是很明显:

    首先,请注意安全对象可以只存在于内存中,也可以存在于由钥匙串存储备份的内存中。每个安全对象都有数据(定义对象的实际数据)和元数据(描述数据的附加信息)。但只有由钥匙串存储备份的对象才能具有元数据,因为元数据属性被定义为钥匙串 API 的一部分,而不是对象 API 的一部分。如果对象未存储在钥匙串中,则它们根本无法拥有元数据。请参阅我的问题的更新 2

    将私钥与其证书匹配所需的公钥哈希存储在元数据中。所以没有钥匙串存储备份的SecKeyRef 不能有这样的哈希值。因此,根本不可能使用未存储在钥匙串中的密钥生成身份 (SecIdentityRef)。证书不必存储在钥匙串中,但钥匙有。

    到目前为止,我只是使用SecItemAdd() 添加我的密钥,这似乎完全符合名称的含义,它只是将项目添加到钥匙串中,它只执行名称所暗示的操作,所以它不会做任何事情但将项目添加到钥匙串按原样。如果该项目已经有一个公钥散列,当它被添加到一个(新的/不同的)钥匙串时也会有一个公钥散列,但只有钥匙串中已经存在的项目才能有这个属性。结果是一个项目没有正确的公钥哈希集,这就是我所有问题的原因。

    现在我的代码使用了功能更强大的SecItemImport() 函数,因为“导入”可能需要更多的步骤,而不仅仅是添加一些钥匙串。显然,此导入功能还将确保在导入项目时正确填充元数据中的公钥哈希。为了使这种导入成为可能,我的新代码首先需要导出现有的密钥,然后才能将密钥直接重新导入所需的钥匙串。

    更新

    可能有趣的是,存在一个具有以下语法的函数:

    SecIdentityRef SecIdentityCreate(
        CFAllocatorRef allocator, 
        SecCertificateRef certificate, 
        SecKeyRef privateKey
    );
    

    这正是我想要的方式。 ffmpeg 实际上是using it。但这是私有 API,如果您打算将您的软件提交到任何应用商店,则不能使用它(使用私有 API 会导致您的软件被拒绝)。

    【讨论】:

      猜你喜欢
      • 2014-12-15
      • 1970-01-01
      • 1970-01-01
      • 2014-08-19
      • 2015-08-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多