linux内核cryto接口的实现以及与openssl的比较

linux内核实现了crypto接口,用于类似IPSec之类要在内核中实现的与操作系统绑定的安全机制,如果不是用于这样的机制,不要使用内核中的crypto接口,总的来说,linux的crypto中最重要的结构体有两个:crypto_tfm和crypto_alg
struct crypto_tfm {
    u32 crt_flags;
    union {
        struct cipher_tfm cipher;
        struct digest_tfm digest;
        struct compress_tfm compress;
    } crt_u; //该联合体指出linux实现了cipher,digest和compress三类算法,每一类中有许多种算法,注意这个联合的每一个都仅仅实现了一大类算法的实现封装,并不是具体的实现。
    struct crypto_alg *__crt_alg; 
    char __crt_ctx[] __attribute__ ((__aligned__));
};
下面这个结构体才是具体算法的实现,上面crypto_tfm中的crt_u中一系列函数都是对下面结构体中cra_u中一系列函数的封装:
struct crypto_alg {
    struct list_head cra_list;
    u32 cra_flags;  //这个flags很重要
    unsigned int cra_blocksize;
    unsigned int cra_ctxsize;
    unsigned int cra_alignmask;
    int cra_priority;
    char cra_name[CRYPTO_MAX_ALG_NAME];
    char cra_driver_name[CRYPTO_MAX_ALG_NAME];
    union {
        struct cipher_alg cipher;
        struct digest_alg digest;
        struct compress_alg compress;
    } cra_u;
    int (*cra_init)(struct crypto_tfm *tfm);
    void (*cra_exit)(struct crypto_tfm *tfm);
    struct module *cra_module;
};
上面的两个结构中都有一个联合体,事实上每个联合体字段都是一个完整的算法实现,而且三个算法类型几乎没有任何共同点可言,对于摘要类算法来说,回调函数是init,update,final等,而对于cipher而言,就是encrypt和decrypt等,linux实际上是利用联合体的性质来将三类算法封装到了一个结构体中。
     crt_u里面的回调函数和cra_u中的回调函数名称几乎一模一样,但是它们的层次不同,crt中的函数实现了一大类算法的运行逻辑,比如cipher中的des中的块应该怎么分割等等,虽然对于摘要算法,sha1或者别的什么的算法逻辑没有什么区别,但是对于cipher来讲就不是这样了,同一种算法可能拥有ecb,cbc,fcb等不同的模式,于是就来了个中间层,这个中间层就是上面的联合体crt_u。
     最后,linux将所有的三大类算法组织成了并列的链表,在注册算法的时候要通过crypto_register_alg将一个crypto_alg结构体注册进内核,就是说摘要和加密算法不做区别,只有在crypto_alloc_tfm的时候才会根据算法的名称和flags来确定这个算法到底是做什么的,判断的依据就是crypto_alg中的cra_name字段和cra_flags字段。
     从上述结构体和代码可以看出,linux对加crypto的算法的组织要远比openssl的简单,这也许是linux内核运行效率决定的吧。下面看一下重要的crypto_alloc_tfm函数,很多工作都在这个函数里面完成,比如初始化函数的执行逻辑,也就是初始化crt_u的一堆函数指针,顺便说一下,如果你把crypto_alg理解成一个静态的结构的话,那么crypto_tfm就是一个动态的容器,这种动静结合的方式在优秀的开源代码中很常见,都是用静态的结构初始化动态的结构,然后再根据一些策略逻辑完成动态结构的初始化,有时候动态结构就是一个用于解除耦合的桥梁,比如一些过程的controler,有的时候它就是一个上下文环境或者说是一个容器,比如命名为xxx_ctx的结构体,对于linux内核的cryptoAPI,crypto_tfm可以说是一个桥梁,也可以说是一个容器,一个controler,具体是什么不重要,重要的是代码灵活了:
struct crypto_tfm *crypto_alloc_tfm(const char *name, u32 flags)
{
    struct crypto_tfm *tfm = NULL;
    struct crypto_alg *alg;
    unsigned int tfm_size;
    alg = crypto_alg_mod_lookup(name);//根据名称来查找算法实现
    tfm_size = sizeof(*tfm) + crypto_ctxsize(alg, flags);
    tfm = kzalloc(tfm_size, GFP_KERNEL);
    tfm->__crt_alg = alg;
    crypto_init_flags(tfm, flags);    
    crypto_init_ops(tfm);
    alg->cra_init(tfm);
...
    return tfm;
}
crypto_init_ops函数完成了具体实现的封装,也就是说它初始化了crypto_tfm结构体中crt_u中的一系列函数指针:
static int crypto_init_ops(struct crypto_tfm *tfm)
{
    switch (crypto_tfm_alg_type(tfm)) {
    case CRYPTO_ALG_TYPE_CIPHER:
        return crypto_init_cipher_ops(tfm);//初始化cipher封装函数,下面例子中以digest来说明,它在这里的调用逻辑和cipher原理是一样的,都是在crypto_init_ops实现具体实现的封装过程。
...
}
下面是使用linux内核中cryptoAPI实现的digest的一个例子的大致过程,流程和openssl的evp系列是差不多的,只是在涉及算法实现的点上有差异:
char *Kern_Digest(const void *data, size_t count,
        unsigned char *md, unsigned int *size, const char *name)
{
    struct crypto_tfm *tfm;
    struct scatterlist sg[1];
        tfm = crypto_alloc_tfm(name, 0);
        sg_init_one(sg, data, count); //此往下,摘要算法的实现已经确定了,接下来只剩下回调函数的调用了
        crypto_digest_init(tfm);
        crypto_digest_update(tfm, sg, 1);
        crypto_digest_final(tfm, md);
    if (size != NULL)
        *size = tfm->cra_digest.dia_digestsize;
        crypto_free_tfm(tfm);
}
crypto_digest_xxx系列函数仅仅是具体实现的封装,以init为例:
static inline void crypto_digest_init(struct crypto_tfm *tfm)
{
    tfm->crt_digest.dit_init(tfm);//dit_init依然是一个封装,它在crypto_init_ops中被初始化,这个dit_YYY系列的封装才是有意义的封装。
}
这种封装解除了调用者和实现者之间的耦合。下面是openssl中的digest的实现过程:
int EVP_Digest(const void *data, size_t count,
        unsigned char *md, unsigned int *size, const EVP_MD *type, ENGINE *impl)
{  //传入的type参数其实只是一个空壳,仅仅包含了nid字段是有效的,对于EVP_MD来说就是type字段,因为别的字段具体是什么还要看engine的实现,所以此处的参数type可以仅仅理解成一个算法id。
    EVP_MD_CTX ctx;
    EVP_MD_CTX_init(&ctx);
    M_EVP_MD_CTX_set_flags(&ctx,EVP_MD_CTX_FLAG_ONESHOT);
    ret=EVP_DigestInit_ex(&ctx, type, impl)
      && EVP_DigestUpdate(&ctx, data, count)
      && EVP_DigestFinal_ex(&ctx, md, size);
    EVP_MD_CTX_cleanup(&ctx);
...
}

在对外呈现的接口上,openssl和linux是一致的,openssl需要一个算法的id,而linux需要一个算法的名称,所不同的是,openssl有engine的支持,这样同一个算法就可以指定不同的实现,engine作为算法名称或者id和算法实现之间的桥梁存在,解除了它们之间的耦合。而linux由于没有engine,那么只要给定了一个算法名称,比如sha1,那么只能得到唯一的实现,虽然内核可能通过增加优先级字段来影响算法实现的被选中率,但是这种做法远没有openssl的engine灵活,但是反过来想一下,内核中的机制都是比较稳定的,不经常变动的,而且linux本身就不提倡你在内核完成一些功能,除非万不得已,因此linux的crypto的设计也算是圆满了。



 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1271919

上一篇:Java 简单操作接口 JDBI


下一篇:带你读《云原生应用开发 Operator原理与实践》第二章 Operator 原理2.2Client-go 原理(十六)