CryptAcquireContext返回NTE_BAD_KEY_STATE

博客围绕随机数安全性展开,指出C/C++及常见方式产生随机数安全性不足,介绍了Windows和Linux平台安全产生随机数的方法。重点讲述了Windows下CryptAcquireContext获取安全服务失败的问题,通过分析找到解决办法,但不清楚深层次原因,还提及该API已不推荐使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们知道,通过rand函数产生的随机数安全性不高,为了保证随机数的安全性,我们一般会选择一个随机数种子,通过该种子增加破解随机数的难度。C/C++下是如下代码:

#include <stdlib.h>
#include <time.h>
int main()
{  
   srand((unsigned)time(NULL));
   printf("%d\n",rand());   
   return 0;
}

但是,这种方式产生的随机数的安全性也不高。在实际生产环境中,可能需要产生大量的随机数,由于time的精度是秒,因此,很有可能同时产生的随机数是同一个,导致安全性的降低。

因此,Windows平台提供了如下安全的方式产生随机数:

//-------------------------------------------------------------------
// Declare and initialize variables.
HCRYPTPROV hCryptProv = NULL;        // handle for a cryptographic
                                    // provider context
LPCSTR UserName = "MyKeyContainer";  // name of the key container
                                    // to be used
//-------------------------------------------------------------------
// Attempt to acquire a context and a key
// container. The context will use the default CSP
// for the RSA_FULL provider type. DwFlags is set to zero
// to attempt to open an existing key container.
if(CryptAcquireContext(
  &hCryptProv,               // handle to the CSP
  UserName,                  // container name
  NULL,                      // use the default provider
  PROV_RSA_FULL,             // provider type
  0))                        // flag values
{
   printf("A cryptographic context with the %s key container \n",
 UserName);
   printf("has been acquired.\n\n");
}
else
{
//-------------------------------------------------------------------
// An error occurred in acquiring the context. This could mean
// that the key container requested does not exist. In this case,
// the function can be called again to attempt to create a new key
// container. Error codes are defined in Winerror.h.
if (GetLastError() == NTE_BAD_KEYSET)
{
  if(CryptAcquireContext(
     &hCryptProv,
     UserName,
     NULL,
     PROV_RSA_FULL,
     CRYPT_NEWKEYSET))
   {
     printf("A new key container has been created.\n");
   }
   else
   {
     printf("Could not create a new key container.\n");
     exit(1);
   }
 }
 else
 {
     printf("A cryptographic service handle could not be "
         "acquired.\n");
     exit(1);
  }
 
} // End of else.
//-------------------------------------------------------------------
// A cryptographic context and a key container are available. Perform
// any functions that require a cryptographic provider handle.
//-------------------------------------------------------------------
// When the handle is no longer needed, it must be released.
if (CryptReleaseContext(hCryptProv,0))
{
   printf("The handle has been released.\n");
}
else
{
   printf("The handle could not be released.\n");
}

Linux平台提供了如下方法产生随机数:

int fd = open("/dev/urandom", O_RDONLY);
if (fd == -1)
{
  printf("open /dev/urandom fail,error:%d", errno);
}
size_t size = 32;
char* output = new char[size];
while (size)
{
  ssize_t len = read(fd, output, size);
  if (len < 0)
  {
    // /dev/urandom reads CAN give EAGAIN errors! (maybe EINTR as well)
    if (errno != EINTR && errno != EAGAIN)
    {
      printf("read /dev/urandom fail, error:%d\n", errno);
    }
    continue;
   }
  output += len;
  size -= len;
}
close(fd);
delete[] output;
return 0;

最近遇到了一个问题,Windows下通过CryptAcquireContext获取一个安全服务CSP( Cryptographic Service Provider)时,失败了,错误码是NTE_BAD_KEY_STATE(0x8009000B )

奇怪的是,这个错误只在客户的一台电脑上出现,其他电脑没有遇到这种情况。相关功能已经上线2年多了,这么久还是第一次收到客户反馈这个问题。

