You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Windows CryptoAPI生成与OpenSSL格式一致的分离签名及属性填充

我刚好处理过类似的需求——要让Windows CryptoAPI生成和OpenSSL命令openssl smime -sign -md sha256 -in {f_in} -signer {cert} -inkey {key} -out {f_out} -outform DER完全一致的分离式S/MIME签名,核心就是对齐认证属性的设置顺序、ASN.1编码格式,尤其是你困惑的CMSG_SIGNER_ENCODE_INFO的rgAuthAttr填充部分,下面给你一步步拆解实现:

先明确OpenSSL的默认行为

这条OpenSSL命令生成的是分离式CMS(PKCS#7)签名,默认会包含三个认证属性,顺序固定:

  1. contentType(OID: 1.2.840.113549.1.9.3):值为data类型(OID: 1.2.840.113549.1.7.1)
  2. signingTime(OID: 1.2.840.113549.1.9.5):值为当前UTC时间的ASN.1 UTCTime格式
  3. messageDigest(OID: 1.2.840.113549.1.9.4):值为输入文件的SHA256哈希值

CryptoAPI必须严格复刻这个属性顺序和值,才能生成和OpenSSL完全一致的签名。

步骤1:加载Windows证书存储中的证书与私钥

首先要从系统证书存储中定位目标证书,并获取对应的私钥句柄:

HCERTSTORE hStore = CertOpenSystemStore(NULL, "MY"); // 打开个人证书存储
if (!hStore) { /* 错误处理 */ }

// 根据证书主题、指纹等查找目标证书
PCCERT_CONTEXT hCert = CertFindCertificateInStore(hStore, 
                                                  X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                                                  0, CERT_FIND_SUBJECT_STR, L"你的证书主题", NULL);
if (!hCert) { /* 错误处理 */ }

// 获取匹配的私钥句柄
HCRYPTPROV hProv = NULL;
DWORD dwKeySpec = 0;
BOOL fCallerFreeProv = FALSE;
if (!CryptAcquireCertificatePrivateKey(hCert, 
                                       CRYPT_ACQUIRE_COMPARE_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG,
                                       NULL, &hProv, &dwKeySpec, &fCallerFreeProv)) {
    /* 错误处理 */
}
步骤2:正确填充rgAuthAttr数组

这是核心部分,我们需要逐个构建三个认证属性,然后按顺序组装到数组中:

2.1 构建contentType属性

这个属性的值是szOID_PKCS_7_DATA的ASN.1编码:

CRYPT_ATTRIBUTE contentTypeAttr = {0};
contentTypeAttr.pszObjId = szOID_PKCS_9_CONTENT_TYPE;
contentTypeAttr.cValue = 1;
contentTypeAttr.rgValue = (CRYPT_DATA_BLOB*)LocalAlloc(LPTR, sizeof(CRYPT_DATA_BLOB));

// 编码OID为DER格式
if (!CryptEncodeObjectEx(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                        szOID_PKCS_7_DATA, NULL,
                        CRYPT_ENCODE_ALLOC_FLAG, NULL,
                        &contentTypeAttr.rgValue[0].pbData,
                        &contentTypeAttr.rgValue[0].cbData)) {
    /* 错误处理 */
}

2.2 构建signingTime属性

这个属性的值是当前UTC时间的ASN.1 UTCTime编码:

CRYPT_ATTRIBUTE signingTimeAttr = {0};
signingTimeAttr.pszObjId = szOID_PKCS_9_SIGNING_TIME;
signingTimeAttr.cValue = 1;
signingTimeAttr.rgValue = (CRYPT_DATA_BLOB*)LocalAlloc(LPTR, sizeof(CRYPT_DATA_BLOB));

SYSTEMTIME st;
GetSystemTime(&st); // 获取当前UTC时间
// 编码为ASN.1 UTCTime
if (!CryptEncodeObjectEx(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                        szOID_PKCS_9_SIGNING_TIME, &st,
                        CRYPT_ENCODE_ALLOC_FLAG, NULL,
                        &signingTimeAttr.rgValue[0].pbData,
                        &signingTimeAttr.rgValue[0].cbData)) {
    /* 错误处理 */
}

2.3 构建messageDigest属性

首先计算输入文件的SHA256哈希,再包装成属性:

// 计算输入文件的SHA256哈希
HCRYPTHASH hHash = NULL;
BYTE hashBuf[32];
DWORD hashLen = sizeof(hashBuf);

if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash)) { /* 错误处理 */ }

// 读取文件并更新哈希(示例代码,需处理大文件分块)
HANDLE hFile = CreateFileA("{f_in}", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) { /* 错误处理 */ }

