ASN.1 语法及 X.509 证书格式解析解析
实验室有个项目需要用 Zeek 解析证书公钥信息,不过 zeek 并不支持该功能。而目前网上都是直接调用 openssl 来提取公钥。无奈之下,只能自己实现一个了。
语法
ASN.1 语法格式
ASN.1(Abstract Syntax Notation dot one)是一种抽象语法标记。用来定义抽象数据类型形式的标准,可以用来描述一个协议、数据结构应该长什么模样。尽管设计目的上不涉及数据结构如何存储,但由于使用广泛,实际上存在一套默认的规范。
作为一个抽象语法,其主要目的是描述数据结构(类型定义),可以与 TypeScript 做类比(某种程度上,这里的语法和 TS 很相似)。需要考虑的语法只有一个 类型名 ::= 基础类型 { 类型具体描述 }
,根据不同的基础类型进行拼接组合即可(基础类型见下文)
如果我们需要设计一个 Blog 协议,包含地址、标题、日期、正文、状态。那么可以按照如下格式进行定义
Blog ::= SEQUENCE { address PrintableString, title UTF8String, date UTCTime, content String, status BlogStatus } BlogStatus ::= ENUMERATED { Draft(0), Published(1), Hidden(2) }
X.509 证书定义样例
Certificate ::= SIGNED SEQUENCE{ version [0] Version DEFAULT v1988, serialNumber CertificateSerialNumber, signature AlgorithmIdentifier, issuer Name, validity Validity, subject Name, subjectPublicKeyInfo SubjectPublicKeyInfo } Version ::= INTEGER {v1988(0)} CertificateSerialNumber ::= INTEGER Validity ::= SEQUENCE{ notBefore UTCTime, notAfter UTCTime } SubjectPublicKeyInfo ::= SEQUENCE{ algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING } AlgorithmIdentifier ::= SEQUENCE{ algorithm OBJECT IDENTIFIER, parameters ANY DEFINED BY algorithm OPTIONAL }
编码
ASN.1 严格包含三部分内容:类型编码、长度编码、数据
类型编码
ASN.1 中类型共占一个字节,由三部分组成:类别位、结构化位、原始类型
类别位
表示类型要解释的上下文,占据高 位
第 8 位 | 第 7 位 | 含义 |
---|---|---|
0 | 0 | 通用(Universal) |
0 | 1 | 应用(Application) |
1 | 0 | 上下文特定(Context Specific) |
1 | 1 | 专用(Private) |
结构化位
对于容器类型(如列表),用来表示容器内各个元素类型是否相同。如果相同,则可以只声明一次类型。占据第 位
原始类型
表示数据的基本类型,占据低 位
序号 | 十六进制 | 类型 | 解释 |
---|---|---|---|
1 | 0x01 |
BOOLEAN | 布尔类型 |
2 | 0x02 |
INTEGER | 整数 |
3 | 0x03 |
BIT_STRING | 比特流 |
4 | 0x04 |
OCTET_STRING | 字节流 |
5 | 0x05 |
NULL | 空值 |
6 | 0x06 |
OID | 对象标识符 |
16 | 0x10 |
SEQUENCE | 数组 |
17 | 0x11 |
SNMP_SET | 集合 |
19 | 0x13 |
PrintableString | ASCII编码可视字符 |
20 | 0x14 |
T61_STRING | |
22 | 0x16 |
IA5_String | ASCII编码 |
23 | 0x17 |
UTCTIME | 时间戳 |
长度编码
长度编码由一个字节作为起始,包含如下三种类型
短编码
最高位为 ,低 位表示 的长度。
如 0x05
表示数据长度为 5
长编码
最高位为 ,低 位表示 的长度,而后存在该长度的字节存储实际的长度。
如 0x82 0x04 0x92
中,0x82
表示使用长编码,且长度位为 ,后续 0x04 0x92
为实际的数据长度,为
Zeek 解析证书公钥
根据上面的语法,可以看出来实际上 ASN.1 只需要一个简单的递归函数即可搞定。
由于这里只需要提取一个字段,甚至不必全文解析。
首先要实现一个 ASN.1 类型解析的函数(Zeek Script 语法,别的语言肯定比这写起来更容易)
function read_part(content:string): ASN1Object { local result = ReadBytesResult(); result= read_n_bytes(content, 1); content = result$content; local object_type = bytestring_to_count(result$result); if (object_type == 0) { return ASN1Object( $data_type=object_type, $length=0, $value="", $rest=content ); } result = read_n_bytes(content, 1); content = result$content; local object_length = bytestring_to_count(result$result); if (object_length & 0x80 > 0) { object_length = object_length & 0x7f; result = read_n_bytes(content, object_length); content = result$content; object_length = bytestring_to_count(result$result); } result = read_n_bytes(content, object_length); local object_value = result$result; content = result$content; return ASN1Object( $data_type=object_type, $length=object_length, $value=object_value, $rest=content ); }
接下来按照 X.509 规定的格式,反复调用该函数即可