ASN.1 轻松入门2

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

工业与物联网:

 

其他语法

以 -- 开头的文字是注释。 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 等等。

类型长度数据
02 03 01 00 01

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

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

标签

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

以下是一些常见的标签:

标签(十进制)标签(十六进制)对应类型
2 02 INTEGER
3 03 BIT STRING
4 04 OCTET STRING
5 05 NULL
6 06 OBJECT IDENTIFIER
12 0C UTF8String
16 10(和 30)* SEQUENCE 和 SEQUENCE OF
17 11(和 31)* SET 和 SET OF
19 13 PrintableString
22 16 IA5String
23 17 UTCTime
24 18 GeneralizedTime

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

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

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

标签类别

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

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

标准规范中使用的大多是通用标签,因为通用标签已经涵盖了所有常用数据结构。 例如,证书序列号就是用朴实无华的 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 中存在可省略元素的情况。

posted @   serene1312  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示