ie下增加导入pfx证书功能
问题概述
目前wine下不支持pfx证书导入功能,但是window下证书导入很多种方案,比如ie导入,用certutil.exe导入,组件导入,并且导入存放方案也有很多种,wine目前只支持ie下简单的cer证书导入,该文档方案旨在支持wine下ie的pfx证书导入功能
名词解释
- pfx证书:PFX(Personal Information Exchange)是一种证书和私钥的打包格式,也称为PKCS#12,通常用密码保护(对称加密)。大概组成部分如下:
- 一个或多个X.509证书(公钥)
- 对应的私钥(RSA/ECC私钥)
- 证书链(可选的中间CA证书)
- 属性信息(友好名称、密钥用法等)
- GNUTLS:GNUTLS(GNU Transport Layer Security Library)是一个开源的SSL/TLS加密库。目前wine后端解密框架就用的是这个。
- ActiveX:ActiveX 是 Microsoft 在1996年推出的组件对象模型(COM)技术,主要用于在Web浏览器中嵌入和运行丰富的交互式内容。用户可以通过自己编写的JavaScript脚本,来查询注册表找到对应的DLL/OCX,从而调用dll中的接口函数。
现状
- 用户目前使用的是ie导入的pfx证书,弹出无法识别,导入效果如下:

修复方案
增加wine下无法导入.pfx证书界面
打开日志通道定位ie导入证书的界面是通过cryptui进行显示的

该模块导入证书主要分两类,ui模式导入和非ui模式导入,代码如下
1
2
3
4
5
6if (!(dwFlags & CRYPTUI_WIZ_NO_UI))
ret = show_import_ui(dwFlags, hwndParent, pwszWizardTitle, pImportSrc,
hDestCertStore);
else if (pImportSrc)
ret = do_import(dwFlags, hwndParent, pwszWizardTitle, pImportSrc,
hDestCertStore);在ie中进行导入证书是通过
show_import_ui进行处理,该函数是一个页面式的窗口过程1
2
3
4
5
6
7
8
9......
pages[nPages].pfnDlgProc = import_welcome_dlg_proc;
......
pages[nPages].pfnDlgProc = import_file_dlg_proc;
......
pages[nPages].pfnDlgProc = import_store_dlg_proc;
......
pages[nPages].pfnDlgProc = import_finish_dlg_proc;
......pfx证书导入在import_file_dlg_proc函数中处理,大致流程是
import_file_dlg_proc->import_validate_filename->open_store_from_file->CryptQueryObject在CryptQueryObject函数中就是处理证书的,该模块就实现cryptui->crypt32模块的跳转函数
但是在
import_validate_filename函数中根本没有处理pfx文件加密的问题,pfx中存在私钥,大部分情况都会需要输入密码流程完整性考虑密码输入应该在
>CryptQueryObject函数中处理,但是crypt32模块理论上不应该有ui处理,所以pfx密码输入需要放在import_validate_filename函数中,当HCERTSTORE source = open_store_from_file(data->dwFlags, fileName,&data->contentType);函数获取data->contentType == CERT_QUERY_CONTENT_PFX时,通过pfx导入函数将source/证书存储句柄重新获取,修改代码如下,通过
show_import_pfx_password_dialog获取pfx密码,通过PFXImportCertStore导入1
2
3
4
5
6
7
8
9
10
11
12
13
14HCERTSTORE source = open_store_from_file(data->dwFlags, fileName,
&data->contentType);
if(data->contentType == CERT_QUERY_CONTENT_PFX){
WCHAR password[MAX_STRING_LEN] = {0};
if (show_import_pfx_password_dialog(hwnd, fileName, password, MAX_STRING_LEN))
{
CRYPT_DATA_BLOB blob;
if(CRYPT_ReadBlobFromFile(fileName, &blob)){
if(PFXIsPFXBlob(&blob))
source = PFXImportCertStore(&blob, password,
CRYPT_EXPORTABLE | CRYPT_USER_KEYSET);
}
}
}导入证书显示效果如下:

