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

OpenSSL3自定义CNG Provider客户端mTLS无法指定证书私钥

自定义OpenSSL3 Provider实现mTLS客户端认证的问题

开发背景

基于C语言开发mTLS客户端认证功能,使用自定义OpenSSL3 Provider(基于Microsoft CNG API实现):

  • Provider调度表已实现函数:
    • OSSL_FUNC_PROVIDER_GETTABLE_PARAMS
    • OSSL_FUNC_PROVIDER_GET_PARAMS
    • OSSL_FUNC_PROVIDER_QUERY_OPERATION
    • OSSL_FUNC_PROVIDER_GET_CAPABILITIES
    • OSSL_FUNC_PROVIDER_TEARDOWN
  • 查询操作函数返回以下操作的调度表:
    • OSSL_OP_STORE:从Windows证书存储枚举并加载证书和私钥
    • OSSL_OP_KEYMGMT:基础密钥管理操作
    • OSSL_OP_KEYEXCH:EC密钥交换实现(当前为存根,待完善)
    • OSSL_OP_SIGNATURE:ECDSA签名操作实现
  • 当capability为TLS-GROUP时,get capabilities函数返回secp256r1组,确保Client Hello包含该组。

目标是使用Windows证书存储中由Provider加载的特定证书进行密钥交换认证:证书私钥存储在CNG容器中,密钥对采用EC secp256r1曲线,证书信息如下:

> openssl x509 -in user1.cert.pem -text -noout
Certificate:
    Data:
        ...
        Signature Algorithm: ecdsa-with-SHA256
        ...
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
            ...
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Cert Type:
                SSL Client, S/MIME
            Netscape Comment:
                OpenSSL Generated Client Certificate
            ...
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                TLS Web Client Authentication
    Signature Algorithm: ecdsa-with-SHA256
    ...

操作步骤与异常现象

通过OSSL_OP_STORE加载证书和私钥,得到X509和EVP_PKEY结构,调用SSL_CTX_use_certificate和SSL_CTX_use_PrivateKey设置SSL上下文,SSL_CTX_check_private_key返回1,但抓包发现异常:

  1. 首次Client Hello的supported_group扩展包含secp256r1 (0x0017),但key_share extension使用的是x25519组的临时密钥,而非指定证书的公钥;
  2. 服务器不支持x25519,回复Hello Retry Request并选择secp256r1,符合预期;
  3. 客户端随后中止连接,错误提示该密钥类型无对应操作,排查发现客户端尝试生成新的secp256r1密钥对,但未实现keymgmt密钥生成函数;
  4. 实现keymgmt密钥生成函数后,客户端每次运行都会生成新密钥,而非使用指定的密钥对;
  5. 尝试调用SSL_use_certificate、SSL_use_PrivateKey和SSL_check_privateKey均返回1,但指定的证书和私钥仍未被使用。

相关代码实现

以下是keymgmt导出函数(OpenSSL用于匹配证书与私钥):

int cng_keymgmt_export_ec(void* keydata, int selection, OSSL_CALLBACK* param_cb, void* cbarg)
{
    printf(">> cng_keymgmt_export_ec \n");

    T_CNG_KEYMGMT_KEYDATA* cng_keydata = (T_CNG_KEYMGMT_KEYDATA*) keydata;
    OSSL_PARAM* p                      = NULL;
    OSSL_PARAM params[3];
    unsigned char* raw_keydata = NULL;

    if (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY)
    {
        /* Tough luck, no private key exporting planned */
        return 0;
    }

    if (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY)
    {
        DWORD public_blob_size;
        SECURITY_STATUS ss = NCryptExportKey(
            cng_keydata->windows_key_handle,
            0,
            BCRYPT_PUBLIC_KEY_BLOB,
            NULL,
            NULL,
            0,
            &public_blob_size,
            0
        );
        if (ss != ERROR_SUCCESS)
        {
            return 0;
        }
        DWORD publicBlobExpectedSize = public_blob_size;
        PBYTE public_blob            = malloc(public_blob_size);
        if (public_blob == NULL)
        {
            return 0;
        }
        ss = NCryptExportKey(
            cng_keydata->windows_key_handle,
            0,
            BCRYPT_PUBLIC_KEY_BLOB,
            NULL,
            public_blob,
            publicBlobExpectedSize,
            &public_blob_size,
            0
        );
        if (ss != ERROR_SUCCESS)
        {
            free(public_blob);
            return 0;
        }

        ULONG magic = ((BCRYPT_KEY_BLOB*) public_blob)->Magic;
        if (magic != BCRYPT_ECDH_PUBLIC_P256_MAGIC && magic != BCRYPT_ECDH_PUBLIC_P384_MAGIC &&
            magic != BCRYPT_ECDH_PUBLIC_P521_MAGIC && magic != BCRYPT_ECDSA_PUBLIC_P256_MAGIC &&
            magic != BCRYPT_ECDSA_PUBLIC_P384_MAGIC && magic != BCRYPT_ECDSA_PUBLIC_P521_MAGIC)
        {
            free(public_blob);
            return 0;
        }

        BCRYPT_ECCKEY_BLOB* pb = (BCRYPT_ECCKEY_BLOB*) public_blob;
        ULONG xy_size          = pb->cbKey;

        const char* curve_group_name = NULL;
        if (magic == BCRYPT_ECDH_PUBLIC_P256_MAGIC || magic == BCRYPT_ECDSA_PUBLIC_P256_MAGIC)
        {
            curve_group_name = "P-256";
        }
        else if (magic == BCRYPT_ECDH_PUBLIC_P384_MAGIC || magic == BCRYPT_ECDSA_PUBLIC_P384_MAGIC)
        {
            curve_group_name = "P-384";
        }
        else if (magic == BCRYPT_ECDH_PUBLIC_P521_MAGIC || magic == BCRYPT_ECDSA_PUBLIC_P521_MAGIC)
        {
            curve_group_name = "P-521";
        }

        size_t raw_keydata_size = sizeof(unsigned char) * (1 + 2 * xy_size);
        raw_keydata             = (unsigned char*) malloc(raw_keydata_size);
        if (NULL == raw_keydata)
        {
            free(public_blob);
            return 0;
        }

        raw_keydata[0] = 0x04;
        memcpy(raw_keydata + 1, public_blob + sizeof(BCRYPT_ECCKEY_BLOB), xy_size);
        memcpy(raw_keydata + 1 + xy_size, public_blob + sizeof(BCRYPT_ECCKEY_BLOB) + xy_size, xy_size);

        free(public_blob);

        params[0] = OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_GROUP_NAME, curve_group_name, 0);
        params[1] = OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_PUB_KEY, raw_keydata, raw_keydata_size);
        params[2] = OSSL_PARAM_construct_end();
        p         = params;
    }

    if (selection & OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS)
    {
        if (p == NULL)
        {
            return 0;
        }
    }

    if (selection & OSSL_KEYMGMT_SELECT_OTHER_PARAMETERS)
    {
        if (p == NULL)
        {
            return 0;
        }
    }

    int ret = param_cb(p, cbarg);
    if (raw_keydata)
    {
        free(raw_keydata);
    }
    return ret;
}

疑问

我遗漏了什么?这个问题已经困扰我两周,甚至考虑辞职,昨天我哭了。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 12:25:55