0%

【Linux技术分享】ie下增加导入pfx证书功能

ie下增加导入pfx证书功能

问题概述

目前wine下不支持pfx证书导入功能,但是window下证书导入很多种方案,比如ie导入,用certutil.exe导入,组件导入,并且导入存放方案也有很多种,wine目前只支持ie下简单的cer证书导入,该文档方案旨在支持wine下ie的pfx证书导入功能

名词解释

  • pfx证书:PFX(Personal Information Exchange)是一种证书和私钥的打包格式,也称为PKCS#12,通常用密码保护(对称加密)。大概组成部分如下:
    1. 一个或多个X.509证书(公钥)
    2. 对应的私钥(RSA/ECC私钥)
    3. 证书链(可选的中间CA证书)
    4. 属性信息(友好名称、密钥用法等)
  • GNUTLS:GNUTLS(GNU Transport Layer Security Library)是一个开源的SSL/TLS加密库。目前wine后端解密框架就用的是这个。
  • ActiveX:ActiveX 是 Microsoft 在1996年推出的组件对象模型(COM)技术,主要用于在Web浏览器中嵌入和运行丰富的交互式内容。用户可以通过自己编写的JavaScript脚本,来查询注册表找到对应的DLL/OCX,从而调用dll中的接口函数。

    现状

  1. 用户目前使用的是ie导入的pfx证书,弹出无法识别,导入效果如下:
    图1

修复方案

增加wine下无法导入.pfx证书界面

  1. 打开日志通道定位ie导入证书的界面是通过cryptui进行显示的
    图2

  2. 该模块导入证书主要分两类,ui模式导入和非ui模式导入,代码如下

    1
    2
    3
    4
    5
    6
    if (!(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);
  3. 在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;
    ......
  4. pfx证书导入在import_file_dlg_proc函数中处理,大致流程是import_file_dlg_proc->import_validate_filename->open_store_from_file->CryptQueryObject

  5. 在CryptQueryObject函数中就是处理证书的,该模块就实现cryptui->crypt32模块的跳转函数

  6. 但是在import_validate_filename函数中根本没有处理pfx文件加密的问题,pfx中存在私钥,大部分情况都会需要输入密码

  7. 流程完整性考虑密码输入应该在>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/证书存储句柄重新获取,

  8. 修改代码如下,通过show_import_pfx_password_dialog获取pfx密码,通过PFXImportCertStore导入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    HCERTSTORE 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);
    }
    }
    }
  9. 导入证书显示效果如下:
    图3

    处理wine下无法获取.pfx证书密钥问题

  10. 应用程序通过activeX调用用户模块,再获取指定pfx密钥,目前获取失败,显示如下:
    图4

  11. 通过日志发现wine是调用CRYPT_RegReadSerializedFromReg函数 从注册表读取序列化的证书数据

    1
    0160:trace:crypt:CRYPT_RegReadSerializedFromReg Adding cert with hash L"EEC56C95D5A99DC4E06FE92225387507C2B30061"
  12. 创建证书上下文

    1
    2
    0160:trace:crypt:CertCreateCertificateContext (00000001, 03732EAE, 1059)
    0160:trace:crypt:CryptDecodeObjectEx (0x00000001, #0002, 03732EAE, 1059, 0x00008000, 00000000, 03169B30, 03169B34)
  13. 读取证书属性(关键步骤)
    0160:trace:crypt:read_serialized_KeyProvInfoProperty L”mecry”,L”Microsoft Enhanced Cryptographic Provider v1.0”,1,00000000,0,00000000,3

  14. 设置证书属性
    0160:trace:crypt:CertSetCertificateContextProperty (00D16ED8, 2, 00000000, 0441B130)
    0160:trace:crypt:CertSetCertificateContextProperty (00D16ED8, 3, 00000000, 03169A0C)

  15. 访问私钥,尝试获取与证书关联的私钥,打开密钥容器 “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)

  16. 解密受保护的私钥(核心),私钥数据以加密形式存储(738字节)使用 CryptUnprotectData 解密,解密后得到596字节的RSA私钥BLOB
    0160:trace:crypt:CryptUnprotectData called
    0160:trace:crypt:CryptUnprotectData pDataOut cbData: 596

  17. 私钥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)

  18. 导入私钥到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

  19. 哈希计算创建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)

  20. 签名操作失败,调用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, …)
    调用签名函数

  21. RSAENH_CPHashData函数主要是通过RSAENH_CPGetUserKey获取密钥,这个函数就失败了,RSAENH_CPGetUserKey函数实现逻辑只处理了AT_KEYEXCHANGEAT_SIGNATURE,但是传入了dwKeySpec=00000003导致返回为空

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    switch (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;
    }

  22. 00000003 是 AT_KEYEXCHANGE (1) + AT_SIGNATURE (2) 的组合值,增加case AT_BOTH:解决该问题,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    case 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只能显示一个问题

  1. 根据上图发现我在ie中导入了两个pfx证书,但是wine下用activeX调用显示时只看见了一个,如下:
    图5
  2. 开启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
  3. 将window下的certutil.exe模块和依赖拷贝至目录中,获取当前容器下的证书内容如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    my "个人的"
    (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)
  4. 可以明显看到有两个证书,从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 命令成功完成。
  5. 由于wine下是能看见一个证书并非两个证书都无法看见,学习wine下代码,定位导入证书函数流程为import_key->CryptAcquireContextW->RSAENH_CPAcquireContext->new_key_container->create_container_key,在create_container_key函数中发现该函数是通过注册表将证书进行写入读取\Software\Wine\Crypto\RSA,打开wine容器下的注册表只能看到一项,如下:
    图6
  6. 在pfx的导入/查询函数都是通过CryptAcquireContextW,目前导入的时候默认为null导致两个证书都在默认容器中mecry(当前用户)
  • 在RSAENH_CPAcquireContext中的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    if (pszContainer && *pszContainer)
    {
    lstrcpynA(szKeyContainerName, pszContainer, MAX_PATH);
    }
    else
    {
    DWORD dwLen = sizeof(szKeyContainerName);
    if (!GetUserNameA(szKeyContainerName, &dwLen)) return FALSE;
    }
  1. 目前问题比较清晰,需要增加每个证书对应的容器即可,将每个容器进行区分,增加代码如下
    1
    2
    3
    4
    generate_name_from_pfx(pfx->pbData, pfx->cbData,
    container_name, ARRAY_SIZE(container_name));

    prov = import_key( data, container_name, flags );

总体解决方案总结

  1. 需要在cryptui模块中增加pfx导入密码输入地方,并且将pfx进行导入
  2. 需要在获取密钥的地方增加支持该控件的参数
  3. 需要对每个证书生成一个唯一的容器标识,防止插件找不到证书

效果展示

图7
图8