asn1编码格式的解析过程

本文以x509的解析为例说明asn1的编码格式的解析逻辑。x509证书的解析实际上是asn1格式的解析,这里着重说的是asn1的ber编码的解析,总的来讲,asn1格式的解析过程有三个重要的元素,一个是asn1数据本身,一个是openssl的内部数据结构,比如X509_st,还有一个指导asn1数据往内部数据结构填充的结构体,这个过程实际上就是d2i,而反向的过程就是i2d,asn1作为抽象语法标记语言,ber其实只是其一种编码实现,不管什么实现都要体现“抽象数据结构”本身,这种数据结构其实很显然,标记了类型,标记了数据的长度以及数据本身等,每一种标记都是自解释的,因此最完美的解决方案就是将openssl的内部数据结构也规整为前面所说的这种“抽象数据结构”,也即是说,将内部数据结构作为asn1的另一种编码格式,和ber并列的编码格式,这样就很好理解d2i的过程了,同样i2d是一种和d2i并列的转换,这么理解的话,ber编码和内部编码实质上是同一种意思的两种不同表述,以x509为例,如果现在有一个x509的der格式的数字证书,有一个openssl的x509数据结构,以下的两个数据结构就是这个d2i过程的指导结构:
ASN1_ITEM_st表述一个“项”,这个“项”是一种复合结构,templates是一个ASN1_ITEM_st容器,该容器可以容纳一个ASN1_ITEM_st也可以容纳多个ASN1_ITEM_st,多个ASN1_ITEM_st同样以templates为更低一级的容器,实质上在我们的例子中,x509_cinf结构体就是一个ASN1_ITEM_st,其中包含一系列的ASN1_TEMPLATE,这个一会会谈到:
struct ASN1_ITEM_st {
    char itype;            /* The item type, primitive, SEQUENCE, CHOICE or extern */
    long utype;            /* underlying type */
    const ASN1_TEMPLATE *templates;    //用于itype是SEQUENCE的情况,而一个templates又要包含一个ASN1_ITEM,注释0
    long tcount;            /* Number of templates if SEQUENCE or CHOICE */
    const void *funcs;        /* functions that handle this type */
    long size;            //被描述的内部结构体的大小
};
ASN1_TEMPLATE是ASN1_ITEM的内容容器,真正的内容还是ASN1_ITEM,由于ASN1是一个大的嵌套体,所以每个ASN1_ITEM还可以包含别的ASN1_ITEM,这些ITEM通过TEMPLATE进行汇总,也就是说一个TEMPLATE可以容纳很多存在于TEMPLATE中的ITEM:
struct ASN1_TEMPLATE_st {
    unsigned long flags;        //指示一些特殊用途,比如变长结构或者是可选信息等
    long tag;            //asn.1的tag
    unsigned long offset;        //该TEMPLATE在被描述结构体的偏移
    ASN1_ITEM_EXP *item;        //每个TEMPLATE由一个ASN1_ITEM描述,此字段指向该ITEM
};
以下几个#define是几个帮助宏,学过c语言的人都能看懂:
#define ASN1_ITEM_start(itname) /
        const ASN1_ITEM * itname##_it(void) /
        { /
                static const ASN1_ITEM local_it = { /
#define ASN1_ITEM_end(itname) /
        }; /
        return &local_it; /
        }
#define ASN1_SEQUENCE_ref(tname, cb, lck) /
    static const ASN1_AUX tname##_aux = {NULL, ASN1_AFLG_REFCOUNT, offsetof(tname, references), lck, cb, 0}; /
    ASN1_SEQUENCE(tname)
#define ASN1_SEQUENCE(tname) /
    static const ASN1_TEMPLATE tname##_seq_tt[]
