数字签名使用证书和密钥从USB令牌

Digital Signing using certificate and key from USB token

本文关键字:USB 令牌 密钥 证书 数字签名      更新时间:2023-10-16

我想使用来自USB令牌(加密狗)的用户密钥和证书对文件签名。

我一直在寻找这个一段时间,在stackoverflow和其他网站,但没有得到任何有用的除了一些好的功能在。net框架(我没有使用)。

似乎由于密钥没有暴露,加密是由硬件本身完成的。这是否意味着每个硬件制造商都提供了自己的api,而没有通用的方法来解决这个问题?

而且,我读到一旦令牌插入计算机,它的证书就被加载到系统存储中。可以使用商店的证书吗?如何在存储中的其他证书中识别和访问这样的证书?那私钥呢?

当证书可以从。p12或。pfx文件中提取时,我使用OpenSSL进行数字签名。

如果我错了,请纠正我,我是这个话题的新手。

我不知道我会把OpenSSL引擎的任何方面描述为"相当简单"。命令行版本很混乱,我只能在自己编写代码后才能弄清楚命令行在做什么。操作顺序和生命周期没有很好地调用(我仍然不完全知道这些是什么,但我的系统正在运行并且不再泄漏内存,所以,耶。)

我已经把功能版本在github: https://github.com/tkil/openssl-pkcs11-samples

以下是tok-sign.c相关部分的导览:

首先是一些帮助:

#define FAIL( msg, dest )                      
    do {                                       
        fprintf( stderr, "error: " msg "n" ); 
        goto dest;                             
    } while ( 0 )
/* mandatory is "not optional"... */
const int CMD_MANDATORY = 0;

可能关于dynamic引擎最奇怪的事情是它真的是一个元引擎:你给它提供各种参数,当你给它LOAD时,它加载动态库并使一个新的引擎可用。一旦你知道正确的操作顺序,它在代码中是很简单的。在这里,我们访问动态引擎,配置它,然后要求它引入pkcs11引擎:

ENGINE_load_dynamic();
ENGINE * dyn = ENGINE_by_id( "dynamic" );
if ( ! dyn )
    FAIL( "retrieving 'dynamic' engine", free_out_sig_file );
// this is the bridge between OpenSSL and any generic PCKS11 provider:
char * engine_pkcs11_so = "/opt/crypto/lib/engines/engine_pkcs11.so";
if ( 1 != ENGINE_ctrl_cmd_string( dyn, "SO_PATH", engine_pkcs11_so, CMD_MANDATORY ) )
    FAIL( "dyn: setting so_path <= 'engine_pkcs11.so'", free_dyn );
if ( 1 != ENGINE_ctrl_cmd_string( dyn, "ID", "pkcs11", CMD_MANDATORY ) )
    FAIL( "dyn: setting id <= 'pkcs11'", free_dyn );
if ( 1 != ENGINE_ctrl_cmd( dyn, "LIST_ADD", 1, NULL, NULL, CMD_MANDATORY ) )
    FAIL( "dyn: setting list_add <= 1", free_dyn );
if ( 1 != ENGINE_ctrl_cmd( dyn, "LOAD", 1, NULL, NULL, CMD_MANDATORY ) )
    FAIL( "dyn: setting load <= 1", free_dyn );

此时,如果所有这些调用都成功,那么您的OpenSSL实例现在可以访问一个名为"pkcs11"的新引擎。现在我们需要访问这个新引擎,正确配置它,并让它初始化自己:

ENGINE * pkcs11 = ENGINE_by_id( "pkcs11" );
if ( ! pkcs11 )
    FAIL( "pkcs11: unable to get engine", free_dyn );
// this is the actual pkcs11 provider we're using.  in this case, it's
// from the OpenSC package.
char * opensc_pkcs11_so = "/opt/crypto/lib/opensc-pkcs11.so";
if ( 1 != ENGINE_ctrl_cmd_string( pkcs11, "MODULE_PATH", opensc_pkcs11_so, CMD_MANDATORY ) )
    FAIL( "setting module_path <= 'opensc-pkcs11.so'", free_pkcs11 );
