如何通过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)签名,默认会包含三个认证属性,顺序固定:
contentType(OID: 1.2.840.113549.1.9.3):值为data类型(OID: 1.2.840.113549.1.7.1)signingTime(OID: 1.2.840.113549.1.9.5):值为当前UTC时间的ASN.1 UTCTime格式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);
关键注意事项
- 属性顺序绝对不能错:OpenSSL生成的签名中,认证属性的顺序是固定的,CryptoAPI如果打乱顺序,ASN.1结构就会不一致,导致签名无法和OpenSSL的结果匹配。
- 哈希算法严格对应:必须使用
CALG_SHA_256,和OpenSSL的-md sha256保持一致。 - 编码格式要匹配:设置
CMSG_ENCODE_ASN_FLAG确保生成DER编码,而非PEM。 - 分离签名标志:
CMSG_DETACHED_FLAG必须设置,否则会生成内嵌内容的签名,和OpenSSL的分离签名不符。
内容的提问来源于stack exchange,提问作者vaduha
相关产品推荐
相关产品推荐