ASN1_SEQUENCE_ref其实就是表示一系列的ASN1_TEMPLATE,也就是一个ASN1_TEMPLAT类型的数组,这些数组就是上面的注释0;
ASN1_SEQUENCE_ref(X509, x509_cb, CRYPTO_LOCK_X509) = { //这里仅仅讲x509,而没有说x509_cinf,但是原理一样。注释1 
    ASN1_SIMPLE(X509, cert_info, X509_CINF),
    ASN1_SIMPLE(X509, sig_alg, X509_ALGOR),
    ASN1_SIMPLE(X509, signature, ASN1_BIT_STRING)
} ASN1_SEQUENCE_END_ref(X509, X509)
#define ASN1_SEQUENCE_END_ref(stname, tname) /
    ;/  //上面ASN1_TEMPLATE数组定义的结束,这种用法“真的很奇妙”
    ASN1_ITEM_start(tname) /
        ASN1_ITYPE_SEQUENCE,/
        V_ASN1_SEQUENCE,/
        tname##_seq_tt,/  //这就是那个ASN1_TEMPLATE数组,到此为止,这个数组是已经定义过的,就在注释1处被定义
        sizeof(tname##_seq_tt) / sizeof(ASN1_TEMPLATE),/
        &tname##_aux,/
        sizeof(stname),/
        #stname /
    ASN1_ITEM_end(tname)
下面的几个宏更能体现出来item和template的意义,openssl中最最难理解的恐怕就是这些宏了,比windows api的宏还可怕,不同的是,windows的api宏主要是为了兼顾兼容性,而openssl的宏主要是为了迎合asn1的简洁结构和实现上的高效,如此复杂的d2i过程在openssl中调用过程竟然没有太深的调用层次,可谓精妙!
#define ASN1_EXP_EX(stname, field, type, tag, ex) /
    ASN1_EX_TYPE(ASN1_TFLG_EXPLICIT | ex, tag, stname, field, type)
#define ASN1_SIMPLE(stname, field, type)     ASN1_EX_TYPE(0,0, stname, field, type)
#define ASN1_EXP_SEQUENCE_OF_OPT(stname, field, type, tag) /
    ASN1_EXP_EX(stname, field, type, tag, ASN1_TFLG_SEQUENCE_OF|ASN1_TFLG_OPTIONAL)
#define ASN1_EXP_EX(stname, field, type, tag, ex) /
    ASN1_EX_TYPE(ASN1_TFLG_EXPLICIT | ex, tag, stname, field, type)
下面的这个宏最终定义了一个ASN1_TEMPLATE,可以看到最后一个字段是ASN1_ITEM_ref(type),实际上ASN1_ITEM_ref也是一个宏,它得到了一个ASN1_ITEM,也就是这个ASN1_TEMPLATE容纳的一个ASN1_ITEM:
#define ASN1_EX_TYPE(flags, tag, stname, field, type) { /
    (flags), (tag), offsetof(stname, field),/
    #field, ASN1_ITEM_ref(type) }
#define ASN1_ITEM_ref(iptr) (&(iptr##_it))
理解了上面的宏和过程之后,接下来看看具体的d2i过程的函数调用过程:
ASN1_VALUE *ASN1_item_d2i(ASN1_VALUE **pval, const unsigned char **in, long len, const ASN1_ITEM *it)
{
    ASN1_TLC c;  //初始化一个context,这个context可以保存中间的状态
    ASN1_VALUE *ptmpval = NULL;
    if (!pval)
        pval = &ptmpval;
    c.valid = 0;
    if (ASN1_item_ex_d2i(pval, in, len, it, -1, 0, 0, &c) > 0)
        return *pval;
    return NULL;
}
见下图:
asn1编码格式的解析过程_第1张图片
所以说,openssl中的d2i的过程实际上是很简单的,无非就是将具体数据的传输语法在抽象语法的指导下转换为实际语法,上面的例子中只是说明了如何转换为openssl的c语言的内部结构体的过程,但是并不仅仅有这么一种方式,如果你使用的是java,那么应该还有另一套类似的d2i/i2d的代码,转换过程中,i可能有很多,比如c语言的结构体,java语言的类等等,d也可能有很多种(注意仅仅是广义的说,侠义的说,d仅仅指der),不变的2(to)本身,它就是一套指导方案,其实就是asn规范本身,指的就是抽象语法本身,在转换过程中,传输语法和实际语法之间保持高度的一致性,因此出现那么多的递归也就不足为奇了。
     最后看一些openssl中具体代码,这些代码我认为是很值得体会的:
static int asn1_template_noexp_d2i(ASN1_VALUE **val,
                const unsigned char **in, long len,
                const ASN1_TEMPLATE *tt, char opt,
                ASN1_TLC *ctx)
{
    int flags, aclass;
    int ret;
    const unsigned char *p, *q;
    flags = tt->flags;
    aclass = flags & ASN1_TFLG_TAG_CLASS;
    p = *in;
    q = p;
    if (flags & ASN1_TFLG_SK_MASK) {  //如果该项是一个可变大小的数组,比如x509的扩展项
        int sktag, skaclass;
        char sk_eoc;
        ...
        ret = asn1_check_tlen(&len, NULL, NULL, &sk_eoc, NULL,
                    &p, len, sktag, skaclass, opt, ctx);
        else if (ret == -1)
            return -1;
        if (!*val)
            *val = (ASN1_VALUE *)sk_new_null();
        ...
        while(len > 0) {
            ASN1_VALUE *skfield;
            q = p;
            skfield = NULL;
            if (!ASN1_item_ex_d2i(&skfield, &p, len,
                        ASN1_ITEM_ptr(tt->item),
                        -1, 0, 0, ctx))
                ...
            len -= p - q;
            if (!sk_push((STACK *)*val, (char *)skfield)) //val本身就是一个STACK_OF类型的数据结构,是一个变长的内存区域,容纳很多可选项
                ...
        }
            ...
    }
    ...
    else {
        ret = ASN1_item_ex_d2i(val, &p, len, ASN1_ITEM_ptr(tt->item),
                            -1, 0, opt, ctx);
        else if (ret == -1)
            return -1;
    }

    *in = p;
    return 1;
}
int ASN1_item_ex_d2i(ASN1_VALUE **pval, const unsigned char **in, long len,
            const ASN1_ITEM *it,
            int tag, int aclass, char opt, ASN1_TLC *ctx)
{
    ...
    switch(it->itype) {
        case ASN1_ITYPE_PRIMITIVE:
        ... //下面的这个asn1_d2i_ex_primitive真正实现了数据的分配和指派
        return asn1_d2i_ex_primitive(pval, in, len, it,....);
        break;
    ...
}
在asn1_check_tlen的调用路径中存在下列代码:
for (i = 0, tt = it->templates; i < it->tcount; i++, tt++)
{
    const ASN1_TEMPLATE *seqtt;
    ASN1_VALUE **pseqval;
    seqtt = asn1_do_adb(pval, tt, 1);
    pseqval = asn1_get_field_ptr(pval, seqtt); //由tt中的offset计算要读取数据在接收结构体里面的偏移
    ...
    else isopt = (char)(seqtt->flags & ASN1_TFLG_OPTIONAL);
    ret = asn1_template_ex_d2i(pseqval, &p, len, seqtt, isopt, ctx);
    ...
    else if (ret == -1) {
        ASN1_template_free(pseqval, seqtt);
        continue;
    }
    len -= p - q;
}
asn1_template_ex_d2i中调用的asn1_check_tlen中可以确定可选项是否存在,形参中ASN1_TLC *ctx是个很重要的参数,它在整个解析过程中起作用,在解析的开始,也就是ASN1_item_d2i中作为局部变量初始化,然后一直到该函数返回后销毁,这个参数在解析过程中存储一些中间状态和临时变量,但凡一些繁琐的冗长的复杂的操作都要有类似的结构体,在linux内核中释放内存时用到的struct scan_control也是类似的结构。在ssl握手的过程中,为了重用消息,或者说为了处理可选消息,用到了struct ssl3_state_st中的tmp字段:
if (s->s3->tmp.message_type == SSL3_MT_SERVER_DONE) { //本次读取的消息和需要读取的不对应,那么暂存起来,下次就不再读取了,而是用这次的。
    s->s3->tmp.reuse_message=1;
    return(1);
}
然后在具体的读取消息函数的最开始:
if (s->s3->tmp.reuse_message) {
    s->s3->tmp.reuse_message=0;
    s->init_msg = s->init_buf->data + 4; //重用了上次读取但是没有使用的消息
    s->init_num = (int)s->s3->tmp.message_size;
    return s->init_num;
}
ASN1_TLC *ctx的用途之一也是这样子的,在asn1_check_tlen中:
if (ctx && ctx->valid) {  //上次读到了,但是没有使用,比如上次是在解析可选项,然而该可选项不存在,于是返回了-1,把读取的结果暂存起来供以后使用(*)
    i = ctx->ret;
    plen = ctx->plen;
    pclass = ctx->pclass;
    ptag = ctx->ptag;
    p += ctx->hdrlen;
} else {
    i = ASN1_get_object(&p, &plen, &ptag, &pclass, len);
    if (ctx) { //保存信息,以便供上面的(*)处使用
...
        ctx->valid = 1;
    }
}
if (exptag >= 0) {
    if ((exptag != ptag) || (expclass != pclass)) {
        if (opt) return -1;
        asn1_tlc_clear(ctx);
        ASN1err(ASN1_F_ASN1_CHECK_TLEN, ASN1_R_WRONG_TAG);
        return 0;
    }
    asn1_tlc_clear(ctx); //清除ctx->valid,代表此次读取的信息被使用了
...
}

你可能感兴趣的:(asn1编码格式的解析过程)