if ( 1 != ENGINE_ctrl_cmd_string( pkcs11, "PIN", key_pin, CMD_MANDATORY ) )
    FAIL( "setting pin", free_pkcs11 );
if ( 1 != ENGINE_init( pkcs11 ) )
    FAIL( "pkcs11: unable to initialize engine", free_pkcs11 );

现在我们可以访问令牌,我们可以获得私钥用于OpenSSL操作:

EVP_PKEY * key = ENGINE_load_private_key( pkcs11, key_id, NULL, NULL );
if ( ! key )
    FAIL( "reading private key", free_pkcs11 );

但是,我不知道如何通过ENGINE接口提取相应的证书,所以我直接去LibP11:

PKCS11_CTX * p11_ctx = PKCS11_CTX_new();
if ( ! p11_ctx )
    FAIL( "opening pkcs11 context", free_key );
if ( 0 != PKCS11_CTX_load( p11_ctx, opensc_pkcs11_so ) )
    FAIL( "unable to load module", free_p11_ctx );
PKCS11_SLOT * p11_slots;
unsigned int num_p11_slots;
if ( 0 != PKCS11_enumerate_slots( p11_ctx, &p11_slots, &num_p11_slots ) )
    FAIL( "enumerating slots", free_p11_ctx_module );
PKCS11_SLOT * p11_used_slot =
  PKCS11_find_token( p11_ctx, p11_slots, num_p11_slots );
if ( ! p11_used_slot )
    FAIL( "finding token", free_p11_slots );
PKCS11_CERT * p11_certs;
unsigned int num_p11_certs;
if ( 0 != PKCS11_enumerate_certs( p11_used_slot->token, &p11_certs, &num_p11_certs ) )
    FAIL( "enumerating certs", free_p11_slots );

最后,我们开始为CMS_Sign请求收集数据。我们查看令牌上的所有证书,挑选出与私钥对应的证书,然后将其余的证书存储在OpenSSL STACK_OF(X509):

中。
STACK_OF(X509) * extra_certs = sk_X509_new_null();
if ( ! extra_certs )
    FAIL( "allocating extra certs", free_p11_slots );
X509 * key_cert = NULL;
for ( unsigned int i = 0; i < num_p11_certs; ++i )
{
    PKCS11_CERT * p11_cert = p11_certs + i;
    if ( ! p11_cert->label )
        continue;
    // fprintf( stderr, "p11: got cert label='%s', x509=%pn",
    //         p11_cert->label, p11_cert->x509 );
    if ( ! p11_cert->x509 )
    {
        fprintf( stderr, "p11: ... no x509, ignoringn" );
        continue;
    }
    const char * label = p11_cert->label;
    const unsigned int label_len = strlen( label );
    if ( strcmp( label, key_label ) == 0 )
    {
        // fprintf( stderr, "p11: ... saving as signing certn" );
        key_cert = p11_cert->x509;
    }
    else if ( strncmp( label, "encrypt", 7 ) == 0 &&
              label_len == 8 &&
              '0' <= label[7] && label[7] <= '3' )
    {
        // fprintf( stderr, "p11: ... ignoring as encrypting certn" );
    }
    else
    {
        // fprintf( stderr, "p11: ... saving as extra certn" );
        if ( ! sk_X509_push( extra_certs, p11_cert->x509 ) )
            FAIL( "pushing extra cert", free_extra_certs );
    }
}
if ( ! key_cert )
    FAIL( "finding signing cert", free_extra_certs );

最后,我们可以对数据签名,然后将签名输出到一个der格式的文件中:

CMS_ContentInfo * ci = CMS_sign( key_cert, key, extra_certs, in_data_file,
                                 CMS_DETACHED | CMS_BINARY );
if ( ! ci )
    FAIL( "could not create signing structure", free_extra_certs );
if ( 1 != i2d_CMS_bio( out_sig_file, ci ) )
       FAIL( "could not write signature in DER", free_ci );

之后,只是清理:

free_ci:
    CMS_ContentInfo_free( ci );
free_extra_certs:
    /* these certs are actually "owned" by the libp11 code, and are
     * presumably freed with the slot or context. */
    sk_X509_free( extra_certs );