BYTE fileBuf[4096];
DWORD bytesRead;
while (ReadFile(hFile, fileBuf, sizeof(fileBuf), &bytesRead, NULL) && bytesRead > 0) {
    if (!CryptHashData(hHash, fileBuf, bytesRead, 0)) { /* 错误处理 */ }
}
CloseHandle(hFile);

// 获取哈希值
if (!CryptGetHashParam(hHash, HP_HASHVAL, hashBuf, &hashLen, 0)) { /* 错误处理 */ }
CryptDestroyHash(hHash);

// 构建messageDigest属性
CRYPT_ATTRIBUTE messageDigestAttr = {0};
messageDigestAttr.pszObjId = szOID_PKCS_9_MESSAGE_DIGEST;
messageDigestAttr.cValue = 1;
messageDigestAttr.rgValue = (CRYPT_DATA_BLOB*)LocalAlloc(LPTR, sizeof(CRYPT_DATA_BLOB));
messageDigestAttr.rgValue[0].cbData = hashLen;
messageDigestAttr.rgValue[0].pbData = (BYTE*)LocalAlloc(LPTR, hashLen);
CopyMemory(messageDigestAttr.rgValue[0].pbData, hashBuf, hashLen);

2.4 组装rgAuthAttr到CMSG_SIGNER_ENCODE_INFO

注意属性顺序必须和OpenSSL一致:contentType → signingTime → messageDigest

CRYPT_ATTRIBUTE rgAuthAttr[3] = {contentTypeAttr, signingTimeAttr, messageDigestAttr};

CMSG_SIGNER_ENCODE_INFO signerInfo = {0};
signerInfo.cbSize = sizeof(CMSG_SIGNER_ENCODE_INFO);
signerInfo.pCertInfo = CertGetCertificateContextData(hCert);
signerInfo.hCryptProv = hProv;
signerInfo.dwKeySpec = dwKeySpec;
signerInfo.cAuthAttr = 3;
signerInfo.rgAuthAttr = rgAuthAttr;
步骤3:生成DER格式的分离签名

最后调用CryptoAPI的消息编码函数生成签名:

// 打开消息编码器,设置分离签名+DER编码标志
DWORD msgFlags = CMSG_DETACHED_FLAG | CMSG_ENCODE_ASN_FLAG;
HCRYPTMSG hMsg = CryptMsgOpenToEncode(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
                                      msgFlags, CMSG_SIGNED,
                                      (void*)&signerInfo, NULL, NULL);
if (!hMsg) { /* 错误处理 */ }

// 重新读取输入文件,更新到消息编码器
hFile = CreateFileA("{f_in}", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) { /* 错误处理 */ }

while (ReadFile(hFile, fileBuf, sizeof(fileBuf), &bytesRead, NULL) && bytesRead > 0) {
    if (!CryptMsgUpdate(hMsg, fileBuf, bytesRead, FALSE)) { /* 错误处理 */ }
}
CryptMsgUpdate(hMsg, NULL, 0, TRUE); // 结束消息更新
CloseHandle(hFile);

// 获取生成的DER签名数据
DWORD derSize = 0;
CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, NULL, &derSize);
BYTE* derSignData = (BYTE*)LocalAlloc(LPTR, derSize);
if (!CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, derSignData, &derSize)) { /* 错误处理 */ }

// 写入到输出文件
HANDLE hOutFile = CreateFileA("{f_out}", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hOutFile != INVALID_HANDLE_VALUE) {
    WriteFile(hOutFile, derSignData, derSize, &bytesRead, NULL);
    CloseHandle(hOutFile);
}

// 清理所有资源
LocalFree(derSignData);
CryptMsgClose(hMsg);
CertFreeCertificateContext(hCert);
CertCloseStore(hStore, 0);
if (fCallerFreeProv) CryptReleaseContext(hProv, 0);
// 别忘了释放三个属性中LocalAlloc的内存
LocalFree(contentTypeAttr.rgValue[0].pbData);
LocalFree(contentTypeAttr.rgValue);
LocalFree(signingTimeAttr.rgValue[0].pbData);
LocalFree(signingTimeAttr.rgValue);
LocalFree(messageDigestAttr.rgValue[0].pbData);
LocalFree(messageDigestAttr.rgValue);
关键注意事项
  1. 属性顺序绝对不能错:OpenSSL生成的签名中,认证属性的顺序是固定的,CryptoAPI如果打乱顺序,ASN.1结构就会不一致,导致签名无法和OpenSSL的结果匹配。
  2. 哈希算法严格对应:必须使用CALG_SHA_256,和OpenSSL的-md sha256保持一致。
  3. 编码格式要匹配:设置CMSG_ENCODE_ASN_FLAG确保生成DER编码,而非PEM。
  4. 分离签名标志:CMSG_DETACHED_FLAG必须设置,否则会生成内嵌内容的签名,和OpenSSL的分离签名不符。

内容的提问来源于stack exchange,提问作者vaduha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:36:54