1.什么是ASN.1(抽象语法标记1)?
ASN.1(Abstract Syntax Notation One)是ITU-T的一个标准集,它用来编码及表示通用数据类型,这些数据类型有可打印串值,八位位组串值,位串值,整数值以及用可移值方式组合而成的其他类型序列值。简单的说,ASN.1指定了以何种方式对非平凡的数据类型进行编码,以便其他任何平台及第三方工具都能够解释其内容。比如,字母a在一些平台以ASCII编码为十进制数值97,而在其他非ASCII平台上,可能会是另外的编码.而ASN.1指定了一种编码方式,在任何平台上,字母a的编码都是统一的.
2.ASN.1语法.
ASN.1语法遵循传统的巴科斯范式BNF风格.最基本的表达式如:Name ::= type . 表示为定义某个名称为Name的元素,它的类型为type. 例如: MyName ::= IA5String . 表示为定义了一个名为MyName的元素或变量,其类型为ASN.1类型IA5String (类似于ASCII字符串).
2.1 ASN.1显式值(Explict Value).
有些时候,我们需要定义一种ASN.1类型,它的子集元素包含预定义值.Name ::= type (Explict Value) . 显式值(Explict Value)必须是ASN.1类型允许选择的值,而且也必须是元素所允许的值.例: MyName ::= IA5String (Tom) 表示MyName是字符串Tom的IA5String编码.又例如: MyName ::= IA5String(Tom|Joe) 表示字符串的值既可以是Tom, 也可以是Joe.
这种语法的使用是为了扩展确定的解码器.例:
PublicKey ::= SEQUENCE {
KeyType BOOLEAN(0),
Modulus INTEGER,
PubExponent INTEGER
}
PrivateKey ::= SEQUENCE {
KeyType BOOLEAN(1)
Modulus INTEGER,
PubExponent INTEGER,
PrivateExponent INTEGER
}
2.2 ASN.1容器(container)
容器是值是一个包含了其他相同或者不同类型元素的数据类型(例如序列值SEQUENCE或集合值SET类型).目的是为了组合一些复杂的数据类型集.ASN.1规范定义了4种容器类型:序列、单一序列(SEQUENCE OF)、集合和单一集合(SET OF).虽然它们意义不同,但是语法是一样的.
Name ::= Container {Name Type [ Name Type…]} 方括号中的内容和容器的元素个数都是可选项.还可以进行嵌套定义.
例:
UserRecord ::= SEQUENCE {
Name SEQUENCE {
First IA5String,
Last IA5String
},
DoB UTCTIME
}
将其粗略的翻译成C语言中的结构如下:
struct UserRecord {
struct Name {
char *First,
char *Last
};
time_t DoB;
}
将其粗略的翻译成Object Pascal语言中的记录如下(Object Pascal不支持嵌套记录):
Type
Name = record
First : String;
Last : String;
end;
UserRecord = record
aName : Name;
DoB : DateTime;
end;
2.3ASN.1修改器
ASN.1定义了各种修改器,如可选(OPTIONAL),默认(DEFAULT),和选择(CHOICE)。他们可以改变表达式的声明.典型地用于定义一种要求编码灵活,而定义又不繁琐的类型.
<1>.可选(OPTIONAL)。顾名思义,其表示改变一个元素以便在编码时它的类型是可选择的.即编码器可以忽略这个元素,解码器不能假设它将出现.但当邻接的两个元素具有相同的类型时,会给解码器带来一些问题.
定义: Name ::= Type OPTIONAL
例如:
Float ::= SEQUENCE {
Exponent INTEGER OPTIONAL,
Mantissa INTEGER,
Sign BOOLEAN
}
当解码器读取这个结构时,在它看来第一个整数(INTEGER)可能是Exponent,也有可能认为是Mantissa. 一般建议不使用这种方式定义结构.
<2>.默认(DEFAULT).默认修改器允许容器包含默认值.如果待编码的数据值等同于它的默认值,那么它将在发送的数据流中被忽略.例如:
Command ::= SEQUENCE {
Token IA5String(NOP) DEFAULT,
Parameter INTEGER
}
如果编码器把Token看成是代表字符串NOP,那么序列将按照定义的那样编码为:
Command ::= SEQUENCE {
Parameter INTEGER
}
<3>.选择(CHOICE). 选择修改器允许一个元素在给定的实例中可以有多个可能值.实质上说,解码器将尝试所有期望的解码算法,直到有一个类型符合为止.当一个复杂的容器中包含其他容器时,时候选择器就十分有用了.例如:
UserKey ::= SEQUENCE {
Name IA5String,
StartDate UTCTIME,
Expire UTCTIME,
KeyData CHOICE {
ECCKey ECCKeyType,
RSAKey RSAKeyType
}
}
上例简单的允许ECC也允许RSA密钥的公钥证书.
3.ASN.1数据类型
ASN.1针对广泛的应用定义了多种数据类型,我们这里只讨论跟密码学应用相关的数据类型.我们将讨论如下数据类型:
- 布尔型 (Boolean);
- 八位位组串 (OCTET String);
- 位串 (BIT String);
- IA5String;
- 可打印字符串 (PrintableString);
- 整数 (INTEGER);
- 对象标识符 (OBJECT Identifier, OID);
- 世界协调时 (UTCTIME);
- 空 (NULL);
- 序列,单一序列;
- 集合;
- 单一集合;
任何ASN.1编码都是以两个字节开始(或者八位位组,含有8个二进制位),不管什么类型,它们都是通用的.第一个字节是类型标识符,也包含一些修正位;第二各字节是长度.
3.1 ASN.1头字节
头字节(hearder byte)位于ASN.1编码的开始,由3部分组成。如下图:
<1>. 类别位。
类别位(classification bits)由两位表示,用来描述数据将要解释的上下文。
位8 | 位7 | 类别 |
0 | 0 | 通用(Universal) |
0 | 1 | 应用(Application) |
1 | 0 | 上下文特定(Context Specific) |
1 | 1 | 专用(Private) |
所有的类型中,通用类别最常用。
<2>. 结构化位。
结构化位(constructed bit)表示一个给定的编码是否是相同类型的多种编码的结构化。结构化元素是容器类型必需的,因为在逻辑上,它们只是其他元素的集合。
结构化元素有自己的头字节和长度字节,之后是元素各个要素组件的单独编码。也就是说,这些要素组件是独立地可解码ASN.1数据类型。
严格的说,容器类是唯一允许使用结构化位的数据类型。这是因为对于其他数据类型,给定内容,只允许一种编码。所以其他所有数据类型的结构化位都为0。
<3>. 原始类型。
ASN.1头字节的低5位定义了32种ASN.1的原始类型(primitive type)
代码 | ASN.1类型 | 作用 |
1 | 布尔型 | 储存布尔值 |
2 | 整数 | 储存大整数 |
3 | 位串 | 存储位数组 |
4 | 八位位串 | 存储字节数组 |
5 | 空 | 预留位(例如在选择修改器中) |
6 | 对象标识符 | 标识算法及协议 |
16 | 序列和单一序列 | 未分类元素的容器 |
17 | 集合和单一集合 | 已分类元素的容器 |
19 | 可打印字符串 | ASCII编码(忽略一些不可打印字符) |
22 | IA5String | ASCII编码 |
23 | 世界协调时 | 以统一格式表示的时间 |
3.2 ASN.1长度编码
根据编码的实际长度,ASN.1定义了两种长度编码(length encoding)方法,长编码和短编码。
编码字节的最高位代表的是短编码还是长编码;而低7位则形成一个长度立即数。
<1>. 短编码。
在短编码中,负载的长度必须小于128字节。长度立即数用来表示负载的长度。例如,对于一个长度为65 (0x41)的负载进行编码,其长度编码字节只需简单的设置为0x41即可。因为其最高位是0,则编码器可以判断出这是短编码,而且长度是65。
<2>. 长编码。
在长编码中,定义了附加的抽象数据来对长度进行编码,它仅适用于所有长度为128字节或以上的负载。在这种模式下,长度立即数存储的是为了表示负载长度所需的字节数。这个长度必须以big-endian格式进行编码。(其实big endian是指低地址存放最高有效字节(MSB),而little endian则是低地址存放最低有效字节(LSB)。)。
例如,为一个长度为47310 (0xB8CE)的负载进行编码,因为它的长度大于127, 所以要采用长编码方式。实际的长度需要两个字节来表示。则,长度编码字节为0x82; 然后用big-endian格式存储的长度值为0xB8 0xCE。则全部长度编码为 0x82 B8 CE。
3.3 ASN.1布尔类型
布尔编码的负载或者是全0或者是全1的八位位组。头字节以0x01开始,长度编码字节为0x01,负载内容取决于布尔值的取值。
布尔值 | 编码 |
False | 0x01 01 00 |
True | 0x01 01 FF |
3.4 ASN.1整数类型
整数类型表示一个有符号的任意精度的标量,它的编码是可移植,平台无关的。
正整数的编码比较简单。每个字节表示的最大整数是255 (0xFF), 存储的实际数值分成字节大小的数字,并且以big-endian格式存储。例如:
八位位组{Xk, Xk-1,…., X0}将以递减的顺序从Xk到X0进行存储.编码规定正整数的第一个字节的最高位必须是0,即Xk的最高为必须是0,为1的话则为负数.例如: x = 49468= 193 * 256 + 60 = 0xC1 * 0x FF + 0x3C; 即X1=0xC1, X0 = 0x3C. 按正常规定,编码应该是 0x02 02 C1 3C, 但是X1的最高位是1, 应该被看成负数.最简单的方法是用前端零字节进行填充.编码变为 0x02 02 00 C1 3C.
负整数的编码有些复杂.要先找到一个最小的256的幂,使它比要编码的负数的绝对值还要大.例如:x = – 1555; 被1555大的256的最小的幂是256^2 = 65536; 然后将这个数跟负数相加以得到2的补码. 65536 + (-1555) = 63981 = 0xF9 * 0xFF + 0x ED. 则编码为 0x02 02 F9 ED.
以下是一些常用整数编码的例子.
值 | 编码 |
0 | 0x02 01 00 |
1 | 0x02 01 01 |
2 | 0x02 01 02 |
127 | 0x02 01 7F |
128 | 0x02 02 00 80 |
-1 | 0x02 01 FF |
-128 | 0x02 01 80 |
-32768 | 0x02 02 80 00 |
1234567890 | 0x02 04 49 96 02 D2 |
3.5 ANS.1位串类型
位串(BIT STRING)类型以可移植形式表示位数组.除了ASN.1头部两个字节之外,还有一个附加的头部用来表示填充数据(通常是一个字节,因为填充是为了形成一个完整的字节).编码规则:位串的第一位放到第一个负载字节的第8位;位串的第二位放到第一个负载字节的第7位; 依此类推.填充满第一个负载字节,就继续填充第二个负载字节.如果最后一个负载字节未被填充满,空的位用0来填充, 0的个数存放到头部用来表示填充数据的那个字节里.
下面举例说明:
有一个位串{1,0,0,0,1,1,1,0,1,0,0,1},开始填充负载字节.第一个字节填充后为10001110 = 0x 8E; 第二个字节填充后为10010000 = 0x90, 低位4个0为填充的空位.则,负载为2个字节加上表示填充0个数的一个字节0x04总共3个字节.则完整的编码为:0x03 03 04 8E 90.
解码器通过计算8 * 负载长度 – 填充数 来得到存储输出所需要的位数.
3.6 ASN.1八位位组串类型
八位位组串(OCTET STRING)是保存字节数组,它和位串类型(BIT STRING)很相似.这种编码非常简单,像其他类型一样对头部进行编码,然后直接将八位位组复制过去即可.例如:对{FE, ED, 6A, B4}编码;首先存储类型0x04, 接着是长度0x04,然后是字节本身0xFE ED 6A B4; 完整的编码为 0x04 04 FE ED 6A B4.
3.7 空类型
空(NULL)类型实际上是”占位符”, 它是含有空白选项的选择修改器所特有.例如:
MyAccount ::= SEQUENCE {
Name IA5String,
Group IA5String,
Credentials CHOICE{
rsaKey RSAPublicKey,
passwdHash OCTET STRING,
none NULL
}
}
在上面这个结构中,帐号的证书应该包含一个RSA密钥或一个密码散列值或什么都没有.
空类型的编码是 0x05 00.
3.8 ASN.1 对象标识符类型
对象标识符(OBJECT IDENTIFIER, OID)类型用层次的形式来表示标准规范.标识符树通过一个点分的十进制符号来定义,这个符号以组织,子部分然后是标准的类型和各自的子标识符开始.
例如:MD5的OID 是 1.2.840.113549.2.5 表示为”iso(1) member-body (2) US (840) rsadsi(113549) digestAlgorithm (2) md5 (5)”, 所以当解码程序看到这个OID时,就知道是MD5散列.
OID在公钥算法标准中很流行,它指出证书绑定了哪种散列算法. 同样,也有公钥算法,分组算法,和操作模式的OID. 它们是一种高效且可移植的表示数据包中所选算法的形式.
对OID的编码规则:
-
- 前两部分如果定义为x.y, 那么它们将合成一个字40*x + y, 其余部分单独作为一个字节进行编码.
- 每个字首先被分割为最少数量的没有头零数字的7位数字.这些数字以big-endian格式进行组织,并且一个接一个地组合成字节. 除了编码的最后一个字节外,其他所有字节的最高位(位8)都为1.
举例: 30331 = 1 * 128^2 + 108 * 128 + 123 分割成7位数字(0x80)后为{1,108,123}设置最高位后变成{129,236,123}.如果该字只有一个7位数字,那么最高为0.
MD5 OID的编码:
1. 将1.2.840.113549.2.5转换成字数组 {42, 840, 113549, 2, 5}.
2. 然后将每个字分割为带有最高位的7位数字,{{0x2A},{0x86,0x48},{0x86,0xF7,0x0D},{0x02},{0x05}}.
3. 最后完整的编码为 0x06 08 2A 86 48 86 F7 0D 02 05.
3.9 ASN.1序列和集合类型
序列(SEQUENCE)和单一序列(SEQUENCE OF)以及相应的集合(SET)和单一集合(SET OF)类型叫做”结构”类型或简单容器.它们是一种用来把相关数据元素收集为一个独立的可解码元素的简单方法.
序列编码有以下性质:
1. 编码是结构化的.即头字节的位6必须设置.
2. 编码的内容是由ASN.1序列类型定义列表中的所有数据类型值的完全编码所组成,并且按照它们出现的顺序进行编码,除非这些类型被可选(OPTIONAL)或默认(DEFAULT)关键字所引用.
例:考虑如下序列
User ::== SEQUENCE{
ID INTEGER,
Active BOOLEAN
}
当取值为{32,TRUE}时,编码为 0x 30 06 02 01 20 01 01 FF} 在ASN.1文档里,使用空格来表示编码的属性.
0x30 06
02 01 20
01 01 FF
3.10 ASN.1可打印字符串和IA5String类型
可打印字符串(PrintableString)和IA5String类型定义了一种独立于本地代码页和字符集定义,在任何平台上都可以将ASCII字符串编码为可读字符串的可移植方法.
可打印字符串对象是ASCII集合的一个有限子集,这个子集包括32,39,40~41,43~58,61,63以及65~122.
IA5String类型的编码对象是ASCII集合中的大多数.包括NULL,BEL,TAB,NL,LF,CR以及32~126.
可打印字符串和IA5String的编码和八位位组串相似.可打印字符串的头字节是0x13, IA5String的是0x16. 例如:”Hello World”的编码为0x13 0B 48 65 6D 6D 6F 20 57 6F 72 6D 64.
3.11 ASN.1世界协调时类型
世界协调时(UTCTIME)定义了一种相对GMT时间的标准时间(以日期)编码.它使用”YYMMDDHHMMSSZ”的格式分别表示年,月,日,时,分,秒. 其中”Z”是遗留自初始的UTCTIME. 如果没有”Z”,就允许两种附加组”[+/-]hh ‘mm'”,其中”hh”和”mm”分别为与GMT的时差和分差. 如果有”Z”,则时间是以Zulu或GMT时间表示.
字符串的编码按照IA5String编码规则进行转换(ASCII字符集),其头字节为0x17而不是0x16. 例如:
July 4,2003 at 11:33 and 28 seconds编码为”030704113328Z”, 再编码0x17 0D 30 33 30 37 30 34 31 31 33 33 32 38 5A.
注:本文来自网络。