当前位置: 首页 > article >正文

ASN.1 轻松入门2

REDISANT 提供互联网与物联网开发测试套件 #

互联网与中间件:

  • Redis Assistant
  • ZooKeeper Assistant
  • Kafka Assistant
  • RocketMQ Assistant
  • RabbitMQ Assistant
  • Pulsar Assistant
  • HBase Assistant
  • NoSql Assistant
  • Etcd Assistant
  • Garnet Assistant

工业与物联网:

  • MQTT Assistant
  • Modbus 从站模拟器
  • Modbus 主站模拟器
  • IEC104/101 主站/客户端 模拟器
  • IEC104/101 从站/服务端 模拟器
  • IEC61850 客户端模拟器
  • IEC61850 服务端模拟器
  • DL/T 645 主站模拟器
  • CJ/T 188 主站模拟器
  • BACnet 浏览与模拟器
  • OPCUA 客户端模拟器
  • COMTRADE 图表浏览

其他语法

以 -- 开头的文字是注释。 SEQUENCE 和 SET 类型的字段标注 OPTIONAL 表示可以省略,或者标注 DEFAULT foo 表示省略后的默认值为 foo。 具有长度的类型(字符串、OCTET STRING、BIT STRING、SET OF、SEQUENCE OF)可以通过 SIZE 参数限定长度必须在某一范围内或恰为某个数。

类型定义结尾可以使用花括号限定该类型只能取某些值。 例如此处定义 Version 字段只能取三个值中的一个,并对这三个值分别赋予了含义:

Version ::= INTEGER { v1(0), v2(1), v3(2) }

这种方法也常用于标识 OID 的名称。注意此时没有用逗号,因此表示的是一个整体,而非可供选择的多个值。 以 RFC 5280 为例:

id-pkix  OBJECT IDENTIFIER  ::=
         { iso(1) identified-organization(3) dod(6) internet(1)
                    security(5) mechanisms(5) pkix(7) }

接下来还会出现 [数字]、IMPLICIT、EXPLICIT、UNIVERSAL、APPLICATION 等语法要素, 这些语法关系到编码细节,我们将在下文探讨。

编码

ASN.1 有很多种编码:BER、DER、PER、XER 等等。 BER (Basic Encoding Rules) 是一种很灵活的编码, DER (Distinguished Encoding Rules) 则在 BER 的基础上加入了规范化的规则,从而保证同样的信息编码方式也是唯一的。 PER (Packed Encoding Rules) 编码更为紧凑,适合对存储空间或传输速度要求较高的场景。 XER (XML Encoding Rules) 则在需要使用 XML 的情况下能够派上用场。

HTTPS 证书通常采用 DER 编码。 用 BER 也是可以的,但是数字签名必须根据 DER 编码计算,和证书的实际编码方式无关,所以用 BER 相当于自讨苦吃。 本文以介绍 BER 为主,并随之说明 DER 在其基础上施加的额外规定。

阅读本章时建议在另一窗口打开这个实际证书解码示例,方便对照。

类型–长度–数据

与 Protocol Buffers 和 Thrift 一样,BER 的编码形式称为“类型–长度–数据”(type–length–value,常缩写为 TLV)。 也就是说,如果按顺序读取 BER 编码的每个字节,首先读取到的是类型信息,在 ASN.1 中称为“标签”。 标签可以有一个或多个字节,表示存储的是哪种数据,例如 INTEGER、UTF8String 等等。

类型长度数据
020301 00 01

接下来出现的是长度,表示数据究竟有多少个字节。 再接下来自然就是数据本身了。 例如,以十六进制表示的字节 02 03 01 00 01 表示一个 INTEGER(02 就是 INTEGER 类型对应的标签),长度是 03,后面的 3 个字节 01 00 01 则是数据。

与类型–长度–数据的编码方式不同,JSON、CSV、XML 等格式通过分隔符实现编码,也就是事先并不知道数据的长度,出现特定的分隔符(比如 JSON 中的 } 和 XML 中的 </some-tag>)便表示数据结束。

标签

标签通常只有一个字节。 如果使用多个字节还可以编码任意大小的标签,但通常没有这个必要。

以下是一些常见的标签:

标签(十进制)标签(十六进制)对应类型
202INTEGER
303BIT STRING
404OCTET STRING
505NULL
606OBJECT IDENTIFIER
120CUTF8String
1610(和 30)*SEQUENCE 和 SEQUENCE OF
1711(和 31)*SET 和 SET OF
1913PrintableString
2216IA5String
2317UTCTime
2418GeneralizedTime

这些标签都属于“通用”标签(universal tags),由 ASN.1 核心规范定义,在所有 ASN.1 模块中都有着相同的含义。此外还有一些无关紧要的通用标签,这里略去不表。

这些标签的值都小于 31(0x1F),其实是有原因的。第 8、7、6 位(也就是标签字节的最高三位)有着特殊含义,所以大于 31 的通用标签只能用多字节标签表示。 有一小部分通用标签的值大于 31,但是数量很少。

