如何从Windows证书存储的PCCERT_CONTEXT私钥获取NCRYPT_KEY_HANDLE用于CNG加解密?
刚好之前处理过类似的场景,从PCCERT_CONTEXT拿到CNG的NCRYPT_KEY_HANDLE确实需要绕一小步,但完全可以实现,而且之后就能无缝使用NCryptEncrypt/BCryptEncrypt这些现代CNG函数了。下面一步步给你拆解:
一、获取PCCERT_CONTEXT对应的NCRYPT_KEY_HANDLE
核心思路是用CryptAcquireCertificatePrivateKey这个兼容CryptoAPI和CNG的过渡函数,强制获取CNG格式的密钥句柄,具体步骤如下:
调用CryptAcquireCertificatePrivateKey获取通用密钥句柄
这个函数可以根据证书私钥的存储类型返回对应的句柄,我们通过参数强制它返回CNG的密钥句柄:// 假设你已经从证书存储拿到了PCCERT_CONTEXT(比如用CertFindCertificateInStore) PCCERT_CONTEXT pCertContext = ...; DWORD dwKeySpec = 0; BOOL fCallerFreeProv = FALSE; HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hGenericKey = NULL; // 关键参数:CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG 强制使用CNG密钥 if (!CryptAcquireCertificatePrivateKey( pCertContext, CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, NULL, &hGenericKey, &dwKeySpec, &fCallerFreeProv)) { // 错误处理:调用GetLastError()获取具体错误码 printf("CryptAcquireCertificatePrivateKey failed: %lu\n", GetLastError()); goto Cleanup; }验证并转换为NCRYPT_KEY_HANDLE
用NCryptIsKeyHandle检查拿到的通用句柄是否是CNG类型的,是的话直接转换即可:NCRYPT_KEY_HANDLE hNcryptKey = NULL; if (NCryptIsKeyHandle(hGenericKey)) { hNcryptKey = (NCRYPT_KEY_HANDLE)hGenericKey; printf("Successfully got NCRYPT_KEY_HANDLE!\n"); } else { // 如果返回的是旧CryptoAPI的HCRYPTPROV句柄,说明证书私钥是用旧Provider存储的 // 这种情况要么重新生成用CNG Provider的证书,要么把密钥导入到CNG存储 printf("Got legacy CryptoAPI handle instead of CNG key handle.\n"); goto Cleanup; }释放资源(重要)
根据fCallerFreeProv的标记,记得释放拿到的句柄:Cleanup: if (fCallerFreeProv) { if (NCryptIsKeyHandle(hGenericKey)) { NCryptFreeObject(hGenericKey); } else { CryptReleaseContext((HCRYPTPROV)hGenericKey, 0); } }
二、使用NCRYPT_KEY_HANDLE进行CNG加解密
拿到NCRYPT_KEY_HANDLE后,就可以直接用CNG的函数完成加解密了。这里以NCryptEncrypt(密钥存储层面的加密)为例,步骤很清晰:
// 假设hNcryptKey已经成功获取 BYTE pbPlainText[] = "This is the data to encrypt!"; DWORD dwPlainTextLen = sizeof(pbPlainText) - 1; // 去掉字符串结束符 BYTE* pbEncryptedData = NULL; DWORD dwEncryptedLen = 0; NTSTATUS status; // 第一步:获取加密后的数据长度(传入NULL缓冲区计算长度) status = NCryptEncrypt( hNcryptKey, pbPlainText, dwPlainTextLen, NULL, // 不需要附加数据 NULL, 0, &dwEncryptedLen, 0); // 加密标志,根据密钥类型调整(比如非对称加密可能需要NCRYPT_PAD_PKCS1_FLAG) if (status != ERROR_SUCCESS) { printf("NCryptEncrypt (get length) failed: 0x%08X\n", status); goto EncryptCleanup; } // 第二步:分配加密缓冲区 pbEncryptedData = (BYTE*)LocalAlloc(LPTR, dwEncryptedLen); if (!pbEncryptedData) { printf("Failed to allocate memory for encrypted data.\n"); goto EncryptCleanup; } // 第三步:执行加密 status = NCryptEncrypt( hNcryptKey, pbPlainText, dwPlainTextLen, NULL, pbEncryptedData, dwEncryptedLen, &dwEncryptedLen, NCRYPT_PAD_PKCS1_FLAG); // 非对称加密常用PKCS#1填充,根据你的算法调整 if (status != ERROR_SUCCESS) { printf("NCryptEncrypt failed: 0x%08X\n", status); goto EncryptCleanup; } // 现在pbEncryptedData里就是加密后的数据,长度是dwEncryptedLen printf("Encrypted %lu bytes of data.\n", dwEncryptedLen); EncryptCleanup: if (pbEncryptedData) { LocalFree(pbEncryptedData); } // 记得hNcryptKey的释放要和之前的Cleanup逻辑合并
如果想用BCryptEncrypt(纯算法层面的加密),需要先从NCRYPT_KEY_HANDLE导出密钥Blob,然后获取算法提供者句柄,再调用BCryptEncrypt,流程稍复杂,但适合需要更底层算法控制的场景。
几个关键注意点
- 确保证书的私钥是用CNG兼容的Provider生成的(比如
Microsoft RSA SChannel Cryptographic Provider),旧的Provider生成的私钥无法直接拿到NCRYPT_KEY_HANDLE。 - 如果是访问本地机器存储里的证书,可能需要管理员权限才能获取私钥句柄。
- 加密时的填充标志要和密钥的算法匹配,比如RSA加密常用
NCRYPT_PAD_PKCS1_FLAG,ECC可能不需要填充。
内容的提问来源于stack exchange,提问作者jorgen
相关产品推荐
相关产品推荐