free_p11_slots:
    PKCS11_release_all_slots( p11_ctx, p11_slots, num_p11_slots );
free_p11_ctx_module:
    PKCS11_CTX_unload( p11_ctx );
free_p11_ctx:
    PKCS11_CTX_free( p11_ctx );
free_key:
    EVP_PKEY_free( key );
free_pkcs11:
    ENGINE_free( pkcs11 );
free_dyn:
    ENGINE_free( dyn );

您可以通过使用OpenSSL的引擎来实现这一点-并为其提供pkcs# 11引擎。

这可以通过命令行(-engine标志)或通过设置引擎来完成。openssl发行版/apps/中的apps.c有很好的例子。

一个典型的命令行调用看起来像
usr/bin/openssl << EOM
engine dynamic -pre SO_PATH:/Library/OpenSC/lib/engines/engine_pkcs11.so  -pre ID:pkcs11 -pre LIST_ADD:1 -pre LOAD -pre MODULE_PATH:opensc-pkcs11.so
req -engine pkcs11 -batch -subj "/CN=moi" -new -key slot_$SLOT-id_$KID -keyform engine -x509 -out cert.pem -text
EOM

在设备上创建&签名请求。

签名:

${OPENSSL} << EOM || exit 1
engine -vvvv dynamic 
    -pre SO_PATH:/Library/OpenSC/lib/engines/engine_pkcs11.so  
    -pre ID:pkcs11 
    -pre LIST_ADD:1 
    -pre LOAD 
    -pre MODULE_PATH:$PKCS 
    -pre PIN:$PIN  
    -pre VERBOSE 
x509 -engine pkcs11 -req -in req.csr -out signed.pem 
    -CAfile $CA 
    -keyform engine -key $SLOT:$CAKID 
    -cert $CAKID.pem

请注意,对于后者,未打补丁的openssl不允许引用卡上的证书(只允许引用密钥)。所以我们需要先将其提取并存储为文件。

和使用卡上的密钥将客户端认证连接到服务器:

${OPENSSL} << EOM || exit 1
engine -vvvv dynamic 
    -pre SO_PATH:/Library/OpenSC/lib/engines/engine_pkcs11.so  
    -pre ID:pkcs11 
    -pre LIST_ADD:1 
    -pre LOAD 
    -pre MODULE_PATH:$PKCS 
    -pre PIN:$PIN  
    -pre VERBOSE 
s_client -engine pkcs11 -connect localhost:1443 
    -CAfile $CA 
    -keyform engine -key $SLOT:$KID 
    -cert $KID.pem
EOM

转换为本机代码相当简单——只需在openssl apps.c文件中的apps.c中搜索engine即可——看看这是如何完成的。大多数情况下是一些

pkey = ENGINE_load_private_key(e, file,

而不是原来的

BIO_read_filename(key,file)
pkey=d2i_PrivateKey_bio(key, NULL);

有两个选项:

  1. PKCS # 11。几乎每个USB加密令牌和智能卡供应商都为pkcs# 11提供了一个驱动程序DLL,您可以调用它。我需要指出的是,pkcs# 11接口规范相当松散,这导致了不同供应商之间的奇怪和不兼容。例如,您可能需要在一个设备中使用一组证书属性,而在另一个设备中使用不同的属性集。
  2. CryptoAPI。大多数供应商提供了一个CryptoAPI模块(CSP),它将证书"映射"到Windows证书存储中,您可以使用它来进行签名,就像在Windows证书存储中使用任何证书一样。这意味着在Windows API中使用各种Crypt*, Cert*和类似的函数。

我不认为OpenSSL可以用于你的任务-你需要使用CryptoAPI或pkcs# 11。

我们的SecureBlackbox产品提供了一个统一的高级接口,用于根据各种加密标准和使用pkcs# 11和/或CryptoAPI签署数据。仍然在pkcs# 11的情况下,您(或执行签名的系统的操作人员)需要知道pkcs# 11驱动程序DLL的路径。SecureBlackbox可以在c++中使用它的库版本。