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 中类型共占一个字节,由三部分组成:类别位、结构化位、原始类型

类别位

表示类型要解释的上下文,占据高 22

第 8 位 第 7 位 含义
0 0 通用(Universal)
0 1 应用(Application)
1 0 上下文特定(Context Specific)
1 1 专用(Private)

结构化位

对于容器类型(如列表),用来表示容器内各个元素类型是否相同。如果相同,则可以只声明一次类型。占据第 66

原始类型

表示数据的基本类型,占据低 55

序号 十六进制 类型 解释
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 时间戳

长度编码

长度编码由一个字节作为起始,包含如下三种类型

短编码

最高位为 00,低 77 位表示 [1,127][1,127] 的长度。

0x05 表示数据长度为 5

长编码

最高位为 11,低 77 位表示 [1,127][1,127] 的长度,而后存在该长度的字节存储实际的长度。

0x82 0x04 0x92 中,0x82 表示使用长编码,且长度位为 22,后续 0x04 0x92 为实际的数据长度,为 11701170

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 规定的格式,反复调用该函数即可

参考资料