标 * 的两个标签在编码中一定是 0x30 和 0x31,因为第 6 位表示该字段是单一字段还是复合字段。 这两种类型必然是复合字段,所以第 6 位只能是 1。 详见单一字段与复合字段的说明。

标签类别

通用标签把“好用”的数字都用光了,但这并不妨碍我们定义自己的标签。 除了通用标签外还有三种标签:程序内部标签、特定语境标签和私有标签。 标签类别可通过第 7 位和第 8 位区分:

类别第 8 位第 7 位
通用(Universal)00
程序内部(Application)01
特定语境(Context-specific)10
私有(Private)11

标准规范中使用的大多是通用标签,因为通用标签已经涵盖了所有常用数据结构。 例如,证书序列号就是用朴实无华的 INTEGER 类型编码的,标签值为 0x02。 但有时标准中也需要定义特定语境标签来区分 SET 和 SEQUENCE 中的可省略元素,或者区分 CHOICE 中同类型的选项。 以下述定义为例:

Point ::= SEQUENCE {
  x INTEGER OPTIONAL,
  y INTEGER OPTIONAL
}

OPTIONAL 字段如果省略,在编码中就完全不存在,这样一来只有 x 坐标和只有 y 坐标的 Point 就无法区分。 例如,一个只声明 x 坐标为 9 的 Point 编码如下(30 是 SEQUENCE 的标签):

30 03 02 01 09 

这是一个长度为 3(字节)的 SEQUENCE,包含一个长度为 1 的 INTEGER 元素,且其数值为 9。 但一个 y 坐标为 9 的 Point 也得这么编码,于是就出现了歧义。

编码指令

为避免出现歧义,标准规范需要通过编码指令为每个字段分配互不相同的标签。 既然通用标签不能随意改变含义,我们只好改用其他类别,比如程序内部标签:

Point ::= SEQUENCE {
  x [APPLICATION 0] INTEGER OPTIONAL,
  y [APPLICATION 1] INTEGER OPTIONAL
}

不过这种情形下最常用的还是特定语境标签,表示方法就是方括号内只有一个数字:

Point ::= SEQUENCE {
  x [0] INTEGER OPTIONAL,
  y [1] INTEGER OPTIONAL
}

这样一来,只声明 x 坐标为 9 的 Point 编码时便不再用 INTEGER 的通用标签,而是将标签的第 8 和第 7 位分别改为 1 和 0,表示特定语境标签,其他低位设为 0,得到以下编码:

30 03 80 01 09 

y 坐标为 9 的 Point 编码方式如出一辙,只不过低位要设为 1:

30 03 81 01 09 

x 和 y 坐标都为 9 的 Point 则可以编码如下:

30 06 80 01 09 81 01 09 

长度

类型–长度–数据中的长度指的一定是数据的字节数,数据中嵌套的所有字段也包含在内。 因此,只含有一个元素的 SEQUENCE 长度也不是 1,而是该元素编码后有多少个字节。

长度也有两种编码方式:短编码和长编码。 短编码就是一个字节,取值范围是 0 到 127。

长编码则至少有两个字节。第一个字节的第 8 位必须为 1, 其余 7 位表示这个长度字段还有几个字节。 接下来就是一个多字节整数,给出了长度的具体数值。

可想而知,这样得到的长度值可以非常大。 长度最大时第一个字节是 254(255 是保留值,供将来扩展使用),表示这个长度字段内还有足足 126 个字节。 如果这 126 个字节都是 255,那么实际数据的长度会达到 21008−1 字节(超过 10294 GB)。

此外,同一长度值的长编码并不唯一,比如一个字节的数字可以拿两个字节表示,短编码就能表示的数字也可以用长编码。 所以 DER 规定必须采用最短的编码方式。

安全警示:不要轻信长度字段的值! 举例来说,待解码的数据流究竟有没有该字段显示的那么长还是需要核实的。

不定长编码

在 BER 中,字符串、SEQUENCE、SEQUENCE OF、SET 和 SET OF 类型的字段即使事先不知道长度也可以直接编码,通过数据流输出就可以采用这种方式。 具体方法是将长度字段设为一个字节 0x80,数据中连续存放若干个编码后的字段,最后以两个字节 00 00(可以认为是一个标签和长度都为 0 的字段)结尾。 例如,UTF8String 的不定长编码就是若干个 UTF8String 拼接在一起,然后在末尾加上 00 00

不定长编码还可以任意嵌套。 例如,在 UTF8String 的不定长编码中,待拼接的每一段 UTF8String 本身也可以选用定长或不定长编码。

作为长度的 0x80 字节并不存在歧义,因为它既不是短编码,也不是长编码。 它的第 8 位是 1,按理说应该是长编码,其余 7 位表示还有多少个字节。 但这 7 位都是 0,说明长度要用 0 个字节来表示,这是不允许的。

DER 禁止使用不定长编码, 所以必须使用定长编码,提前写明数据的实际长度。

单一字段与复合字段

