抽象语法表示(标记)ASN.1(Abstract Syntax Notation One )一种数据定义语言,描述了对数据进行表示、编码、传输和解码的数据格式。网络管理系统中的管理信息库(MIB)、应用程序的数据结构、协议数据单元(PDU)都是用ASN.1定义的。
ASN.1优点:
通过如下的独立:
⑴独立于机器;
⑵独立于程序语言;
⑶独立于应用程序的内部表示,用一种统一的方式来描述数据结构。
解决如下的不同:
⑴程序语言之间数据类型不同
⑵不同机器平台之间数据的存储方式不同
⑶不同种类的计算机内部数据表示不同
比如:IBM为EBCDIC,其它为ASCⅡ;Intel的芯片从右到左计数字节数,而Motorola的芯片则从左到计数字节数。
在任何需要以数字方式发送信息的地方,都可使用ASN.1发送各种形式信息。包括音频、视频、图片、数据等。由于各种系统对数据的定义并不完全相同, 这自然给利用其它系统的数据造成了障碍。表示层就担负了消除这种障碍的任务。表示层如同应用程序和网络之间的翻译官:主要解决用户信息的语法表示问题,即提供统一的、格式化的表示和转换数据服务。数据的压缩、解压、加密、解密都在该层完成。
ASN.1语法遵循传统的巴科斯范式BNF风格.最基本的表达式如: Name ::= type . 表示为定义某个名称为Name的元素,它的类型为type. 例如: MyName ::= IA5String . 表示为定义了一个名为MyName的元素或变量,其类型为ASN.1类型IA5String (类似于ASCII字符串).
有些时候,我们需要定义一种ASN.1类型,它的子集元素包含预定义值. Name ::= type(Explict Value) . 显式值(ExplictValue).必须是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
}
容器是值一个包含了其他相同或者不同类型元素的数据类型(例如序列值SEQUENCE或集合值SET类型).目的是为了组合一些复杂的数据类型集.ASN.1规范定义了4种容器类型:序列,单一序列(SEQUENCEOF),集合和单一集合(SET OF).虽然它们意义不同,但是语法是一样的.
Name ::= Container {Name Type [ Name Type...]} 方括号中的内容和容器的元素个数都是可选项.还可以进行嵌套定义.
例:
UserRecord::= SEQUENCE {
Name SEQUENCE {
First IA5String,
Last IA5String
},
DoB UTCTIME
}
将其粗略的翻译成C语言中的结构如下:
structUserRecord {
struct Name {
char *First,
char *Last
};
time_t DoB;
}
将其粗略的翻译成ObjectPascal语言中的记录如下(ObjectPascal不支持嵌套记录):
Type
Name = record
First : String;
Last : String;
end;
UserRecord = record
aName : Name;
DoB : DateTime;
end;
ASN.1定义了各种修改器,如可选(OPTIONAL),默认(DEFAULT),和选择(CHOICE). 他们可以改变表达式的声明.典型地用于定义一种要求编码灵活,而定义又不繁琐的类型.
<1>.可选(OPTIONAL)。顾名思义,其表示改变一个元素以便在编码时它的类型是可选择的.即编码器可以忽略这个元素,解码器不能假设它将出现.但当邻接的两个元素具有相同的类型时,会给解码器带来一些问题.
定义: Name ::= TypeOPTIONAL
例如:
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密钥的公钥证书.
任何ASN.1编码都是以两个字节开始(或者八位位组,含有8个二进制位),不管什么类型,它们都是通用的.第一个字节是类型标识符,也包含一些修正位;第二各字节是长度.基本类型如下:
.布尔型(Boolean);
.八位位组串 (OCTETString);
.位串 (BITString);
.IA5String;
.可打印字符串(PrintableString);
.整数 (INTEGER);
.对象标识符 (OBJECTIdentifier, OID);
.世界协调时(UTCTIME);
.空 (NULL);
.序列,单一序列;
.集合;
.单一集合;
布尔编码的负载或者是全0或者是全1的八位位组。头字节以0x01开始,长度编码字节为0x01,负载内容取决于布尔值的取值。
布尔值 | 编码 |
False | 0x01 01 00 |
True | 0x01 01 FF |
整数类型表示一个有符号的任意精度的标量,它的编码是可移植,平台无关的。
正整数的编码比较简单。每个字节表示的最大整数是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 02F9 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 |
位串(BITSTRING)类型以可移植形式表示位数组.除了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 * 负载长度 - 填充数来得到存储输出所需要的位数.
八位位组串(OCTET STRING)是保存字节数组,它和位串类型(BIT STRING)很相似.这种编码非常简单,像其他类型一样对头部进行编码,然后直接将八位位组复制过去即可.例如:对{FE, ED, 6A, B4}编码;首先存储类型0x04, 接着是长度0x04,然后是字节本身0xFE ED 6A B4; 完整的编码为 0x04 04 FE ED 6A B4.
空(NULL)类型实际上是"占位符", 它是含有空白选项的选择修改器所特有.例如:
MyAccount ::= SEQUENCE {
Name IA5String,
Group IA5String,
Credentials CHOICE{
rsaKey RSAPublicKey,
passwdHash OCTET STRING,
none NULL
}
}
在上面这个结构中,帐号的证书应该包含一个RSA密钥或一个密码散列值或什么都没有.
空类型的编码是 0x05 00.
对象标识符(OBJECTIDENTIFIER, 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.
序列(SEQUENCE)和单一序列(SEQUENCE OF)以及相应的集合(SET)和单一集合(SET OF)类型叫做"结构"类型或简单容器.它们是一种用来把相关数据元素收集为一个独立的可解码元素的简单方法.
序列编码有以下性质:
1. 编码是结构化的.即头字节的位6必须设置.
2. 编码的内容是由ASN.1序列类型定义列表中的所有数据类型值的完全编码所组成,并且按照它们出现的顺序进行编码,除非这些类型被可选(OPTIONAL)或默认(DEFAULT)关键字所引用.
例:考虑如下序列
User ::== SEQUENCE{
ID INTEGER,
Active BOOLEAN
}
当取值为{32,TRUE}时,编码为 0x 3006 02 01 20 01 01 FF} 在ASN.1文档里,使用空格来表示编码的属性.
0x30 06
02 01 20
01 01 FF
可打印字符串(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 6D6D 6F 20 57 6F 72 6D 64.
世界协调时(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.