1.1 What to do
通过Openssl和PKCS#11接口,使用USBKEY中的私钥和证书来签发一个下级证书。
1.2 背景
数字证书颁发过程一般为:用户首先产生自己的密钥对,并将公共密钥及部分个人身份信息传送给认证中心。认证中心在核实身份后,将执行一些必要的步骤,以确信请求确实由用户发送而来,然后,认证中心将发给用户一个数字证书,该证书内包含用户的个人信息和他的公钥信息,同时还附有认证中心的签名信息。
一个标准的X.509数字证书包含以下一些内容:
证书的版本信息;
证书的序列号,每个证书都有一个唯一的证书序列号;
证书所使用的签名算法;
证书的发行机构名称,命名规则一般采用X.500格式;
证书的有效期,现在通用的证书一般采用UTC时间格式,它的计时范围为1950-2049;
证书所有人的名称,命名规则一般采用X.500格式;
证书所有人的公开密钥;
证书发行者对证书的签名。
简而言之,CA从PKCS#10证书请求(或者P7格式)中读取用户信息和公钥信息,使用这些信息封装成一个X.509格式(可能是不同版本,比较普遍是V3),此时唯一没有包括的是证书发行者对证书的签名,此时使用CA的私钥进行签名,得到签名值后CA将其填充到X.509相对应的结构中去,一个X.509证书宝宝就此诞生了。
此处唯一不同的是CA的公私钥对和证书都存放在USBKEY中(当然也能存放在加密机或加密卡中),所以将通过USBKEY的PKCS#11接口完成上述操作,而证书相关操作就由Openssl代劳了。
1.3 正题
第一、使用Usbkey向某个CA申请一个证书
通过下面的命令来验证,第一组公私钥对和证书是签名证书,第二组是加密证书。可以很明显地看出他们是通过Csp方式操作整个证书申请过程的。
C:\Program Files\Smart card bundle>pkcs11-tool.exe --module DMPKCS11.dll –O
Certificate Object, type = X.509 cert
label: cert addey by CSP
ID: 37af001ddbd525e640ca3c3f6d78b009741d1f48
Public Key Object; RSA 1024 bits
label: pub key addey by CSP
ID: 37af001ddbd525e640ca3c3f6d78b009741d1f48
Usage: encrypt, verify
Private Key Object; RSA
label: private key addey by CSP
ID: 37af001ddbd525e640ca3c3f6d78b009741d1f48
Usage: decrypt, sign
Certificate Object, type = X.509 cert
label: cert addey by CSP
ID: ab268f4320a426b4a6ce70d757cd11fcd83b8ddd
Public Key Object; RSA 1024 bits
label: pub key addey by CSP
ID: ab268f4320a426b4a6ce70d757cd11fcd83b8ddd
Usage: encrypt, verify
Private Key Object; RSA
label: private key addey by CSP
ID: ab268f4320a426b4a6ce70d757cd11fcd83b8ddd
Usage: decrypt, sign
第二、生成PKCS#11的证书请求
这里直接使用Java程序生成一个证书请求。
import java.io.OutputStreamWriter;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.jce.PKCS10CertificationRequest;
import org.bouncycastle.openssl.PEMWriter;
/**
* Generation of a basic PKCS #10 request.
*/
public class PKCS10CertRequestExample
{
public static PKCS10CertificationRequest generateRequest(
KeyPair pair)
throws Exception
{
return new PKCS10CertificationRequest(
"SHA256withRSA",
new X500Principal("C=CN,ST=上海,L=上海,O=火星,OU=北极,CN=超人"),
pair.getPublic(),
null,
pair.getPrivate());
}
public static void main(
String[] args)
throws Exception
{
// create the keys
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", "BC");
kpGen.initialize(1024, Utils.createFixedRandom());
KeyPair pair = kpGen.generateKeyPair();
PKCS10CertificationRequest request = generateRequest(pair);
PEMWriter pemWrt = new PEMWriter(new OutputStreamWriter(System.out));
pemWrt.writeObject(request);
pemWrt.close();
}
}
证书请求
-----BEGIN CERTIFICATE REQUEST-----
MIIBoDCCAQkCAQAwYjEPMA0GA1UEAwwG6LaF5Lq6MQ8wDQYDVQQLDAbljJfmnoEx
DzANBgNVBAoMBueBq+aYnzEPMA0GA1UEBwwG5LiK5rW3MQ8wDQYDVQQIDAbkuIrm
tbcxCzAJBgNVBAYTAkNOMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCw7iyU
/8p1lCxnJifdqxNYO1cTVg35BBtscQsrtug9Br3Vge/kNX9KC5xOGhdcK1IDjl3d
1CGsRtnb4dEFqtkjKWQ1z5WZxXWoVfkqwP3AJg8y10BhiiDqPPbn3II4o8Nc+bvz
tDm32HbNXcyXWLR5aEJx1FiJYdDmDbRbgGrcawIDAQABMA0GCSqGSIb3DQEBCwUA
A4GBAJSr2pe1LJp++gSWAc7yVufbnYXG3QgzIdoEUhP1I/3LNeqUYyuTaL/fTgAF
oEjTvwOlAVizcve8qiD9/ApY+MtjgRKFDbZYnkC3mRgJTDxV3WzDmdj4YEQGIUVG
O+XRfiWP132n9N3aI6gaJVj2m7Zu56akrE3F2c4kawZL/aIK
-----END CERTIFICATE REQUEST-----
第三、程序签发
1. engine_pkcs11的使用方式
使用openssl调用USBKEY的PKCS#11接口,可以通过OpenSC项目的engine_pkcs11接口。原本使用编写openssl配置文件方式(见[1]),但是就是无法使用,两次调用ListEngine()都无法发现pkcs11 engine的影子。
Openssl.conf 内容:
openssl_conf = openssl_def
[openssl_def]
engines = engine_section
[engine_section]
pkcs11 = pkcs11_section
[pkcs11_section]
engine_id = pkcs11
dynamic_path = "C:\\Program Files\\Smart card bundle\\engine_pkcs11.dll"
MODULE_PATH = C:\\Windows\\System32\\DMPKCS11.dll
init = 0
[req]
distinguished_name = req_distinguished_name
[req_distinguished_name]
可以通过下面命令验证配置文件并没有写错,openssl识别出了pkcs11 engine,并且生成了证书请求。
C:\Program Files\Smart card bundle>openssl req -config openssl.conf -engine pkcs11 -new -key id_37af001ddbd525e640ca3c3f6d78b009741d1f48 -keyform engine -out req.pem -text -x509 -subj "/CN=Andreas Jellinghaus"
engine "pkcs11" set.
PKCS#11 token PIN:
所以最后还是使用动态调用的方式导入pkcs11 engine,即ENGINE_load_dynamic。所以两次调用ListEngine()后发现, dynamic engine导入pkcs11 engine后就会被其替换。
导入前
id: dynamic, name: Dynamic engine loading support
导入后
id: pkcs11, name: pkcs11 engine
2. 导出USBKEY中的CA证书
需要导出CA证书,这是因为CA需要填充X.509格式中的证书的发行机构名称。
通过” LOAD_CERT_CTRL”命令来获取证书,输入的参数为证书的表示。
"slot_0-id_37af001ddbd525e640ca3c3f6d78b009741d1f48"
slot_0 PKCS#11 表示的第一个插槽(一个插槽配一个Token)
id_37af001ddbd525e640ca3c3f6d78b009741d1f48 证书的Id号(同一组公私钥对和证书这个ID是相同的),这个ID可以通过pkcs11-tools获得。
命令返回的parms.cert就指向一个X.509结构的证书。
但是必须要注意的是导出证书前,必须设置过正确的PIN
struct {
const char * cert_id;
X509 * cert;
} parms;
parms.cert_id = "slot_0-id_37af001ddbd525e640ca3c3f6d78b009741d1f48";
parms.cert = NULL;
ENGINE_ctrl_cmd(e, "LOAD_CERT_CTRL", 0, &parms, NULL, 1);
3. 证书请求
通过JAVA生成的证书请求,直接复制粘贴到工程目录下的文本文件certreq.txt即可,并且需要包含BEGIN/END部分。
4. 从证书请求中获取用户信息
//设置证书的主体名称,req就是刚刚生成的请求证书
X509_set_subject_name(m_pClientCert, X509_REQ_get_subject_name(req));
//设置证书的公钥信息
X509_set_pubkey(m_pClientCert, X509_PUBKEY_get(req->req_info->pubkey));
5. 设置证书的签发者信息
//设置证书的签发者信息,m_pCACert是CA证书
X509_set_issuer_name(m_pClientCert, X509_get_subject_name(m_pCACert));
6. 证书签名
注意这里采用的是sha1的摘要算法,当然也可使用MD5
//设置签名值
// EVP_sha1 是否可以设置成别的,如EVP_md5
// 这样一份X509证书就生成了,下面的任务就是对它进行编码保存。
X509_sign(m_pClientCert, m_pCAKey, EVP_sha1());
此处还有些补充的内容,为了验证X509_sign调用PKCS#11接口的情况,自己实现了一个PKCS#11的包装壳(68个导出函数),实现时注意C_GetFunctionList应该指向本包装壳的函数,不然错误的使用实际的C_GetFunctionList作返回结构便也就失去意义了。X509_sign的调用方式还是不同的,java中如果使用SHA1WithRSA传入到PKCS#11接口的C_Sign或者C_SignUpdate的数据是完整的明文,但是X509_sign传入的是一个ASN.1 Sequence的一个结构,结构中包含待签名数据的摘要散列。
举例来说:
待加密的数据是Hello World! ,在C_Sign传入的数据中就可以发现Hello World!的SHA-1的摘要散列。
待加密:Hello World!
SHA-1: 2EF7BDE608CE5404E97D5F042F95F89F1C232871
C_Sign:
30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14 2e f7 bd e6 08 ce 54 04 e9 7d 5f 04 2f 95 f8 9f 1c 23 28 71
使用ASN.1dump来观察就看的更加清楚了。
从X509_sign的实现也可以看的这一点,在RSA_Sign之前首先进行摘要算法,并且这个摘要并不使用PKCS#11中的接口函数,直接使用Openssl自己的摘要算法,所以传入到最后的只是明文摘要散列了。
int X509_sign(X509 *x, EVP_PKEY *pkey, const EVP_MD *md)
{
//先进行ret->cert_info->signature,以及ret->sig_alg的设置;
inl=i2d_X509_CINF(ret->cert_info,NULL);//求出证书编码后的长度
buf_in=(unsigned char *)OPENSSL_malloc((unsigned int)inl);//申请空间
outll=outl=EVP_PKEY_size(pkey1);
buf_outl=(unsigned char *)OPENSSL_malloc((unsigned int)inl);
if ((buf_in == NULL) ││ (buf_outl== NULL))
{
outl=0;
goto err;
}
p=buf_in;//p与buf-in共享一段地址
i2d_X509_CINF(ret->cert_info,&p);//将证书编码存入buf-in
EVP_MD_CTX_init(&ctxl);//初始化
EVP_SignInit(&ctxl,dgst);//将需要使用的摘要算法存入ctxl中
EVP_SignUpdate(&ctxl,(unsigned char *)buf_in,inl);//存入证书的编码值
EVP_DigestFinal(&ctxl,&(m[0]),&m_len);//求取编码的长度为m_len摘要值存入m中
RSA_sign(ctxl->digest->type,m,m_len,buf_out,outl,pkey->pkey.rsa)//求取摘要值的签名值,最后将长度为outl的签名值存入buf-out。
......
7. 最后生成的证书
USBKEY 中包含证书是向三级CA申请的,所以处于第四级,使用第四级证书来签发新证书,” 超人”宝宝就只能到第五级去了(也许是第五项修炼吧)。
其实还是个问题,第四级证书报“此证书似乎对于所选的目的是有效。”,出现此问题的原因嵌入在消息中的签名证书链包含一个无效的交叉引用,估计第四级是一个用户证书,要消除这个感叹号,第四级证书的证书用法中应该包含Digital Signature, Certificate Signing, Off-line CRL Signing, CRL Signing (86)这几项
8.完整代码
// SignWithOpenSSL.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/engine.h>
#include <openssl/conf.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/rsa.h>
#include <openssl/x509v3.h>
#define OPENSSL_LOAD_CONF
#define UC_ENGINE_SOPATH "C:\\Program Files\\Smart card bundle\\engine_pkcs11.dll"
#define UC_EXPECTED_ENGINE_ID "pkcs11"
#define UC_ENGINE_MODULEPATH "C:\\Windows\\System32\\DMPKCS11.dll"
// 列出当前所有的engine
/*
ENGINE *ENGINE_get_first(void);
ENGINE *ENGINE_get_last(void);
ENGINE *ENGINE_get_next(ENGINE *e);
ENGINE *ENGINE_get_prev(ENGINE *e);
*/
void ListEngine()
{
ENGINE *current;
current = ENGINE_get_first();
if( NULL != current )
{
printf("id: %s, name: %s\n",
ENGINE_get_id(current),
ENGINE_get_name(current));
while( NULL != (current = ENGINE_get_next(current)))
{
printf("id: %s, name: %s\n",
ENGINE_get_id(current),
ENGINE_get_name(current));
}
}
}
/*存储证书*/
int save_cert(X509 *pCert, char *pCertFile)
{
BIO *pbio;
if(NULL == pCert || NULL == pCertFile)
{
return -1;
}
pbio = BIO_new_file(pCertFile, "w");
if(NULL == pbio)
{
return -1;
}
if(!i2d_X509_bio(pbio, pCert))
{
printf("save_cert:call PEM_write_bio_X509 error ");
return -1;
}
printf("Bingo, New Cert is borned\n");
BIO_free(pbio);
return 0;
}
void add_subject_entity(X509_NAME *pSubjectName, char *key, char *value)
{
int nid;
X509_NAME_ENTRY *ent;
if( (nid =OBJ_txt2nid(key)) == NID_undef )
{
printf(" add_subject_entity:concert nid error");
return ;
}
ent = X509_NAME_ENTRY_create_by_NID( NULL, nid, MBSTRING_UTF8,
(unsigned char*)value, -1);
if(ent == NULL)
{
printf("add_subject_entity:create ent error");
return;
}
if(X509_NAME_add_entry(pSubjectName, ent, -1, 0) != 1)
{
printf("add_subject_entity:add to subjectname error");
return;
}
return;
}
int CreateX509Cert(X509 *m_pCACert, EVP_PKEY *m_pCAKey)
{
// 读取证书请求
BIO *in;
X509_REQ *req=NULL,**req2=NULL;
in = BIO_new_file("certreq.txt","r");
req = PEM_read_bio_X509_REQ(in,NULL,NULL,NULL);
if( req == NULL )
{
printf("DER Decode Error!\n");
}
else
{
printf("DER Decode Success!\n");
}
// 使用usbkey中的私钥进行签名
X509 *m_pClientCert;
m_pClientCert = X509_new();
//设置版本号
X509_set_version(m_pClientCert, 2);
//设置证书序列号,这个sn就是CA中心颁发的第N份证书
ASN1_INTEGER_set(X509_get_serialNumber(m_pClientCert),100);
//设置证书开始时间
X509_gmtime_adj(X509_get_notBefore(m_pClientCert),0);
//设置证书结束时间
X509_gmtime_adj(X509_get_notAfter(m_pClientCert), (long)60*60*24);
//设置证书的主体名称,req就是刚刚生成的请求证书
X509_set_subject_name(m_pClientCert, X509_REQ_get_subject_name(req));
//设置证书的公钥信息
X509_set_pubkey(m_pClientCert, X509_PUBKEY_get(req->req_info->pubkey));
//设置证书的签发者信息,m_pCACert是CA证书
X509_set_issuer_name(m_pClientCert, X509_get_subject_name(m_pCACert));
//设置扩展项目
X509V3_CTX ctx;
X509V3_set_ctx(&ctx, m_pCACert, m_pClientCert, NULL, NULL, 0);
X509_EXTENSION *x509_ext = X509_EXTENSION_new();
x509_ext = X509V3_EXT_conf(NULL, &ctx, "HELLO", "HELLO");
X509_add_ext(m_pClientCert,x509_ext,-1);
//设置签名值
// EVP_sha1 是否可以设置成别的,如EVP_md5
// 这样一份X509证书就生成了,下面的任务就是对它进行编码保存。
X509_sign(m_pClientCert, m_pCAKey, EVP_sha1());
// 输出证书
save_cert(m_pClientCert, "d:\\test.cer");
return 0;
}
int main(int argc, CHAR* argv[])
{
ENGINE *e;
const char *engine_id = "pkcs11";
const char *key_id = "37af001ddbd525e640ca3c3f6d78b009741d1f48";
UI_METHOD *ui_method = NULL;
EVP_PKEY *priv_key;
void *cb_data;
const char *config_name = NULL;
BIO *bio_err=NULL;
/* Load the config file */
//OPENSSL_config(config_name); // 不使用Openssl0.9.8e的配置文件来导入PKCS11
ENGINE_load_dynamic();
ListEngine();
printf("\nLoading Dynamic...\n");
/* Register engine */
printf("Registering enginen");
e = ENGINE_by_id("dynamic");
if(!e) {
/* the engine isn't available */
printf("The engine isn't available\n");
return 0;
}
//int ENGINE_ctrl(ENGINE *e, int cmd, long i, void *p, void (*f)(void));
ENGINE_ctrl(e, ENGINE_CTRL_SET_LOGSTREAM, 0, bio_err, 0);
// 设置engine_pkcs11的路径
ENGINE_ctrl_cmd_string(e, "SO_PATH", UC_ENGINE_SOPATH, 0);
ENGINE_ctrl_cmd_string(e, "ID", UC_EXPECTED_ENGINE_ID, 0);
ENGINE_ctrl_cmd_string(e, "LIST_ADD", "1", 0);
ENGINE_ctrl_cmd_string(e, "LOAD", NULL, 0);
// 设置USBKEY厂商PKCS#11实现的路径
ENGINE_ctrl_cmd_string(e, "MODULE_PATH", UC_ENGINE_MODULEPATH, 0);
// 设置PIN码
if(!ENGINE_ctrl_cmd_string(e, "PIN", "111111", 0)){
printf("Error sending PIN to = engine");
ENGINE_free(e);
return 0;
}
ListEngine();
if(!ENGINE_init(e)) {
/* the engine couldn't initialise, release 'e' */
printf("The engine couldn't initialise\n");
ENGINE_free(e);
return 0;
}
if(!ENGINE_register_RSA(e)){
/* This should only happen when 'e' can't initialise, but the previous
* statement suggests it did. */
printf("This should not happen\n");
abort();
}
// 直接从usb-key中导入证书,但必须初始化后。
struct {
const char * cert_id;
X509 * cert;
} parms;
parms.cert_id = "slot_0-id_37af001ddbd525e640ca3c3f6d78b009741d1f48";
parms.cert = NULL;
ENGINE_ctrl_cmd(e, "LOAD_CERT_CTRL", 0, &parms, NULL, 1);
// Load private key
printf("Loading private key\n");
priv_key = ENGINE_load_private_key(e, key_id, ui_method, &cb_data);
// 产生证书
CreateX509Cert(parms.cert, priv_key);
// Release the functional reference from ENGINE_init()
ENGINE_finish(e);
// Release the structural reference from ENGINE_by_id()
ENGINE_free(e);
return 0;
}
1.4 参考
[1] DNSSEC Signers and OpenSSL
[END]