标签中第一个字节的第 6 位表示该字段是单一(primitive)字段还是复合(constructed)字段。 单一字段中存储的就是数据本身,比如 UTF8String 的数据就是经过 UTF-8 编码的字符串。 复合字段存储的则是若干其他字段,经过编码后拼接在一起。 例如“不定长编码”一节中提到的不定长 UTF8String 就会有多个 UTF8String 字段(各有标签和长度)编码后连在一起,形成复合字段。 复合字段的长度便是拼接后各字段的总字节数。 复合字段可以采用定长或不定长编码, 而单一字段只能用定长编码,因为其数据中没有其他字段,也就无法表明数据该在哪里结束。

INTEGER、OBJECT IDENTIFIER 和 NULL 类型必须是单一字段, 而 SEQUENCE、SEQUENCE OF、SET 和 SET OF 类型必须是复合字段(因为它们的作用本来就是存放多个元素)。 BIT STRING、OCTET STRING、UTCTime、GeneralizedTime 还有各种字符串类型既可以是单一字段,也可以是复合字段,在 BER 中编码者可以自行决定, 但在 DER 中凡是单一、复合均可的类型都必须用单一字段。

EXPLICIT 与 IMPLICIT

前面提到的 [1][APPLICATION 8] 等编码指令还可以加上 EXPLICIT 或 IMPLICIT 关键字。以 RFC5280 为例:

TBSCertificate  ::=  SEQUENCE  {
     version         [0]  Version DEFAULT v1,
     serialNumber         CertificateSerialNumber,
     signature            AlgorithmIdentifier,
     issuer               Name,
     validity             Validity,
     subject              Name,
     subjectPublicKeyInfo SubjectPublicKeyInfo,
     issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     extensions      [3]  Extensions OPTIONAL
                          -- If present, version MUST be v3 --  }

这两个关键字定义的是标签的编码方式,与标签值是明确写出还是自动分配无关。事实上使用这两个关键字时都必须在方括号中写明标签的值。 IMPLICIT 字段的编码方式与其原始类型相同,只是标签的值和类别由方括号中的内容决定。 EXPLICIT 字段则需要先将数据按原始类型编码,再将编码结果套入另一字段中。 外层字段的标签值和类别再由方括号中的内容决定,而且一定是复合字段。

例如,若在 ASN.1 的编码指令中加上 IMPLICIT:

[5] IMPLICIT UTF8String

那么字符串“hi”会被编码为:

85 02 68 69 

但如果在 ASN.1 的编码指令中加上 EXPLICIT:

[5] EXPLICIT UTF8String

则字符串“hi”会被编码为:

A5 04 0C 02 68 69 

如果 IMPLICIT 和 EXPLICIT 关键字都没有出现,默认编码为 EXPLICIT,除非该模块开头指定了“EXPLICIT TAGS”、“IMPLICIT TAGS”或“AUTOMATIC TAGS”。 例如 RFC 5280 定义了两个模块,一个默认编码为 EXPLICIT,另一个则导入了前一模块,并将默认编码设为 IMPLICIT。 IMPLICIT 编码比 EXPLICIT 编码更短。

AUTOMATIC TAGS 的作用与 IMPLICIT TAGS 相同,只是标签的数值([0][1] 等等)会在必要时自动分配,比如 SEQUENCE 中存在可省略元素的情况。


http://www.kler.cn/a/451956.html

相关文章:

  • Docker怎么关闭容器开机自启,批量好几个容器一起操作?
  • 观察者模式和发布-订阅模式有什么异同?它们在哪些情况下会被使用?
  • 基于kraft部署kafka集群
  • 云手机+YouTube:改变通信世界的划时代技术
  • 深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
  • 今日总结 2024-12-24
  • HarmonyOS NEXT 实战之元服务:静态案例效果(二)
  • 131、sqlserver中使用mybatis中的Page进行分页查询时,SQL成功执行(控制台已打印),Page的Records没值bug1.代码复现:
  • NUCLEO-F446RE测试板验证DS100示波器功能
  • 【视觉惯性SLAM:编译及编译工具】
  • 2024.8 设计可解释的 ML 系统以增强对医疗保健的信任:对提出的负责任的临床医生-AI 协作框架的系统评价
  • wordpress调用指定ID分类下浏览最多的内容
  • 印度软件业的发展能给中国软件行业什么样的启示和借鉴
  • C语言-基因序列转换独热码(one-hot code)
  • 开关电源特点、分类、工作方式
  • 【开源免费】基于SpringBoot+Vue.JS在线宠物用品交易网站(JAVA毕业设计)
  • 网络下载ts流媒体
  • JVM简介—1.Java内存区域
  • VBA实现遍历Excel文件将指定的单元格内容拷贝到当前工作簿
  • whisper.cpp: PC端测试 -- 电脑端部署音频大模型
  • 图像处理-Ch6-彩色图像处理
  • 修改输出资源的名称和路径、自动清空上次打包资源
  • 【C 语言】内存节省机制
  • 深入理解.NET内存回收机制
  • 【论文复现】进行不同视角图像的拼接
  • python如何求欧几里得