ASN1语法与BC库的ASN1 API
ASN1和BC库
本文为BC库文档《Java Cryptography Tools and Techniques.pdf》一书的附录A的学习笔记
涉及密码学的绝大多数标准都使用ASN.1
(抽象语法表示法1),这主要是一种用于描述对象如何编码以进行传输的语言。算法参数,密钥,签名和加密的消息。任何你能想到的,很有可能在某个地方会有ASN.1
结构对其进行编码。
基础语法
注释
有行注释和块注释,语法分别如下
行注释
--行内容--
块注释,块注释可以嵌套
/*
块内容
*/
对象标识符
ASN1
对象标识符(Object Identifiers (OID))是为了允许构造全局唯一的命名空间。一个OID
是由一个由点(’ . ')分隔的数字列表组成的,一个OID的结构沿着一条路径,通常被称为弧线,通过一个由3个主要分支扩展的树。这三个主要的分支对应三个机构:ITU-T
、ISO
、ISO/ITU-T
。OID弧线的第一个数字代表这三个组织中的一个,其中数字0分配给ITU-T、数字1分配给ISO,数字2分配给联合ISO/ITU-T组织。
三个主要分支机构的分配是根据一个组织最初在ISO/ITU-IT领域的位置进行的,数字0分配给ITU-T,数字1分配给ISO,数字2分配给联合ISO/ITU-T组织。在这之后,空间的划分变得相当随意,因为主弧的所有者会根据需要在下一层分配号码给其他组织。也就是说,尽管创建OID的方式具有明显的任意性质,但这些数字是全局惟一的,并用于为ASN.1模块、数据类型、算法以及您能想到的任何其他东西提供标识。唯一的复杂性是,因为oid基于组织而不是主体,在某些情况下,您会注意到相同的加密算法将被不同的oid引用。
例如,当您使用SHA256withRSA
调用Signature.getInstance()
时使用的RSA
安全算法有一个与之相关联的对象标识符,如下所示:
iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs-1(1) 11
模块结构
ASN.1模块的基本结构如下:
ModuleName { ObjectIdentifier }
DEFINITIONS Tagging TAGS ::=
BEGIN
EXPORTS export_list ;
IMPORTS import_list ;
body
END
ModuleName
和ObjectIdentifier
具有用于标识所描述的模块的值。
例如:
PKIX1Explicit88 { iso(1) identified-organization(3) dod(6) internet(1)
security(5) mechanisms(5) pkix(7) id-mod(0) id-pkix1-explicit(18) }
DEFINITIONS EXPLICIT TAGS ::=
BEGIN
-- EXPORTS ALL --
-- IMPORTS NONE --
因此模块名为PKIX1Explict88
,唯一标识为1.3.6.1.5.5.7.0.18
。
DEFINITIONS EXPLICIT TAGS::=
告诉我们标签字段是EXPLICIT
,另一种选择是IMPLICIT
,我们稍后会看到两者之间的区别。
EXPORTS可以将本模块的内容导出供其他模块使用,IMPORTS可以导入其他模块的内容在本例中,没有EXPORTS
关键字(它在注释中),这意味着模块中的任何内容都可以导出到另一个模块。IMPORTS
关键字也不见了,而且正如注释所暗示的那样,这意味着不需要导入。
数据类型
ASN.1
类型都属于三类:简单类型、字符串类型和结构化类型。字符串类型进一步细分为两个类别,原始位类型和代表特定字符编码的类型。结构化类型表示两种容器类型:对象的有序序列和对象的无序集。这些类型都在包org.bouncycastle.asn1
包中定义。
简单类型
-
BOOLEAN
表示一个真值或假值。在BC库中由
ASN1Boolean
类表示,使用ASN1Boolean.TRUE
和ASN1Boolean.FALSE
。 -
ENUMERATED
是INTEGER的一种特殊情况,通常来自受限集。在BC库由
ASN1Enumerated
类表示。 -
INTEGER
一个
INTEGER
可以用来表示任意大小的整数。注意:这意味着它们是有符号的,并且只有一种编码形式(二补码 two’s-complement)。BC库中整数表示为ASN1Integer
。 -
NULL
显式空值。重要的是不要将NULL与Java null混淆,Java null实际上更接近ASN.1的absent概念,即不存在编码。NULL用真实的编码来表示它。BC库中它由
ASN1Null
和遗留的DERNull
表示。DERNull.INSTANCE
提供一个常量值。 -
OBJECT IDENTIFIER
我们在上面的对象标识符一节中介绍了这种类型,这是它们的实际类型定义。BC库中它们由
ASN1ObjectIdentifier
类表示。 -
UTCTime
UTCTime
用于定义具有两位数年份的协调世界时。一般来说,两位数年份被解释为代表从1950年到2049年的年份,但显然有一些系统使用不同的窗口,所以总是值得检查如何转换这些窗口。UTCTime
的最低分辨率是秒。UTCTime
由BC库中的ASN1UTCTime
和DERUTCTime
类表示。 -
GeneralizedTime
GeneralizedTime
有一个4位数的年,可以用来表示任意精度的秒。UTCTime
由BC库中ASN1GeneralizedTime
和DERGeneralizedTime
类表示。
大多数基本类型只有一种编码,所以无论您使用DER还是更宽松的BER,它们总是以相同的方式编码。
两个例外是
UTCTime
和GeneralizedTime
。 这两个原语都将时间编码为 ASCII 字符串。 对于CER
和DER
,在GeneralizedTime
的情况下,这意味着秒元素将始终存在,并且小数秒(如果存在)将省略尾随零并且字符串将以 Z终止,因此以UTC/GMT
作为时区。UTCTime
遵循类似的模式。对于CER
和DER
,UTCTime
值被编码为总是存在的秒,并以Z结束。因为在一段数据中编码原语是非常容易的,这些原语可能最终会被用于签名中,如果签名验证者可能强制DER编码,那么在验证时就会出现意想不到的失败。
字符串类型
字符串类型又进一步分为两类。位串(Bit string)类型和字符串(character string)类型。
位串类型
有两种位串类型:
-
BIT STRING
BIT STRING
允许存储任意长的位串。它们被编码为两部分,第一部分是填充位的数量,后面是组成实际位串的字节串。在DER
编码中,填充位都应该为零。BIT STRING
由BC库中的ASN1BitString
和DERBitString
表示。 -
OCTET STRING
OCTET STRING
允许存储任意的字节串(对于更现代的人来说,是字节数组)。BC库中OCTET STRING
由ASN1OctetString
、BEROctetString
、DEROctetString
表示。还有用于BER OCTET
字符串处理的流类——BEROctetStringGenerator
和BEROctetStringParser
。
字符串类型
ASN.1
支持多种字符串类型。其中一些字符串类型表示已经退役的字符集,所以您很少会看到它们(VideotexString
和Teletex
是这方面的两个最好的例子),其他如PrintableString
、IA5String
、BMPString
和UTF8String
可能是最常见的。与位字符串类型一样,所有这些都允许存储任意长的字符串,但在Bouncy Castle
的实践中,我们只发现了需要DER和直接长度(direct-length)编码的情况。
-
BMPString、
BMPString
由BC中DERBMPString
类表示。它的名字来自于Basic Multilingual Plane——一个包含所有与现存语言相关的字符的结构。该字符集由iso10646
表示,与Unicode
表示的字符集相同。 -
GeneralString
GeneralString
由DERGeneralString
类表示。它可以包含iso2375
所述的国际编码字符集登记册所述的任何字符,包括控制字符。 -
GraphicString
GraphicString
由DERGraphicString
类表示。此字符串类型派生自与GeneralString
相同的字符集,但不包含控制字符。只允许打印字符。 -
IA5String
IA5String
是由来自国际字母表5(International Alphabet 5)的字符组成的,这是一个老的ITU-T
提案。现在,它们被认为涵盖了整个ASCII
字符集。该类型由DERIA5String
类表示。 -
NumericString
数字字符串只能包含数字0 ~ 9和空格字符。它由
DERNumericString
类表示。 -
PrintableString
PrintableString
由DERPrintableString
类表示。PrintableString
从ASCII
子集中提取它的字符集:字母a~Z, A~Z, 0到9,以及附加的字符: 空格、单引号、圆括号、+、-、点号、冒号、=、?、/、逗号。 -
TeletexString
最初称为
T61String
,由DERT61String
类表示。它可能很难正确解释,因为虽然它是一个8位字符类型,但它支持使用以ASCII ESC
(转义字符)开头的字符序列来更改字符集。 -
UniversalString
UniversalString
由DERUniversalString
类表示。这个字符串类型是为了国际化而添加的。默认情况下,Bouncy Castle
会将这些字符串转换为显示字符编码的十六进制字符串。 -
UTF8String
UTF8String
表示Universal Transformation Format, 8 bit,是完全国际化的推荐字符串类型。它由DERUTF8String
类表示,现在使用非常广泛。 -
VideotexString
顾名思义,
VideotexString
是为与视频文本系统一起使用而设计的,并适应可以使用控制代码构建简单图像的8位字符。 -
VisibleString
VisibleString
由DERVisibleString
类表示。最初,这种类型只包含来自ISO 646
的字符,但自1994年以来,它被解释为包含没有控制字符的纯ASCII字符。
容器类型(结构化类型)
ASN.1
中有两种容器类型—SET
和SEQUENCE
。这些在BC ASN.1 API
中使用ASN1Set
和ASN1Sequence
类 在最顶层进行支持。
ASN1Set
和ASN1Sequence
类都是抽象类,它们都有提示特定编码风格的实现类。·ASN.1 SEQUENCE·结构是有序的,因此编码风格的选择不会影响编码中元素的顺序。另一方面,ASN.1 SET
结构不是有序的,而是为了满足 DER 的要求,将元素按照其编码的数值排序(通过对元素进行编码然后将其转换为一个 添加足够的尾随零以使每个编码具有相同长度后的无符号整数)。
CHOICE类型
CHOICE是一种特殊类型,它表示ASN.1
字段或变量将是一组可能的ASN.1
类型或结构之一。在Java中并没有类似的东西,但与C中联合相识。
Bouncy Castle提供了一个标记接口ASN1Choice
,可以在表示CHOICE
类型的ASN.1
对象上实现。这个标记接口应该在可以使用的地方使用,因为它有助于提醒Bouncy Castle编码器对CHOICE
强制执行正确的编码规则(下面有更多介绍)。
编码规则
ASN.1
支持一系列不同的编码方式,从精心设计的无歧义且大小最小的二进制格式到使用XML
的通用编码方式。从目前在Bouncy Castle
所做的事情来看,有三组编码规则是最相关的:基本编码规则(Basic Encoding Rules BER),区别编码规则(Distinguished Encoding Rules DER)和规范编码规则(Canonical Encoding Rules CER)。
BER
ASN.1
编码系统的核心是围绕基本编码规则(Basic encoding Rules, BER)建立的。BER编码遵循TLV
(tag-length-value 标签-值长度-值内容)约定。该类型使用tag来标识,接下来是给出内容长度的值,然后是描述内容的字节串。DER和CER都是BER的子集。
BER编码提供了三种编码ASN.1
结构或原语的方法:
-
Primitive definite-length
在这种情况下,整个原语用单个块描述—类型标记tag 后面的长度和数据告诉你关于编码结构中的大小和数据所需的一切。
-
Constructed definite-length
construct definet -length
既用于表示包含多个原语(可能自己构造)的 ASN.1 结构,也用于表示被定义为原语定长编码列表的集合的原语。 在其中之一的情况下,只能通过读取构成构造编码的所有原始编码来确定数据的实际大小以及构成它的字节。 -
Constructed indefinite-length
Constructed indefinite-length也由其他编码组成——可能是不定长本身。 它由长度为 0x80 的八位字节表示,并以由两个值为 0x00 的八位字节组成的内容结束标记终止。 这种编码出现很多 - 它是唯一可以处理可能大于内存资源数据的方法,或者其他考虑因素使得无法计算出编码数据最初可能有多长。
Tagging
作为 TLV
格式,BER 具有一组标准的预定义标签,用于常见原语和结构化类型。 BER
还允许你为容器类型定义自己的标签值。不过,在定义自己的标签值时,有几个不同之处。
第一个变化是ASN.1
对象可以标记为 EXPLICIT
,这意味着您的标记值和对象的原始标记值将包含在编码中,或者ASN.1
对象可以标记为 IMPLICIT
,这意味着您的标记值替换了对象在编码中的原始标记值。 当然,随着原始标签值丢失,如果您不知道原始标签值是什么并且手头没有任何文档告诉您它应该是什么,您可能会问自己如何正确解释编码。 可悲的是,这是无法判断。 这对于结构化类型变得尤为重要,因为显式标记的原语将生成与包含单个原语的隐式标记的 SET 或 SEQUENCE 相同的编码。
Bouncy Castle ASN.1 库中的所有标准 API 类型都支持静态 getInstance()
方法,该方法采用 ASN1TaggedObject
和指示隐式或显式标记的布尔值。 由于 IMPLICIT
和结构化类型的含糊不清,因此在解释 ASN1TaggedObject
类型的对象时使用这些方法(带指示标记)非常重要,并确保使用与使用的标记类型相关的布尔值的正确值。
第二个变化是 ASN.1 对象是 CHOICE。 对于 CHOICE 类型,不能丢失与 CHOICE 值相关的任何标签,因为这是了解 CHOICE 值真正代表什么的唯一线索。
Bouncy Castle 提供了一个标记接口 ASN1Choice
,它可以在 ASN.1
对象上实现,以提醒 Bouncy Castle
编码器强制执行正确的编码规则。 我们建议您在定义代表 ASN.1 CHOICE
的对象时使用它。
DER
由于提供了结构化的基本类型、无序集等,可能有几种方法可以对给定的ASN.1结构进行编码。DER旨在确保相同的ASN.1
数据总是产生相同的编码。
这对于签名数据或MAC
数据尤其重要,因为验证签名ASN.1
数据的能力依赖于验证方能够准确地重新创建已签名数据的编码的能力。
为了确保这种能力,DER对BER增加了以下限制:
- 只允许固定长度(definite-length)编码。
- 只有结构化类型
SEQUENCE
、SET
、IMPLICIT
标记的SEQUENCE
或SET
和EXPLICIT
标记的对象可以使用构造的定长对象。 - 对数据长度的编码必须始终以最小字节数进行编码(不允许前导零)。
- 被设置为
DEFAULT
值的字段不包含在编码中。 SET
中包含的对象在编码前按调整后的编码值排序(在必要的地方附加0以使所有编码长度相同)。
注意:最后两个限制也适用于CER。
DER SET是有序的,ASN.1 SET本身是无序的。除DER外,编码不依赖于SET中的特定顺序。
使用导引
在面向对象的语言中使用像 ASN.1 这样的协议层的困难之一是有时不清楚在何处处理 ASN.1 结构是安全的。例如是该将AlgorithmIdentifier
作为对象org.bouncycastle.asn1.x509.AlgorithmIdentifier
,还是 作为其原语组成SEQUENCE的结构(Java 中的 org.bouncycastle.asn1.ASN1Sequence
)。 在这种情况下,转换实际上并不能很好地工作 - 脱离网络的对象通常是原始组合,但从应用程序中的高级函数传递的对象往往是引用特定标准模块中类型的实际对象。 如果以意外方式使用提供的方法,则强制转换可能很容易导致 ClassCastException。 可以使用简单的构造函数,但可能会导致不必要地创建许多对象并增加复杂性。
在 Bouncy Castle 中,这个问题通过使用静态 getInstance() 方法来解决,这使得强制转换变得不必要。 例如,AlgorithmIdentifier.getInstance()
可以传递一个 AlgorithmIdentifier
对象或一个 ASN1Sequence
对象,该对象表示一个 AlgorithmIdentifier
,并且如果传入一个 null
值将始终返回一个 AlgorithmIdentifier
或 null
。同样 ASN1Sequence.getInstance()
可以传递一个 AlgorithmIdentifier
对象 或 ASN1Sequence
对象,如果传入 null
值,将始终返回 ASN1Sequence
或 null。
定义你自己的对像
通常,如果您正在定义基于 ASN.1
的对象,您应该定义一个继承 org.bouncycastle.asn1.ASN1Object
的类。 这将提供equals()
和 hashCode()
定义,并且需要在扩展类中定义 toASN1Primitive()
方法。 之后,如果您希望遵循现有模式(我们强烈推荐),您应该定义一个静态 getInstance(Object)
方法,该方法能够接受任意类类型(toASN1Primitive()
返回的任何内容)或 null
。
下面的示例代码提供了ASN.1结构的类定义:
SimpleStructure ::= SEQUENCE {
version INTEGER DEFAULT 0,
created GeneralizedTime,
data OCTET STRING,
comment [0] UTF8String OPTIONAL
}
可以使用一下Java代码来定义:
/**
* SimpleStructure的实现,一个示例ASN.1对象 (tagging IMPLICIT).
*
* SimpleStructure ::= SEQUENCE {
* version INTEGER DEFAULT 0,
* created GeneralizedTime,
* data OCTET STRING,
* comment [0] UTF8String OPTIONAL
* }
*/
public class SimpleStructure extends ASN1Object {
//主要的属性都为final,表示这是一个不可变的类,创建了就不可修改
private final BigInteger version;
private final Date created;
private final byte[] data;
private String comment;
/**
* 将传入的对象转换或强制转换为SimpleStructure。
*
* @param obj 待转换的对象
* @return SimpleStructure对象
*/
public static SimpleStructure getInstance(Object obj) {
//如果其本身就是一个SimpleStructure,直接返回这个对象
if (obj instanceof SimpleStructure) {
return (SimpleStructure) obj;
}
//如果是其他类型且不为空,则转为ASN1Sequence(它可以表示任何结构类型)
//后再从ASN1Sequence中取出需要的数据项
else if (obj != null) {
return new SimpleStructure(ASN1Sequence.getInstance(obj));
}
return null;
}
/**
* 创建一个具有默认版本的结构。
*
* @param created 创建时间
* @param data 需要包含的编码过的数据
*/
public SimpleStructure(Date created, byte[] data) {
this(0, created, data, null);
}
/**
* 创建一个具有默认版本和可选注释的结构。
*
* @param created 创建时间
* @param data 编码过的数据
* @param comment 注释
*/
public SimpleStructure(Date created, byte[] data, String comment) {
this(0, created, data, comment);
}
/**
* 创建具有特定版本和可选注释的结构。
*
* @param version 版本
* @param created 创建时间
* @param data 编码过的数据
* @param comment 注释
*/
public SimpleStructure(int version, Date created,
byte[] data, String comment) {
this.version = BigInteger.valueOf(version);
this.created = new Date(created.getTime());
this.data = Arrays.clone(data);
if (comment != null) {
this.comment = comment;
} else {
this.comment = null;
}
}
// BC希望用户养成使用getInstance()的习惯。它是安全的
private SimpleStructure(ASN1Sequence seq) {
int index = 0;
//逐个从ASN1Sequence 中取出里面的各项数据,看是否是指定类型
if (seq.getObjectAt(0) instanceof ASN1Integer) {
this.version = ASN1Integer.getInstance(
seq.getObjectAt(0)).getValue();
index++;
}
//如果不是则赋值默认值
else {
this.version = BigInteger.ZERO;
}
//解析时间值
try {
this.created = ASN1GeneralizedTime.getInstance(
seq.getObjectAt(index++)).getDate();
} catch (ParseException e) {
throw new IllegalArgumentException(
"exception parsing created: " + e.getMessage(), e);
}
//获取字节数组数据
this.data = Arrays.clone(
ASN1OctetString.getInstance(seq.getObjectAt(index++)).getOctets());
//comment是可选的,从ASN1Sequence剩下的数据项中尝试拿到comment
for (int i = index; i != seq.size(); i++) {
ASN1TaggedObject t = ASN1TaggedObject.getInstance(
seq.getObjectAt(i));
//拿到tag number为0的(根据ASN1定义的信息)
if (t.getTagNo() == 0) {
comment = DERUTF8String.getInstance(t, false).getString();
}
}
}
//不可变的对线可以直接返回,可变的要拷贝返回新的对象
public BigInteger getVersion() {
return version;
}
public Date getCreated()
throws ParseException {
return new Date(created.getTime());
}
public byte[] getData() {
return Arrays.clone(data);
}
public String getComment() {
return comment;
}
/**
* 生成对象的DER表示。
*
* @return 由DER原语组成的ASN1Primitive.
*/
@Override
public ASN1Primitive toASN1Primitive() {
//存放各属性值的容器
ASN1EncodableVector v = new ASN1EncodableVector();
//按照ASN1类型将Java字段值封装为对应类型的Java对象
// DER编码规则规定,具有指定的DEFAULT值的字段不包含在编码中
if (!version.equals(BigInteger.ZERO)) {
v.add(new ASN1Integer(version));
}
v.add(new DERGeneralizedTime(created));
v.add(new DEROctetString(data));
if (comment != null) {
v.add(new DERTaggedObject(false, 0, new DERUTF8String(comment)));
}
return new DERSequence(v);
}
}
该类遵循标准模式。它扩展了ASN1Object
,提供了一个getInstance()
方法,还考虑了DER
编码规则(version字段只有在它不是默认值时才进行编码)。getInstance()
方法能够将其大部分工作外包给
ASN1Sequence. getInstance()
和接受ASN1Sequence
的构造函数被标记为private
,因此任何使用它的尝试都必须通过SimpleStructure.getInstance()
方法。
需要注意的另一件事是类是不可变的。这里需要注意的主要事情是,与几乎所有ASN.1
对象不同,ASN1OctetString
不是不可变的——主要是出于性能原因。这就是为什么在getOctets()
方法返回的byte[]
上调用Arrays.clone()
方法的原因。理想情况下ASN1Object
同时提供了equals()
和hashCode()
, ASN1Object
应该是不可变的!
这个例子我们自定义了一个SEQUENCE类型的ASN1对象。如前在容器类型中所述,BC提供了ASN1Sequence类做顶层支持,该类是ASN1结构在Java中的一个标准的映射(其中的各个属性只能使用
getObjectAt
方法访问)。我们在自定义一个Java的POJO类来映射我们定义的ASN1结构的对象时,可以使用ASN1Sequence的对象作为桥梁来将数据对象在Java标准类型与ASN1标准类型之间进行转换----先用ASN1Sequence的getInstance方法将一个传进来的obj(通常为一个ASN1Primitive的子类,或ASN1Encodable的子类,ASN1Encodable可以调用toASN1Primitive获取ASN1Primitive对象)转为ASN1Sequence对象。当然,DER支持的SET类型也能类似地使用ASN1Set类做支持。