注:本文前面一小部分是本人所写,后面大部分是摘抄网友(找不到链接了)文章。
本文只描述用于国密标准的电子签章所需要的知识点。如果想要详细学习ASN.1知识,推荐《ASN.1 Communication between Heterogeneous Systems》和《ASN.1 Complete》
ASN.1 是定义抽象数据类型规格形式的标准。是用于描述数据的表示、编码、传输、解码的灵活的记法。它提供了一套正式、无歧义和精确的规则,以描述独立于特定计算机硬件的对象结构。简单的说,ASN.1就是用二进制数据描述结构和带类型的数据。也因此几乎所有操作系统上的工具都支持 ASN.1,ASN.1 支持如 java,c 和 c++这样流行的编
程语言,和包括 COBOL 这样的较老的编程语言。
ASN.1语法遵循传统的巴科斯范式BNF风格.最基本的表达式如: Name ::= type.
理论的东西慢慢说,先给一个直观的例子。
ASN.1描述某一个数据及类型方式如下:
比如一个IA5String类型数据
比如一个结构体,定义如下:
那实际ASN.1编码的数据可能如下:
1、ASN.1容器
容器是指一个包含了其他相同或者不同类型元素的数据类型(例如序列值SEQUENCE或集合值SET类型).目的是为了组合一些复杂的数据类型集.ASN.1规范定义了4种容器类型:序列,单一序列(SEQUENCE OF),集合和单一集合(SET OF).虽然它们意义不同,但是语法是一样的. Name ::= Container {Name Type [ Name Type...]} 方括号中的内容和容器的元素个数都是可选项.还可以进行嵌套定义。如上述印章头定义。
2、ASN.1修改器
ASN.1定义了各种修改器,如可选(OPTIONAL),默认(DEFAULT),和选择(CHOICE). 他们可以改变表达式的声明.典型地用于定义一种要求编码灵活,而定义又不繁琐的类型。
显然,可选(OPTIONAL)指当前定义的数据可能存在也可能不存在。默认(DEFAULT)指默认修改器允许容器包含默认值.如果待编码的数据值等同于它的默认值,那么它将在发送的数据流中被忽略。选择(CHOICE)允许一个元素在给定的实例中可以有多个可能值,如:
SES_CertList::=CHOICE{
certs CertInfoLists,---------签章者证书
certDigestList CertInfoLists---------签章者证书杂凑值
}
表示SES_CertList子集中可能是certs或可能是certDigestList
3、ASN.1数据类型
密码学相关的数据类型有:
UNIVERSAL 1 布尔类型 (Boolean)
UNIVERSAL 2 整型 (INTEGER)
UNIVERSAL 3 二进制字符串类型(BITSTRING)
UNIVERSAL 4 八进制字符串类型 (OCTETSTRING)
UNIVERSAL 5 空类型 (NULL)
UNIVERSAL 6 对象标识符类型 (OBJECT Identifier, OID)
UNIVERSAL 12 UTF8 字符串类型 (UTF8String)
UNIVERSAL 14-15 保留给本建议的以后版本和国际标准使用
UNIVERSAL 18-22 字符串 类型 (如19-PrintableString,22-IA5String,)
UNIVERSAL 23-24 时间 类型 (23-UTCTIME,24--GeneralizedTime)
任何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)表示一个给定的编码是否是相同类型的多种编码的结构化。结构化元素是容器类型必需的,因为在逻辑上,它们只是其他元素的集合。比如A0表示context[0],是容器类,而86则是一个context[6]的数据类型。
结构化元素有自己的头字节和长度字节,之后是元素各个要素组件的单独编码。也就是说,这些要素组件是独立地可解码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 | GeneralizedTime世界协调时 | 以统一格式表示的时间 |
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)类型实际上是"占位符", 它是含有空白选项的选择修改器所特有.
空类型的编码是 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