微软对NTE_BAD_KEY_STATE这个错误码的解释是:The user password has changed since the private keys were encrypted。这句话的直观解释是,私钥在被加密后,用户密码改变了。这里有两个信息,一是私钥;二是用户密码。但是除此之外没啥其他信息。因为对这个函数的机制不清楚,所以对具体的错误信息也不知道怎么解决。于是问客户有没有更改过密码,用户说没有用户重启过设备,也重启了电脑,问题依然存在

MSDN上对CryptAcquireContext的解释如下:

The CryptAcquireContext function is used to acquire a handle to a particular key container within a particular cryptographic service provider (CSP). This returned handle is used in calls to CryptoAPI functions that use the selected CSP.

CryptAcquireContext函数的作用是从一个特定的密码服务提供者获取一个特定的密钥容器句柄。

后来看到了对CryptAcquireContext函数机制的解释:

The Microsoft software CSPs encrypt the private keys using DPAPI (CryptProtectData), which encrypts this using a master key. The master key is encrypted with the user password. The NTE_BAD_KEY_STATE error code is returned when the master key can't be decrypted. Typically, this is because the user's password has changed and DPAPI wasn't able to deal with it.

The most common issue in this area occurs when a local (non-domain) user's password is administratively reset. On WinXP, this causes all data protected by DPAPI (including user private keys) to be lost; at least until the password is set back. This is by design, and in fact is an important security feature.

这段话提到如下几个信息:

  1. Windows CPS通过DPAPI(数据保护应用编程接口)加密私钥,加密私钥的密钥是主密钥

  2. 主密钥是通过用户密码加密的

  3. 当用户修改密码后就不能解密出主密钥了,所以DPAPI函数就不能处理这个情况了

  4. 这种问题的主要出现场景是本地(非域)用户的密码被管理性重置

  5. 在WinXP上,修改密码使得被DPAPI保护的数据(包括用户私钥)丢失(只是解密不了),直至密码被重新设置

  6. 该策略特意设计的,并且也是一个重要的安全特征

客户的系统是Win7,就他的电脑有这个问题,明确说并没有修改过密码,并且发现问题时已经自行重启过系统。

于是陷入了僵局。

后来在stackoverflow也看到了对这个问题的描述:

However, he is sure that his user password has not changed, nor has the Server password (User computers contain a shortcut that points to an executable on the server, no other users are experiencing this problem). 

After searching through some Microsoft forums, it is clear that others have experienced this error (also having the same password), and that in every case it is highly sporadic and often unique to one computer in a system of machines using the same program

大意是用户遇到这个问题后,束手无策,以至于重装了系统。客户明确表示并没有修改密码,也没有修改服务器密码。在搜索微软论坛后,发现其他人也遇到了类似问题,也没有修改密码。这个问题很分散,在相同系统上运行相同的程序,也只是个别电脑会有这个问题。用了很多精力查找这个问题,也没有找到解决方法。

在这个问题的回复栏,有个小伙伴给出了他的解决方法,将CryptAcquireContext函数的最后一个参数dwFlags由0改为CRYPT_VERIFYCONTEXT,然后解决了这个问题

没有办法,我只能死马当活马医,试了一下,问题果然解决了。但是不清楚为什么?

于是在https://docs.microsoft.com查看具体的接口说明。

dwFlags的说明为:Flag value,通常设置为0,但是一些应用可以设置为其他值,譬如CRYPT_VERIFYCONTEXT。

CRYPT_VERIFYCONTEXT的说明如下:

This option is intended for applications that are using ephemeral keys, or applications that do not require access to persisted private keys, such as applications that perform only hashing, encryption, and digital signature verification. Only applications that create signatures or decrypt messages need access to a private key. In most cases, this flag should be set.

For file-based CSPs, when this flag is set, the pszContainer parameter must be set to NULL. The application has no access to the persisted private keys of public/private key pairs. When this flag is set, temporary public/private key pairs can be created, but they are not persisted.

