`
xitong
  • 浏览: 6158017 次
文章分类
社区版块
存档分类
最新评论

openssl中关于engine的部分代码分析

 
阅读更多

engine在openssl中是一个重要的概念,它提供了一种重载默认算法的方式,首先看一下engine结构体:

struct engine_st {
const char *id;
const char *name;
const RSA_METHOD *rsa_meth;
...//众多类似的结构体,描述dsa,dh,随机数等方法
ENGINE_CIPHERS_PTR ciphers; //返回对称算法集合或者某一个对称算法
ENGINE_DIGESTS_PTR digests; //摘要算法,形式同对称算法
...
ENGINE_GEN_INT_FUNC_PTR init;
ENGINE_GEN_INT_FUNC_PTR finish;
ENGINE_CTRL_FUNC_PTR ctrl; //最重要也是最不重要的一个函数
...
ENGINE_SSL_CLIENT_CERT_PTR load_ssl_client_cert; //这个回调函数可以实现自定义的证书选择方式
...
CRYPTO_EX_DATA ex_data;
...//链表组织
};
int ENGINE_set_default(ENGINE *e, unsigned int flags) //该函数将一个engine注册成系统默认的engine
{
if((flags & ENGINE_METHOD_CIPHERS) && !ENGINE_set_default_ciphers(e))
return 0;
if((flags & ENGINE_METHOD_DIGESTS) && !ENGINE_set_default_digests(e))
return 0;
if((flags & ENGINE_METHOD_RSA) && !ENGINE_set_default_RSA(e))
return 0;
...
if((flags & ENGINE_METHOD_RAND) && !ENGINE_set_default_RAND(e))
return 0;
return 1;
}
int ENGINE_set_default_ciphers(ENGINE *e)
{
if(e->ciphers) {
const int *nids;
int num_nids = e->ciphers(e, NULL, &nids, 0); //由nids数组返回所有ciphers的算法id,num_nids正如字面意思,是数组的有效数据长度
if(num_nids > 0)
return engine_table_register(&cipher_table,
engine_unregister_all_ciphers, e, nids,
num_nids, 1);
}
return 1;
}
cryptodev的所有实现的对称算法的id结构数组:
static struct {
int id;
int nid;
int ivmax;
int keylen;
} ciphers[] = {
{ CRYPTO_DES_CBC, NID_des_cbc, 8, 8, },
{ CRYPTO_3DES_CBC, NID_des_ede3_cbc, 8, 24, },
{ CRYPTO_AES_CBC, NID_aes_128_cbc, 16, 16, },
{ CRYPTO_BLF_CBC, NID_bf_cbc, 8, 16, },
{ CRYPTO_CAST_CBC, NID_cast5_cbc, 8, 16, },
{ CRYPTO_SKIPJACK_CBC, NID_undef, 0, 0, },
{ 0, NID_undef, 0, 0, },
};
static int cryptodev_engine_ciphers(ENGINE *e, const EVP_CIPHER **cipher, const int **nids, int nid)
{
if (!cipher) //如果没有提供cipher参数,那么该函数的作用是得到算法id数组和有效数据长度
return (cryptodev_usable_ciphers(nids));
switch (nid) { //否则就是根据算法id得到具体的EVP_CIPHER结构体
case NID_des_ede3_cbc:
*cipher = &cryptodev_3des_cbc;
break;
...
default:
*cipher = NULL;
break;
}
return (*cipher != NULL);
}
static int cryptodev_usable_ciphers(const int **nids)
{
return (get_cryptodev_ciphers(nids));
}
static int get_cryptodev_ciphers(const int **cnids)
{
static int nids[CRYPTO_ALGORITHM_MAX];
struct session_op sess;
int fd, i, count = 0;
fd = get_dev_crypto();
memset(&sess, 0, sizeof(sess));
sess.key = (caddr_t)"123456781234567812345678";
//将本engine实现的所有的对称算法的id填入传出参数nids数组中
for (i = 0; ciphers[i].id && count < CRYPTO_ALGORITHM_MAX; i++) {
if (ciphers[i].nid == NID_undef)
continue;
sess.cipher = ciphers[i].id;
sess.keylen = ciphers[i].keylen;
sess.mac = 0;
if (ioctl(fd, CIOCGSESSION, &sess) != -1 &&
ioctl(fd, CIOCFSESSION, &sess.ses) != -1)
nids[count++] = ciphers[i].nid; //赋值操作
}
...
return (count);
}
以上是一个engine的cipher的大致实现,
int engine_table_register(ENGINE_TABLE **table, ENGINE_CLEANUP_CB *cleanup,
ENGINE *e, const int *nids, int num_nids, int setdefault)//engine的算法注册函数
{
int ret = 0, added = 0;
ENGINE_PILE tmplate, *fnd;
CRYPTO_w_lock(CRYPTO_LOCK_ENGINE);
if(!(*table)) //第一次插入内容
added = 1;
if(!int_table_check(table, 1))
goto end;
if(added) //如果该table是第一次使用,那么添加一个清理函数,一个table只有一个清理函数
engine_cleanup_add_first(cleanup);
//循环将一个engine的所有相关的算法全部注册进table的哈希表,一共有num_nids个算法,每一个算法的算法id存在于nids数组当中
while(num_nids--) {
tmplate.nid = *nids;
fnd = lh_retrieve(&(*table)->piles, &tmplate); //首先查找看ENGINE_PILE是否已经存在,见下面的函数分析
if(!fnd) {
fnd = OPENSSL_malloc(sizeof(ENGINE_PILE));
fnd->uptodate = 1;
fnd->nid = *nids;
fnd->sk = sk_ENGINE_new_null(); //初始化engine堆栈空间
fnd->funct = NULL;
lh_insert(&(*table)->piles, fnd); //插入到哈希表,见下面的插入函数
}
(void)sk_ENGINE_delete_ptr(fnd->sk, e); //如果是新添加的,那么该函数什么都不做,否则将原来的engine从堆栈删除
if(!sk_ENGINE_push(fnd->sk, e)) //将engine的实例pushu进找到的或新建立的ENGINE_PILE的engine堆栈中
goto end;
fnd->uptodate = 0;
//下面为设置为默认engine的情况。可以实现的一种情形是对于一个engine,某些table它是默认的,而对于别的table来讲它又不是默认的,这是可能的,因为engine的算法注册是按照table分别注册的。
if(setdefault) {
...//设置引用计数
fnd->funct = e; //设置默认的engine,同时也覆盖了原来的默认engine
fnd->uptodate = 1; //可以使用了
}
nids++; //*(nids+1)为下一个算法的算法id
}
ret = 1;
end:
CRYPTO_w_unlock(CRYPTO_LOCK_ENGINE);
return ret;
}
接下来用一个对称算法的初始化来说明一下openssl中的算法组织是如何被实际使用的:
int EVP_EncryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher,
const unsigned char *key, const unsigned char *iv) {
return EVP_CipherInit(ctx, cipher, key, iv, 1);
//上面的被调函数中return EVP_CipherInit_ex(ctx,cipher,NULL,key,iv,enc)
}
int EVP_CipherInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, ENGINE *impl,
const unsigned char *key, const unsigned char *iv, int enc)
{
...
if (!do_evp_enc_engine(ctx, &cipher, impl)) //由一个算法id定位一个算法实现的结构体
return 0;
ctx->cipher=cipher;
...
return 1;
}
static int do_evp_enc_engine(EVP_CIPHER_CTX *ctx, const EVP_CIPHER **pcipher, ENGINE *impl)
{
if(impl) {
ENGINE_init(impl);
else //如果没有提供engine参数,那么就要取系统默认的engine了
impl = ENGINE_get_cipher_engine((*pcipher)->nid);
if(impl) { //得到impl中算法标示为nid的cipher结构体,就是说得到一个对称算法的实现。
const EVP_CIPHER *c = ENGINE_get_cipher(impl, (*pcipher)->nid);
*pcipher = c; //赋值给新分配的cipher空间或者替换掉已经有的cipher
ctx->engine = impl;//设置本ctx所用的engine
} else
ctx->engine = NULL;
return 1;
}
ENGINE *ENGINE_get_cipher_engine(int nid)
{ //选择合适的engine,最终也就是选择一个算法的合适实现,一个engine可以包含所有的算法的实现,但是每一类算法都有一个table,因此一个engine可以实现所有的table,正如engine结构体所示
return engine_table_select(&cipher_table, nid);
}
ENGINE_PILE就是一个engine堆栈,在engine_table_register中实现engine的push操作,在一个ENGINE_PILE中,堆栈中的所有的engine都实现了该ENGINE_PILE所代表的算法
typedef struct st_engine_pile {
int nid; //所有这些堆栈里的engine所实现的算法id
STACK_OF(ENGINE) *sk; //engine堆栈
ENGINE *funct; //默认的engine
int uptodate; //可用性
} ENGINE_PILE;
由于engine可以实现所有的算法,而算法由table分成了大类。每个table又分成了基于ENGINE_PILE的小类,这些ENGINE_PILE是通过哈希来组织的,实际上每一个table就是一个哈希表,内含一个哈希算法和一个比较算法,哈希算法定位出算法小类的大致位置,比较算法用于精确匹配。可以说一个engine包含了所有的table,一个table含有几大类算法,每一大类算法又包含了几小类算法,一个engine可以有选择的实现部分算法或者全部算法,这是从engine到算法的联系,另外还有一个反向的联系,即从算法到engine的联系,就是说一个算法怎么知道都有哪些engine实现了它呢,这就是ENGINE_PILE中这个堆栈sk的作用,最后的结论是算法和engine是一个一对多的关系,engine和算法也是一个一对多的关系。下面的描述最好配合前面一篇文章提供的图解一起理解,理解那幅图可以得到一个感性认识,下面的代码和文字将这种感性认识上升为理性:
struct st_engine_table{
LHASH piles;
}; /* ENGINE_TABLE */
typedef struct lhash_st {
LHASH_NODE **b; 对应于linux内核中的hlist_head
LHASH_COMP_FN_TYPE comp; //该table的节点的比较算法,用于精确匹配
LHASH_HASH_FN_TYPE hash; //该table中节点的哈希值计算函数,用于哈希匹配
...
} LHASH; //哈希表
typedef struct lhash_node_st {
void *data; //实际上就是ENGINE_PILE
struct lhash_node_st *next;
} LHASH_NODE; //哈希表的节点,对应于linux内核中的hlist_node,openssl并没有区分head和node,一律使用LHASH_NODE
void *lh_retrieve(LHASH *lh, const void *data)
{
unsigned long hash;
LHASH_NODE **rn;
void *ret;
lh->error=0;
rn=getrn(lh,data,&hash); //得到data对应的哈希表节点
if (*rn == NULL) {
lh->num_retrieve_miss++;
return(NULL);
} else {
ret= (*rn)->data;
lh->num_retrieve++;
}
return(ret);
}
void *lh_insert(LHASH *lh, void *data)
{
unsigned long hash;
LHASH_NODE *nn,**rn;
void *ret;
lh->error=0;
if (lh->up_load <= (lh->num_items*LH_LOAD_MULT/lh->num_nodes))
expand(lh); //哈希表的自动增长
rn=getrn(lh,data,&hash); //得到data对应的哈希表节点
if (*rn == NULL) { //如果没有找到,那么就分配一个
nn=(LHASH_NODE *)OPENSSL_malloc(sizeof(LHASH_NODE));
nn->data=data;
nn->next=NULL;
*rn=nn;
ret=NULL;
lh->num_insert++;
lh->num_items++;
} else { //如果已经存在该节点了,那么仅仅替换数据
ret= (*rn)->data;
(*rn)->data=data;
lh->num_replace++;
}
return(ret);
}
static LHASH_NODE **getrn(LHASH *lh, const void *data, unsigned long *rhash)
{
LHASH_NODE **ret,*n1;
unsigned long hash,nn;
LHASH_COMP_FN_TYPE cf;
hash=(*(lh->hash))(data); //计算哈希值第一步
lh->num_hash_calls++;
*rhash=hash;
nn=hash%lh->pmax; //计算哈希值第二步
if (nn < lh->p)
nn=hash%lh->num_alloc_nodes;
cf=lh->comp;
ret= &(lh->b[(int)nn]); //根据哈希值定位到一条冲突链表,大致定位
for (n1= *ret; n1 != NULL; n1=n1->next) {
lh->num_comp_calls++;
if(cf(n1->data,data) == 0) //按照此哈希表的比较函数进行精确比较
break;
ret= &(n1->next);
}
return(ret);
}
在调用EVP_EncryptInit的时候,也许仅仅知道一个算法的id,我们的目的是根据这个算法id找到一个该算法的实现,利用上面分析的函数,我们来看看这个过程:
ENGINE *engine_table_select(ENGINE_TABLE **table, int nid) //根据一个算法id和一个算法的大类
{
ENGINE *ret = NULL;
ENGINE_PILE tmplate, *fnd=NULL;
...
tmplate.nid = nid;
fnd = lh_retrieve(&(*table)->piles, &tmplate); //首先找到一个堆栈,优先选择默认实现
if(!fnd) goto end;
...
if(fnd->uptodate) {
ret = fnd->funct; //取得默认实现
goto end;
}
trynext:
ret = sk_ENGINE_value(fnd->sk, loop++);
...//尽可能的在这个engine堆栈的上面选择,如果不行的话再继续找下面的。
goto trynext;
end:
...
return ret;
}
找到了engine之后,最终我们要取得该engine的实现,对于对称算法,由于种类繁多,openssl在engine结构体中提供了ciphers回调函数,现在该调用了,回调函数的封装函数是ENGINE_get_cipher:
const EVP_CIPHER *ENGINE_get_cipher(ENGINE *e, int nid)
{
const EVP_CIPHER *ret;
ENGINE_CIPHERS_PTR fn = ENGINE_get_ciphers(e); //返回e->ciphers
//fn就是engine的ciphers参数,就是一个回调函数,如果最后一个nid参数为0,那么就会返回一个数组,数组的值就是所有cipher的算法ID,以传出参数返回,但是如果最后一个参数不是0,那么就返回该engine所实现的ciphers中算法ID为参数所传入的值对应的EVP_CIPHER结构体。
fn(e, &ret, NULL, nid);
return ret;
}
由上面的流程看见,EVP_系列函数都是取的默认实现,因为它们仅仅传入算法的id(封装在结构体中),engine为NULL,这样就找到了默认的engine。需要指出的是,这里仅仅可能的是默认engine的覆盖,而不能将原来的engine删除,如果你在do_evp_enc_engine中指定了engine,那么cipher返回的就是此engine对应于算法id的cipher结构体,engine的所谓重载仅仅是对于取默认算法的逻辑有意义。下面看一个非对称算法RSA的结构体的确定与初始化:
struct rsa_meth_st {
const char *name;
//一系列rsa算法中的具体步骤实现
int (*rsa_pub_enc) //公钥加密
int (*rsa_pub_dec) //公钥解密
int (*rsa_priv_enc) //私钥加密
int (*rsa_priv_dec) //私钥解密
};
RSA *RSA_new_method(ENGINE *engine)
{
RSA *ret = (RSA *)OPENSSL_malloc(sizeof(RSA));
...
//首先用默认的method初始化ret->meth,仅在#define OPENSSL_NO_ENGINE下作用明显
ret->meth = RSA_get_default_method();
if (engine) { //如果传入了engine,则需要做的仅仅是为ret->engine赋值
...
ret->engine = engine;
} else //否则就要和cipher结构体的定位一样取rsa算法id对应的默认engine作为engine
ret->engine = ENGINE_get_default_RSA();
if(ret->engine) { //如果engine取的是rsa算法的自定义成默认的engine,那么下面的赋值就可以实现rsa方法的重载。
ret->meth = ENGINE_get_RSA(ret->engine); //返回ret->engine->rsa_meth,如果rsa的engine字段的meth字段存在,那么优先使用这个method
...
}
return(ret);
}
int ENGINE_set_default_RSA(ENGINE *e)
{
if(e->rsa_meth) //只有在该engine提供method的时候才会注册,否则没有意义,目的就是在ENGINE_get_default_RSA可以找到默认engine,如果没有提供重载的method,那么即使找到了engine又有什么用呢?
return engine_table_register(&rsa_table,
engine_unregister_all_RSA, e, &dummy_nid, 1, 1);
return 1;
}
ENGINE *ENGINE_get_default_RSA(void)
{
return engine_table_select(&rsa_table, dummy_nid);
}
struct rsa_st {
...
const RSA_METHOD *meth;
ENGINE *engine;
...
}
可以看到很多的非对称算法的注册都是用的dummy_nid作为算法id的,就是说它们根本没有用到和ciphers相同的那一套机制,而是使用了一套更为简单的机制,那就是rsa结构体中存在的meth字段和engine字段,这就更加方便了重载非对称算法,这里的现实意义就是上面分析的openssl中的以table-engine为组织形式的机制在非对称算法中并没有发挥多大的意义,几乎所有的算法将dummy_nid作为算法id这件事表明根本没有必要使用哈希来组织算法,table-engine的组织在非对称算法中仅仅发挥找到默认engine的作用,而这个寻找的代价又过于高价了。
engine中有一个load_ssl_client_cert回调函数,它允许engine的作者自定义证书的选择方式,为何要选择证书呢?考虑一下在ssl握手过程中,如果服务器要求客户端提供证书,那么客户端到底提供哪个证书呢?这就需要选择和裁决了,engine可以把选择和裁决逻辑封装于函数内部,就是说engine自己完全知道怎么选择证书,另外还可以和用户程序互动,让用户选择一个证书,这对于逻辑复杂的程序或者使用GUI的程序是十分方便的,比如可以谈出一个选择框,然后让用户去选择一张证书,对于windows操作系统,还可以重载该函数从而实现从注册表加载证书而不必在使用证书前必须将之导出到文件,下面是一个MSDN上的例子:
int _tmain(int argc, _TCHAR* argv[])
{
HCERTSTORE hCertStore = NULL;
PCCERT_CONTEXT pCertContext = NULL;
TCHAR * pszStoreName = "MY";
hCertStore = CertOpenSystemStore(NULL, pszStoreName);
pCertContext = CryptUIDlgSelectCertificateFromStore(
hCertStore,
NULL,
NULL,
NULL,
CRYPTUI_SELECT_LOCATION_COLUMN,
0,
NULL);
CertFreeCertificateContext(pCertContext);
...
}
于是出现一个选择证书的对话框。在HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/SystemCertificates中包含有一些证书。可以将上述的代码拷贝到基于windows的openssl的应用中,然后实现一个engine,在其load_ssl_client_cert回调函数中调用上述代码,得到一个证书的索引或者别的什么信息。
最后ENGINE_CTRL_FUNC_PTR类型的一个control函数是最有意思的,它类似于linux的ioctl函数,在linux中ioctl可以被任意的使用,作为用户空间和内核空间的一条超级通道,它几乎可以做任何事情,前提是你必须在内核实现ioctl,实现是很简单的,几乎不会有任何拦路虎进行参数检查,但是有个问题,最终ioctl无限膨胀,最终不可收场,实际上ioctl基本是提供给驱动程序进行io控制的,正如其名称所说,而不是作为用户和内核的通道被使用的,但是由于其方便和易用性被认为是一个超级通道了,这里的engine中的ctrl也有一样的效果:
typedef int (*ENGINE_CTRL_FUNC_PTR)(ENGINE *, int, long, void *, void (*f)(void));
最后的void *, void (*f)(void)两个参数几乎能做任何事情,几乎没有任何约束,没有什么阻碍你在里面实现一个和openssl毫不相关的逻辑。于是这个ctrl极端的灵活,极端的易用,但是又极端的难以用好用妙,一旦用不好,代码会无限臃肿下去,最后导致不可收场或者还有你的崩溃(其实我已经崩溃了)。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics