如何从 SecCertificateRef 和 SecKeyRef 获取 SecIdentityRef
How to get a SecIdentityRef from a SecCertificateRef and a SecKeyRef
这是我已经尝试过的方法:
第一个想法是,将两者都放入一个数组中,将该数组与 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
);
原来这行不通。引用文档:
@constant kSecUseItemList
Specifies a dictionary key whose value is a
CFArray
of items. If provided, this array is treated as the set of
all possible items to search, or add if the API being called is
SecItemAdd
. The items in this array may be of type SecKeyRef
,
SecCertificateRef
, SecIdentityRef
, or CFDataRef
(for a persistent
item reference.) The items in the array must all be of the same
type. When this attribute is provided, no keychains are searched.
嗯,它们不是同一类型,所以这行不通。
我的第二次尝试是将这两个项目添加到钥匙串(使用 SecItemAdd()
),这按预期工作,然后找到证书(使用 SecItemCopyMatching()
),这也成功了,最后使用:
SecIdentityRef identity = NULL;
OSStatus copyStatus = SecIdentityCreateWithCertificate(NULL, cert, &identity);
但是 errKCItemNotFound
失败了。
查看钥匙串访问应用程序中的项目,证书和私钥都在那里,它们都是正确的,但它们没有显示为形成身份(它们未在 "My Certificates" 下列出,证书仅列在 "Certificates" 下,密钥列在 "Keys").
下
好的,我做错了什么或者我错过了什么重要的步骤?
如果我将密钥导出到 PKCS#8 并将证书导出到 DER 表示法,然后在命令行上使用 openssl
将两者合并到 PKCS#12 文件中并使用钥匙串访问导入该文件,然后它们在钥匙串访问中显示为身份,并且此身份也可以正常工作(因此私钥确实是证书中 public 密钥的正确密钥)。但这并不是一个真正的选择,因为我的代码不能依赖 OpenSSL,并且理想情况下可以移植到 iOS.
据我了解文档,身份匹配是通过匹配 public 键哈希来完成的,因此这可能与我的问题有关。系统如何知道我 SecKeyRef
的 public 密钥的哈希值,它只是一个原始 RSA 私钥?
文档还说我可以直接用 SecAddItem()
添加 SecIdentityRef
,在这种情况下,我想一切都可能按预期工作(无法添加身份本身,证书和将添加私钥,但我假设以这种方式添加时身份绑定是可以的),但这听起来像是先有鸡还是先有蛋的问题,因为我首先如何获得该身份参考?
我不明白为什么没有 SecCreateIdentity(...)
函数只需要一个 SecCertificateRef
和 SecKeyRef
输入,returns 一个 SecIdentityRef
输出。
更新
这是我在 SecKey.h
中发现的一些有趣信息:
@constant kSecKeyLabel
type blob, for private and public keys
this contains the hash of the public key. This is used to
associate certificates and keys. Its value matches the value
of the kSecPublicKeyHashItemAttr
of a certificate and it's used
to construct an identity from a certificate and a key.
For symmetric keys this is whatever the creator of the key
passed in during the generate key call.
这个值设置不正确。证书中的 public 密钥散列为 0x966C57...
,但我的私钥包含 0x097EAD...
,这看起来像是私钥本身的散列。如果我能以某种方式将此值设置为正确的值,我会尝试。
更新 2
这似乎是另一个死胡同。当我尝试在将密钥添加到钥匙串之前将密钥设置为 kSecAttrApplicationLabel
和 SecKeyUpdate()
时,我得到 errKCItemNotFound
,这是预期的,如文档所述:
A SecKeyRef
instance that represents a key that is stored in a keychain
can be safely cast to a SecKeychainItemRef
for manipulation as a
keychain item. On the other hand, if the key is not stored in a
keychain, casting the object to a SecKeychainItemRef
and passing
it to Keychain Services functions returns errors.
很公平。所以我首先添加密钥,然后从钥匙串中取回它,最后尝试更新 kSecAttrApplicationLabel
,但这也失败了,错误是 errKCNoSuchAttr
.
哦,万一有人想知道为什么我在第一次更新时说属性名为 kSecKeyLabel
而要更新 kSecAttrApplicationLabel
: kSecKeyLabel
是Apple 用于各种 API 调用的旧属性枚举已全部弃用。新的 API 调用(如 SecItemUpdate()
)与字典一起使用,因为使用枚举值作为字典键有点难看,Apple 定义了一组新的字典键,它们是 CFStringRef
.
@constant kSecAttrApplicationLabel
Specifies a dictionary key
whose value is the key's application label attribute.
This is different from the kSecAttrLabel
(which is intended to be
human-readable). This attribute is used to look up a key
programmatically; in particular, for keys of class
kSecAttrKeyClassPublic
and kSecAttrKeyClassPrivate
,
the value of this attribute is the hash of the public key.
This item is a type of CFDataRef
.
Legacy keys may contain a UUID in this field as a CFStringRef
.
所以这似乎是要更新的正确属性,不是吗?除了错误暗示该项目不存在此类属性。即使同一个头文件明确地将此属性列为 SecKeyRef
项的可能属性:
kSecClassKey
item attributes:
kSecAttrAccess
(OS X only)
kSecAttrAccessControl
kSecAttrAccessGroup
(iOS; also OS X if kSecAttrSynchronizable specified)
kSecAttrAccessible
(iOS; also OS X if kSecAttrSynchronizable specified)
kSecAttrKeyClass
kSecAttrLabel
kSecAttrApplicationLabel
[... and so on ...]
更新 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...Ref
CoreFoundation "objects"(Apple 的尝试在纯 C 中模仿一点 OO),而第一个是较新的 API,您通常只使用钥匙串项的字典表示(因为它将免费转换为 Obj-C,您只需要一些当你使用 ARC 时桥接转换),但你也可以选择回退到 CoreFoundation "objects"(当使用 kSecMatchItemList
、kSecUseItemList
或 kSecReturnRef
等属性时)。我实际上很确定实际上只有一个 API 而另一个只是在另一个之上实现的(取决于哪个在哪个之上,较新的可能只是为了方便而存在或者旧的只是为了向后兼容而保留)。
诀窍是先导出内存中的密钥,然后将其直接重新导入到钥匙串中,而不是仅仅将其添加到那里。看下面的代码(注意,是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。
@bartonjs的回答确实解决了我的问题,我只是想在这里提供一些额外的有用信息,因为它解决问题的原因不是很明显:
首先请注意,安全对象可以只存在于内存中,也可以存在于由钥匙链存储备份的内存中。每个安全对象都有数据(定义对象的实际数据)和元数据(描述数据的附加信息)。但是只有钥匙串存储备份的对象可以有元数据,因为元数据属性被定义为钥匙串 API 的一部分,而不是对象 API 的一部分。如果对象没有存储在钥匙串中,它们就没有元数据。请参阅我的问题的更新 2。
public 密钥散列存储在元数据中,这是将私钥与其证书相匹配所必需的。因此,没有钥匙串存储备份的 SecKeyRef
不能有这样的哈希。因此,根本不可能使用未存储在钥匙串中的密钥生成身份 (SecIdentityRef
)。证书不必存储在钥匙串中,但密钥有。
到目前为止,我只是使用 SecItemAdd()
添加我的密钥,这似乎完全符合名称的含义,它只是将项目添加到钥匙串,而且它只按照名称的含义进行操作,所以它赢了除了将项目添加到钥匙串 原样 之外,什么都不做。如果该项目已经有一个 public 密钥哈希,它在添加到 (new/different) 钥匙链时也会有一个 public 密钥哈希,但只有钥匙链中已有的项目才能具有此属性.结果是一个项目没有正确的 public 键哈希集,这是我所有问题的原因。
现在我的代码使用 SecItemImport()
函数,它更强大,因为 "import" 可能需要比仅添加一些钥匙串更多的步骤。显然,此导入功能还将确保在导入项目时正确填充元数据中的 public 键散列。为了使导入成为可能,我的新代码首先需要导出现有密钥,然后它可以将密钥直接重新导入到所需的钥匙串中。
更新
也许有趣的是,存在一个具有以下语法的函数:
SecIdentityRef SecIdentityCreate(
CFAllocatorRef allocator,
SecCertificateRef certificate,
SecKeyRef privateKey
);
这完全符合我的期望。 ffmpeg其实就是using it。但这是私有的 API,如果您打算将软件提交到任何应用商店,则不得使用它(使用私有 API 将会拒绝您的软件)。
这是我已经尝试过的方法:
第一个想法是,将两者都放入一个数组中,将该数组与 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
);
原来这行不通。引用文档:
@constant
kSecUseItemList
Specifies a dictionary key whose value is aCFArray
of items. If provided, this array is treated as the set of all possible items to search, or add if the API being called isSecItemAdd
. The items in this array may be of typeSecKeyRef
,SecCertificateRef
,SecIdentityRef
, orCFDataRef
(for a persistent item reference.) The items in the array must all be of the same type. When this attribute is provided, no keychains are searched.
嗯,它们不是同一类型,所以这行不通。
我的第二次尝试是将这两个项目添加到钥匙串(使用 SecItemAdd()
),这按预期工作,然后找到证书(使用 SecItemCopyMatching()
),这也成功了,最后使用:
SecIdentityRef identity = NULL;
OSStatus copyStatus = SecIdentityCreateWithCertificate(NULL, cert, &identity);
但是 errKCItemNotFound
失败了。
查看钥匙串访问应用程序中的项目,证书和私钥都在那里,它们都是正确的,但它们没有显示为形成身份(它们未在 "My Certificates" 下列出,证书仅列在 "Certificates" 下,密钥列在 "Keys").
下好的,我做错了什么或者我错过了什么重要的步骤?
如果我将密钥导出到 PKCS#8 并将证书导出到 DER 表示法,然后在命令行上使用 openssl
将两者合并到 PKCS#12 文件中并使用钥匙串访问导入该文件,然后它们在钥匙串访问中显示为身份,并且此身份也可以正常工作(因此私钥确实是证书中 public 密钥的正确密钥)。但这并不是一个真正的选择,因为我的代码不能依赖 OpenSSL,并且理想情况下可以移植到 iOS.
据我了解文档,身份匹配是通过匹配 public 键哈希来完成的,因此这可能与我的问题有关。系统如何知道我 SecKeyRef
的 public 密钥的哈希值,它只是一个原始 RSA 私钥?
文档还说我可以直接用 SecAddItem()
添加 SecIdentityRef
,在这种情况下,我想一切都可能按预期工作(无法添加身份本身,证书和将添加私钥,但我假设以这种方式添加时身份绑定是可以的),但这听起来像是先有鸡还是先有蛋的问题,因为我首先如何获得该身份参考?
我不明白为什么没有 SecCreateIdentity(...)
函数只需要一个 SecCertificateRef
和 SecKeyRef
输入,returns 一个 SecIdentityRef
输出。
更新
这是我在 SecKey.h
中发现的一些有趣信息:
@constant
kSecKeyLabel
type blob, for private and public keys this contains the hash of the public key. This is used to associate certificates and keys. Its value matches the value of thekSecPublicKeyHashItemAttr
of a certificate and it's used to construct an identity from a certificate and a key. For symmetric keys this is whatever the creator of the key passed in during the generate key call.
这个值设置不正确。证书中的 public 密钥散列为 0x966C57...
,但我的私钥包含 0x097EAD...
,这看起来像是私钥本身的散列。如果我能以某种方式将此值设置为正确的值,我会尝试。
更新 2
这似乎是另一个死胡同。当我尝试在将密钥添加到钥匙串之前将密钥设置为 kSecAttrApplicationLabel
和 SecKeyUpdate()
时,我得到 errKCItemNotFound
,这是预期的,如文档所述:
A
SecKeyRef
instance that represents a key that is stored in a keychain can be safely cast to aSecKeychainItemRef
for manipulation as a keychain item. On the other hand, if the key is not stored in a keychain, casting the object to aSecKeychainItemRef
and passing it to Keychain Services functions returns errors.
很公平。所以我首先添加密钥,然后从钥匙串中取回它,最后尝试更新 kSecAttrApplicationLabel
,但这也失败了,错误是 errKCNoSuchAttr
.
哦,万一有人想知道为什么我在第一次更新时说属性名为 kSecKeyLabel
而要更新 kSecAttrApplicationLabel
: kSecKeyLabel
是Apple 用于各种 API 调用的旧属性枚举已全部弃用。新的 API 调用(如 SecItemUpdate()
)与字典一起使用,因为使用枚举值作为字典键有点难看,Apple 定义了一组新的字典键,它们是 CFStringRef
.
@constant
kSecAttrApplicationLabel
Specifies a dictionary key whose value is the key's application label attribute. This is different from thekSecAttrLabel
(which is intended to be human-readable). This attribute is used to look up a key programmatically; in particular, for keys of classkSecAttrKeyClassPublic
andkSecAttrKeyClassPrivate
, the value of this attribute is the hash of the public key. This item is a type ofCFDataRef
. Legacy keys may contain a UUID in this field as aCFStringRef
.
所以这似乎是要更新的正确属性,不是吗?除了错误暗示该项目不存在此类属性。即使同一个头文件明确地将此属性列为 SecKeyRef
项的可能属性:
kSecClassKey
item attributes:
kSecAttrAccess
(OS X only)
kSecAttrAccessControl
kSecAttrAccessGroup
(iOS; also OS X if kSecAttrSynchronizable specified)
kSecAttrAccessible
(iOS; also OS X if kSecAttrSynchronizable specified)
kSecAttrKeyClass
kSecAttrLabel
kSecAttrApplicationLabel
[... and so on ...]
更新 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...Ref
CoreFoundation "objects"(Apple 的尝试在纯 C 中模仿一点 OO),而第一个是较新的 API,您通常只使用钥匙串项的字典表示(因为它将免费转换为 Obj-C,您只需要一些当你使用 ARC 时桥接转换),但你也可以选择回退到 CoreFoundation "objects"(当使用 kSecMatchItemList
、kSecUseItemList
或 kSecReturnRef
等属性时)。我实际上很确定实际上只有一个 API 而另一个只是在另一个之上实现的(取决于哪个在哪个之上,较新的可能只是为了方便而存在或者旧的只是为了向后兼容而保留)。
诀窍是先导出内存中的密钥,然后将其直接重新导入到钥匙串中,而不是仅仅将其添加到那里。看下面的代码(注意,是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。
@bartonjs的回答确实解决了我的问题,我只是想在这里提供一些额外的有用信息,因为它解决问题的原因不是很明显:
首先请注意,安全对象可以只存在于内存中,也可以存在于由钥匙链存储备份的内存中。每个安全对象都有数据(定义对象的实际数据)和元数据(描述数据的附加信息)。但是只有钥匙串存储备份的对象可以有元数据,因为元数据属性被定义为钥匙串 API 的一部分,而不是对象 API 的一部分。如果对象没有存储在钥匙串中,它们就没有元数据。请参阅我的问题的更新 2。
public 密钥散列存储在元数据中,这是将私钥与其证书相匹配所必需的。因此,没有钥匙串存储备份的 SecKeyRef
不能有这样的哈希。因此,根本不可能使用未存储在钥匙串中的密钥生成身份 (SecIdentityRef
)。证书不必存储在钥匙串中,但密钥有。
到目前为止,我只是使用 SecItemAdd()
添加我的密钥,这似乎完全符合名称的含义,它只是将项目添加到钥匙串,而且它只按照名称的含义进行操作,所以它赢了除了将项目添加到钥匙串 原样 之外,什么都不做。如果该项目已经有一个 public 密钥哈希,它在添加到 (new/different) 钥匙链时也会有一个 public 密钥哈希,但只有钥匙链中已有的项目才能具有此属性.结果是一个项目没有正确的 public 键哈希集,这是我所有问题的原因。
现在我的代码使用 SecItemImport()
函数,它更强大,因为 "import" 可能需要比仅添加一些钥匙串更多的步骤。显然,此导入功能还将确保在导入项目时正确填充元数据中的 public 键散列。为了使导入成为可能,我的新代码首先需要导出现有密钥,然后它可以将密钥直接重新导入到所需的钥匙串中。
更新
也许有趣的是,存在一个具有以下语法的函数:
SecIdentityRef SecIdentityCreate(
CFAllocatorRef allocator,
SecCertificateRef certificate,
SecKeyRef privateKey
);
这完全符合我的期望。 ffmpeg其实就是using it。但这是私有的 API,如果您打算将软件提交到任何应用商店,则不得使用它(使用私有 API 将会拒绝您的软件)。