处理wine下无法获取
.pfx证书密钥问题应用程序通过activeX调用用户模块,再获取指定
pfx密钥,目前获取失败,显示如下:
通过日志发现wine是调用
CRYPT_RegReadSerializedFromReg函数 从注册表读取序列化的证书数据1
0160:trace:crypt:CRYPT_RegReadSerializedFromReg Adding cert with hash L"EEC56C95D5A99DC4E06FE92225387507C2B30061"
创建证书上下文
1
20160:trace:crypt:CertCreateCertificateContext (00000001, 03732EAE, 1059)
0160:trace:crypt:CryptDecodeObjectEx (0x00000001, #0002, 03732EAE, 1059, 0x00008000, 00000000, 03169B30, 03169B34)读取证书属性(关键步骤)
0160:trace:crypt:read_serialized_KeyProvInfoProperty L”mecry”,L”Microsoft Enhanced Cryptographic Provider v1.0”,1,00000000,0,00000000,3设置证书属性
0160:trace:crypt:CertSetCertificateContextProperty (00D16ED8, 2, 00000000, 0441B130)
0160:trace:crypt:CertSetCertificateContextProperty (00D16ED8, 3, 00000000, 03169A0C)访问私钥,尝试获取与证书关联的私钥,打开密钥容器 “mecry”
0160:trace:crypt:CryptAcquireCertifduiicatePrivateKey (00D16F58, 00000000, 00000000, 0316BEB8, 0316BE74, 00000000)
0160:trace:crypt:CryptAcquireContextW (0316A14C, L”mecry”, L”Microsoft Enhanced Cryptographic Provider v1.0”, 1, 00000000)解密受保护的私钥(核心),私钥数据以加密形式存储(738字节)使用 CryptUnprotectData 解密,解密后得到596字节的RSA私钥BLOB
0160:trace:crypt:CryptUnprotectData called
0160:trace:crypt:CryptUnprotectData pDataOut cbData: 596私钥BLOB结构
07,02,00,00,00,a4,00,00,52,53,41,32,00,04,00,00,01,00,01,00,51,41,bd,94,b1,e8,71,d1,28,b6,a0,5c,8c,59,32,6b…
BLOB类型:07 = PRIVATEKEYBLOB
密钥类型:RSA_KEYX (RSA密钥交换)
密钥长度:1024位(0x400 = 1024)导入私钥到CSP,将解密后的私钥导入到加密服务提供者(CSP),创建RSA密钥对象
0160:trace:crypt:import_key blob type: 7
0160:trace:crypt:new_key alg = “RSA_KEYX”, dwKeyLen = 1024
0160:trace:crypt:import_private_key installing key exchange key哈希计算创建SHA1哈希对象(0x8004 = CALG_SHA1),对数据 “abcde”(5字节)进行哈希
0160:trace:crypt:CryptCreateHash (0x38aa160, 0x8004, 0x0, 00000000, 0316BEBC)
0160:trace:crypt:RSAENH_CPHashData (hProv=0000000a, hHash=0000000d, pbData=0161B760, dwDataLen=5, dwFlags=00000000)签名操作失败,调用
RSAENH_CPHashData函数,pbSignature=00000000,只获取签名长度,没有获取实际签名
0160:trace:crypt:CryptSignHashW (0x389c2e0, 3, (null), 00000000, 00000000, 0316BE64)
0160:trace:crypt:RSAENH_CPSignHash (hProv=0000000a, hHash=0000000d, dwKeySpec=00000003, …)
调用签名函数RSAENH_CPHashData函数主要是通过RSAENH_CPGetUserKey获取密钥,这个函数就失败了,RSAENH_CPGetUserKey函数实现逻辑只处理了
AT_KEYEXCHANGE,AT_SIGNATURE,但是传入了dwKeySpec=00000003导致返回为空1
2
3
4
5
6
7
8
9
10
11
12
13
14
15switch (dwKeySpec)
{
case AT_KEYEXCHANGE:
copy_handle(&handle_table, pKeyContainer->hKeyExchangeKeyPair, RSAENH_MAGIC_KEY,
phUserKey);
break;
case AT_SIGNATURE:
copy_handle(&handle_table, pKeyContainer->hSignatureKeyPair, RSAENH_MAGIC_KEY,
phUserKey);
break;
default:
*phUserKey = (HCRYPTKEY)INVALID_HANDLE_VALUE;
}00000003 是 AT_KEYEXCHANGE (1) + AT_SIGNATURE (2) 的组合值,增加
case AT_BOTH:解决该问题,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15case AT_BOTH:
if (pKeyContainer->hSignatureKeyPair != (HCRYPTKEY)INVALID_HANDLE_VALUE)
{
copy_handle(&handle_table, pKeyContainer->hSignatureKeyPair, RSAENH_MAGIC_KEY,
phUserKey);
}
else if (pKeyContainer->hKeyExchangeKeyPair != (HCRYPTKEY)INVALID_HANDLE_VALUE)
{
copy_handle(&handle_table, pKeyContainer->hKeyExchangeKeyPair, RSAENH_MAGIC_KEY,
phUserKey);
}
else
{
*phUserKey = (HCRYPTKEY)INVALID_HANDLE_VALUE;
}
处理wine下导入两个pfx只能显示一个问题
- 根据上图发现我在ie中导入了两个pfx证书,但是wine下用activeX调用显示时只看见了一个,如下:

- 开启file的日志通道,发现在ie的activeX插件运行时会日志重定向至
iexplore.log,内容如下,只遍历了一个证书:1
2
3
4[INFO] [P0032] [T0540] [2026-01-27 16:36:27:128] [JITDeviceManager.cpp(3508)]: 证书发生变化,广播>证书1张。
[INFO] [P0032] [T0540] [2026-01-27 16:36:27:128] [JITDeviceManager.cpp(3025)]: CLTPLT No.1 JITCertSN[证书[0DDE372A]], SubjectCN[FCMS超级管理员], SubjectDN[CN=FCMS超级管理员, S=62, O=01, O=MOF, C=CN], IssuerCN[Private Certificate Authority Of MOF], IssuerDN[CN=Private Certificate Authority Of MOF, O=MOF, C=CN], IssuerReverseDN[C=CN, O=MOF, CN=Private Certificate Authority Of MOF], SerialNumber[58E15893949F8E2F], EffectiveInfo[03/14/2023], ExpiredInfo[03/11/2033], AsymmType[RSA], HashALg[SHA1], KeySpec[签名],KeyUsage[000000C8],StoreName[MY], ContainerName[mecry], DeviceName[CSP],LibName[], Version[1], KeyType[CSP]
[ERROR] [P0032] [T0540] [2026-01-27 16:36:27:675] [JCPCSPContainer.cpp(125)]: [ExportCertificate][CSP_STANDARD] 导出证书失败 错误码: 0xE000000E - 将window下的
certutil.exe模块和依赖拷贝至目录中,获取当前容器下的证书内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16my "个人的"
(null)
(null) 66dc052e91a6fb2a
CN=Private Certificate Authority Of MOF, O=MOF, C=CN
2023/3/14 15:14
2033/3/11 15:14
CN=FCMS系统管理员, S=62, O=01, O=MOF, C=CN
(null)
(null) eec56c95d5a99dc4e06fe92225387507c2b30061
(null)
CN=FCMS超级管理员, S=62, O=01, O=MOF, C=CN
(null)
(null) 2d519ebaeaf9ca73ba2e4c9f7b9c97fac764c9d6
(null)
0250:fixme:ncrypt:NCryptIsKeyHandle (0xa307e0): stub
(null) - 可以明显看到有两个证书,从window下该命令进行对比,发现wine下缺少很多内容,但是哈希还是有的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28================ 证书 0 ================
序列号: 66dc052e91a6fb2a
颁发者: CN=Private Certificate Authority Of MOF, O=MOF, C=CN
NotBefore: 2023/3/14 15:14
NotAfter: 2033/3/11 15:14
使用者: CN=FCMS系统管理员, S=62, O=01, O=MOF, C=CN
非根证书
证书哈希(sha1): eec56c95d5a99dc4e06fe92225387507c2b30061
密钥容器 = {81D20652-C659-48FC-B6E2-66B82A1C1881}
唯一容器名称: 4cd7be763adea5572744a1f99c3a412b_42bec1a3-8aa0-4868-85e3-2b6d81967bcc
提供程序 = Microsoft Enhanced Cryptographic Provider v1.0
私钥不能导出
通过了签名测试
================ 证书 1 ================
序列号: 58e15893949f8e2f
颁发者: CN=Private Certificate Authority Of MOF, O=MOF, C=CN
NotBefore: 2023/3/14 15:16
NotAfter: 2033/3/11 15:16
使用者: CN=FCMS超级管理员, S=62, O=01, O=MOF, C=CN
非根证书
证书哈希(sha1): 2d519ebaeaf9ca73ba2e4c9f7b9c97fac764c9d6
密钥容器 = 618B9599-FAAC-4990-971D-9357AF422ED6
唯一容器名称: e50ef612cec653f50e591d0cbea64044_42bec1a3-8aa0-4868-85e3-2b6d81967bcc
提供程序 = Microsoft Enhanced Cryptographic Provider v1.0
私钥不能导出
通过了签名测试
CertUtil: -store 命令成功完成。 - 由于wine下是能看见一个证书并非两个证书都无法看见,学习wine下代码,定位导入证书函数流程为
import_key->CryptAcquireContextW->RSAENH_CPAcquireContext->new_key_container->create_container_key,在create_container_key函数中发现该函数是通过注册表将证书进行写入读取\Software\Wine\Crypto\RSA,打开wine容器下的注册表只能看到一项,如下:
- 在pfx的导入/查询函数都是通过
CryptAcquireContextW,目前导入的时候默认为null导致两个证书都在默认容器中mecry(当前用户)
- 在RSAENH_CPAcquireContext中的
1
2
3
4
5
6
7
8
9if (pszContainer && *pszContainer)
{
lstrcpynA(szKeyContainerName, pszContainer, MAX_PATH);
}
else
{
DWORD dwLen = sizeof(szKeyContainerName);
if (!GetUserNameA(szKeyContainerName, &dwLen)) return FALSE;
}
- 目前问题比较清晰,需要增加每个证书对应的容器即可,将每个容器进行区分,增加代码如下
1
2
3
4generate_name_from_pfx(pfx->pbData, pfx->cbData,
container_name, ARRAY_SIZE(container_name));
prov = import_key( data, container_name, flags );
总体解决方案总结
- 需要在
cryptui模块中增加pfx导入密码输入地方,并且将pfx进行导入 - 需要在获取密钥的地方增加支持该控件的参数
- 需要对每个证书生成一个唯一的容器标识,防止插件找不到证书
效果展示

