asn1编码格式的解析过程

2023-11-17

本文以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;
}
见下图:
d2i的调用过程
所以说,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,代表此次读取的信息被使用了
...
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

asn1编码格式的解析过程 的相关文章

随机推荐

  • Android 13 - binder阅读(6)- 使用ServiceManager获取服务

    最近事情好多 使用ServiceManager获取服务就暂时先不学习了 不过从之前的学习中我们也大致可以猜到使用ServiceManager获取服务的过程 根据服务名称获取到ServiceManager中服务代理对应的Handle 再到Bi
  • 服务器443端口响应代码,使用443端口远程服务器

    使用443端口远程服务器 内容精选 换一换 代码迁移工具进行代码迁移时 需要调用Linux下的rpm deb等命令才能完成扫描和迁移相关任务 这些命令和逻辑必须在后端Linux运行 IDE插件只支持以Web模式使用root用户安装工具 不支
  • 2022吴恩达机器学习(Deep learning)课程对应笔记20:特征工程&多项式回归

    2022吴恩达机器学习 Deep learning 课程对应笔记20 特征工程 多项式回归 更新时间 2023 03 20 特征工程 特征工程指的是选择特征 选择特征对模型的影响巨大 多项式回归 多项式回归就是 x n x n xn 对于多
  • 前端例程20221011:文本动态组合

    演示 原理 代码
  • MySQL之CRUD及常见面试题讲解

    目录 一 CRUD是什么 二 什么是SQL注入 三 行转列的使用 四 CRUD中常用关键词 关键词 GROUP BY HAVING ORDER BY 五 聚合函数和连表查询 聚合函数 连表查询 六 DELETE TRUNCATE DROP的
  • 交叉编译libxcb与X11

    交叉编译libxcb与X11 编译前提 工具安装 编译X11 编译依赖项Xtrans 编译依赖项xorgproto XCB完成后编译X11 X11完成 编译xcb 编译依赖项xcb proto 编译依赖项libXau 最后编译xcb XCB
  • (附源码)springboot学生宿舍管理系统 毕业设计453155

    Springboot学生宿舍管理系统 摘 要 科技进步的飞速发展引起人们日常生活的巨大变化 电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用 信息时代的到来已成为不可阻挡的时尚潮流 人类发展的历史正进入一个新时代 在
  • RabbitMQ之Work Queues模式

    RabbitMQ之Work Queues模式 本下面的文字代码原来自官网 附上链接 RabbitMq 之 Work Queues 看完这篇文章对你绝对有好处 好处一 你可以了解透 Work Queues模式 本文章内容98 以上都是来自官网
  • win服务器启动springboot项目,Spring boot项目部署为windows服务

    用途 使用 Spring Boot 开发的 Web 项目 打包生成了一个 jar 包 部署在 Windows 服务器中 设置为开机启动spring boot 生成的 jar 包 直接用 java jar 运行 但是前提是需要登陆用户 并且注
  • linux经典书籍推荐

    对于linux来说 它的用处很多 它可以是java的基础 又可以构建系统 很多人对它越来越感兴趣 下面分享一些linux经典书籍供大家参考 入门篇 LINUX权威指南 书不错 写的很全面也比较广 涉及的不深 做为入门书籍不错 可以比较全面的
  • ConstraintLayout 属性详解 和Chain的使用

    http blog csdn net zxt0601 article details 72736802
  • Nacos手动注册,Nacos内网IP的解决办法,自动获取外网注册服务

    Nacos内网IP的解决办法 自动获取外网注册服务 废话不说 直接上代码 pom文件
  • Appstore审核被拒-[4. DESIGN: PREAMBLE]

    Appstore审核被拒原文如下 原因是设置里有一个版本信息可以响应点击事件进入一个版本详情页 苹果要求版本更新必须使用iOS版本更新内置更新机制 4 DESIGN PREAMBLE Design Preamble The version
  • 【华为OD机试真题 python】二进制差异数【2022 Q4

    前言 华为OD笔试真题 python 本专栏包含华为OD机试真题 会实时更新收纳网友反馈 为大家更新最新的华为德科OD机试试题 为大家提供学习和练手的题库 订阅本专栏后可私信进交流群哦 题目仅供参考 千万不要照抄 题目描述 二进制差异数 对
  • CK草稿本

    调用流程 获得op ptr ck有个工厂模式 const auto op ptrs ck tensor operation device instance DeviceOperationInstanceFactory
  • leetcode无重复字符的最长字串 python实现

    无重复字符的最长字串是一道字符串处理算法的题目 在日常编程中 处理字符串是常见任务 用Python来实现leetcode这道算法题 该题目会涉及到一个概念 滑动窗口 一 题目描述 给定一个字符串 请你找出其中不含有重复字符的 最长子串 的长
  • mac系统vim无法退出insert模式(ESC无效)

    表现 Vim 进入 Insert 模式以后 按 ESC 无法退出 解决方案 按 ctrl c
  • 电感与磁珠

    电感最重要的公式 它说明了电感的很多特性 比如 电感电流不能突变 电感的储能大小 电感的电流与电压的相位关系 还有电感的阻抗为什么是jwL 电感电流不能突变 电感电流为什么不能突变呢 来看这个公式 U等于负的L乘以di比dt Di比dt是指
  • linux上开发应用程序_如何在Linux上安装软件应用程序

    linux上开发应用程序 如何在Linux上安装应用程序 与许多操作系统一样 该问题不仅有一个答案 应用程序可以来自许多来源 几乎无法计数 每个开发团队都可以以自己认为最佳的方式交付软件 知道如何安装给出的内容是成为操作系统的真正超级用户的
  • asn1编码格式的解析过程

    本文以x509的解析为例说明asn1的编码格式的解析逻辑 x509证书的解析实际上是asn1格式的解析 这里着重说的是asn1的ber编码的解析 总的来讲 asn1格式的解析过程有三个重要的元素 一个是asn1数据本身 一个是openssl