For hardware-based CSPs, such as a smart card CSP, if the pszContainer parameter is NULL or blank, this flag implies that no access to any keys is required, and that no UI should be presented to the user. This form is used to connect to the CSP to query its capabilities but not to actually use its keys. If the pszContainer parameter is not NULL and not blank, then this flag implies that access to only the publicly available information within the specified container is required. The CSP should not ask for a PIN. Attempts to access private information (for example, the CryptSignHash function) will fail.

When CryptAcquireContext is called, many CSPs require input from the owning user before granting access to the private keys in the key container. For example, the private keys can be encrypted, requiring a password from the user before they can be used. However, if the CRYPT_VERIFYCONTEXT flag is specified, access to the private keys is not required and the user interface can be bypassed.

主要内容如下:

  1. 这个值主要用于使用临时秘钥的应用或者不需要获取持久化私钥的应用,譬如只进行hash、加密及数字签名认证的应用

  2. 对于基于文件的CSP,如果这个标志置上,那么pszContainer 取值必须为NULL,应用对于持久化的公私钥对中的私钥没有权限。这个时候,可用创建临时的公私钥对,但是不能进行持久化

  3. 对于基于硬件的CSP,譬如智能卡CSP,如果pszContainer 为空,意味着对于获得任何密钥都没有权限。如果pszContainer 不为空,意味着只对获得的容器有公有可用信息权限,试图获取私有信息会失败(譬如CryptSignHash 函数)

  4. 当调用CryptAcquireContext时,许多CSP在授予对密钥容器中的私钥的访问权限之前,都需要所属用户的输入。譬如,私钥可以加密,需要用户提供密码才能使用。但是,如果置CRYPT_VERIFYCONTEXT标志,就不需要访问私钥,从而可以绕过用户界面

在Remarks中,有如下建议:

For performance reasons, we recommend that you set the pszContainer parameter to NULL and the dwFlags parameter to CRYPT_VERIFYCONTEXT in all situations where you do not require a persisted key.

对于不需要持久化密钥的场景,建议使用CRYPT_VERIFYCONTEXT

那么在哪些场景下,应该使用CRYPT_VERIFYCONTEXT 呢?

1. You are creating a hash(创建hash).

2. You are generating a symmetric key to encrypt or decrypt data(产生对称密钥进行数据的加解密).

3. You are deriving a symmetric key from a hash to encrypt or decrypt data(从哈希派生对称密钥以加密或解密数据).

4. You are verifying a signature(验证签名).

5. You plan to export a symmetric key, but not import it within the crypto context's lifetime. A context can be acquired by using the CRYPT_VERIFYCONTEXT flag if you only plan to import the public key for the last two scenarios(准备导出对称密钥,但不在加密上下文的生存期内导入它。).

6. You are performing private key operations, but you are not using a persisted private key that is stored in a key container(正在执行私钥操作,但没有使用存储在密钥容器中的持久化私钥).

微软在介绍CryptAcquireContext函数时,有如下重要提示:

Important  This API is deprecated. New and existing software should start using Cryptography Next Generation APIs. Microsoft may remove this API in future releases.

这个API已经不推荐使用,可能被废弃,需要用下一代的API,微软可能在后续的版本中移除这个API。

下一代API指的是CNG(Cryptography Next Generation), 如BCryptEncrypt 及 BCryptDecrypt。

网站有对CryptAcquireContext的使用及问题说明,对于CRYPT_VERIFYCONTEXT ,是这么说的:

When you are not using a persisted private key, the CRYPT_VERIFYCONTEXT flag can be used when CryptAcquireContext is called. This tells CryptoAPI to create a key container in memory that will be released when CryptReleaseContext is called

CRYPT_VERIFYCONTEXT主要是告诉CryptAcquireContext在内存中创建一个密钥容器,然后调用CryptReleaseContext时释放相关内存。

上面说了这么多,其实依然不知道问题的根源在哪,虽然问题解决了,但是深层次的原因并不知道。网络上找了很多资料,也没有发现所以然。正在读这篇文章的你,有没有想法?

扫描二维码,关注“小眼睛的梦呓”公众号,在手机端查看文章
扫描二维码,关注“清远的梦呓”公众号,在手机端查看文章
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值