ASN1之字符串数据类型与BER和DER编码形式

人越长大,就越习惯压抑内心的真实感受,不再放声大哭放声大笑,什么都只是淡淡的点到为止

Posted by yishuifengxiao on 2025-05-18

ASN.1 字符串数据类型:区别与共同点

共同点

  1. 编码方式:所有ASN.1字符串类型在通过BER(Basic Encoding Rules)、DER或PER进行编码时,最终都会转换成一个字节序列。这个字节序列由三部分组成:

    • Tag(标签):一个唯一的字节,标识这是哪种字符串(例如 0x16 代表 IA5String)。
    • Length(长度):一个或多个字节,指示Value字段的字节数。
    • Value(值):字符串内容根据其特定规则编码后的字节序列。
  2. 核心抽象:在ASN.1抽象语法层,它们都表示文本信息。

区别与常见类型

区别主要在于允许的字符集标签号。以下是关键类型:

类型 描述 允许的字符集 常见标签值 (Hex) 典型用途
IA5String 国际字母表5 7位ASCII字符集(代码值 0-127)。基本上是英文字母、数字、标点符号和一些控制字符。 16 (U) 电子邮件地址(RFC 5280)、域名、Country Code。非常常见
UTF8String Unicode转换格式8 完整的Unicode字符集。这是最通用、限制最少的字符串类型。 0C (U) 现代应用中的任何文本,尤其是需要国际化支持(如中文、表情符号)的场景。最推荐
PrintableString 可打印字符串 ASCII的一个子集:A-Z, a-z, 0-9, 以及空格和特定标点:' ( ) + , - . / : = ? 13 (U) 早期X.509证书中的字段(如Common Name)。限制较多,但仍在用。
NumericString 数字字符串 仅限数字 0-9空格 12 (U) 电话号码、序列号等纯数字标识符。
VisibleString 可见字符串 ISO/I646 IRV(可打印的ASCII字符,无控制字符)。 1A (U) 可显示字符,比PrintableString限制少,但不如IA5String通用。
OCTET STRING 字节串 这不是字符串类型! 它是任意字节的序列,没有字符编码的概念。 04 (U) 存储加密数据、哈希值、证书签名、或任何不解释为文本的二进制数据。极易混淆
BMPString 基本多文种平面字符串 Unicode的UCS-2基本多文种平面(即不包括代理对,编码为双字节/字符)。 1E (U) 较老的Unicode支持方式,正逐渐被更高效的UTF8String取代。
  • (U) 表示标签属于 Universal 类。

核心总结:选择哪种类型?

  • 需要支持任何语言?用 UTF8String
  • 只是英文、数字、邮箱、域名?用 IA5String
  • 是纯数字?用 NumericString
  • 是任意二进制数据(如图片、加密密文)?用 OCTET STRING

字符串与十六进制(Hex)的相互转换

十六进制(Hex)是人类可读的字节表示法。转换过程涉及两个层面:

  1. 字符编码:将文本字符串转换为字节(根据字符串类型的规则)。
  2. 字节表示:将字节转换为十六进制字符串(或反向)。

字符串转Hex

假设我们有一个 UTF8String,其值为 "hello@例.com"

第1步:字符编码(文本 -> 字节)

首先,必须将字符串根据其类型规则编码为字节。

  • "hello@" 部分是ASCII,每个字符占1字节。
    • h -> 0x68
    • e -> 0x65
    • l -> 0x6C
    • l -> 0x6C
    • o -> 0x6F
    • @ -> 0x40
  • "例" 是一个中文字符,在UTF-8中编码为3个字节0xE4 0xBE 0x8B
  • ".com" 又是ASCII,每个字符1字节。
    • . -> 0x2E
    • c -> 0x63
    • o -> 0x6F
    • m -> 0x6D

最终的值字节序列(Value Octets) 为:
68 65 6C 6C 6F 40 E4 BE 8B 2E 63 6F 6D

第2步:ASN.1 BER 编码(字节 -> TLV 结构)

现在,我们为这个字节序列添加ASN.1包装。

  1. Tag: UTF8String 的通用标签是 0x0C
  2. Length: 我们的值有 13 个字节 (0x0D)。
  3. Value: 第一步得到的字节序列。

完整的ASN.1编码是这三部分的拼接:
TLV结构: 0C 0D 68 65 6C 6C 6F 40 E4 BE 8B 2E 63 6F 6D

第3步:转换为最终的Hex字符串

上面的TLV结构已经是一个字节序列了。我们直接将其表示为十六进制字符串,这就是最终结果:
"0C0D68656C6C6F40E4BE8B2E636F6D"

反向转换(Hex -> 字符串)

过程完全相反:

  1. 取Hex字符串 "0C0D68656C6C6F40E4BE8B2E636F6D"
  2. 解析TLV结构:
    • 第一个字节 0x0C -> 标签,告诉我这是UTF8String
    • 第二个字节 0x0D -> 长度,告诉我后面有13个字节是值。
  3. 提取出接下来的13个字节:68 65 6C 6C 6F 40 E4 BE 8B 2E 63 6F 6D
  4. 关键步骤:对这些字节进行解码。因为Tag是UTF8String,所以我必须使用UTF-8解码器来解读这些字节。
    • 68 65 6C 6C 6F 40 -> 解码为 hello@
    • E4 BE 8B -> 解码为
    • 2E 63 6F 6D -> 解码为 .com
  5. 最终得到原始字符串:"hello@例.com"

重要注意事项

  • OCTET STRING 的陷阱:如果你有一个 OCTET STRING 的Hex值(如 0408A1B2C3D4E5F6A7),不要尝试直接将值部分(A1B2C3D4E5F6A7)解码为文本(如UTF-8)。因为它本来就不是文本,强行解码只会得到乱码。它的意义需要根据上层协议来解读(可能是一个密钥、一个哈希值等)。
  • 工具的使用:在实际工作中,我们使用工具或库来完成这些转换:
    • OpenSSLasn1parse 命令可以解析DER/PEM文件并显示Hex和内容。
    • 在线ASN.1解码器:非常直观,粘贴Hex即可解析。
    • 编程库:如Python的 pyasn1asn1crypto,Java的 Bouncy Castle 等。你只需要在代码中操作对象(如 my_string = UTF8String("hello")),库会自动处理编码和解码。

OCTET STRING和BIT STRING的区别与共同点

  • OCTET STRING(八位位组串/字节串):处理数据的天然单位是字节(8位)。它关心的是整个字节的序列。
  • BIT STRING(比特串/位串):处理数据的天然单位是比特(1位)。它可以表示一个长度不是8的倍数的任意比特序列。

共同点

  1. 基本类型:两者都是ASN.1中用于表示二进制数据的基本类型。它们都不对数据内容本身做任何语义上的解释(例如,不像IA5String那样解释为文本)。
  2. TLV结构:在BER(Basic Encoding Rules)、DER等编码规则下,两者都使用TLV(Tag-Length-Value)结构进行编码。
  3. 通用类标签:它们都属于ASN.1的“通用类”(Universal Class),并拥有固定的标签号:
    • OCTET STRING 的标签号是 04 (十六进制)。
    • BIT STRING 的标签号是 03 (十六进制)。
  4. 用途:都常用于编码非结构化数据,如加密散列值、数字签名、加密密钥、硬件寄存器内容等。

区别

特性 OCTET STRING BIT STRING
基本单位 八位位组(Octet),即字节(Byte) 比特(Bit)
长度约束 长度总是8的倍数。长度以字节为单位计量。 长度可以是任意位数(例如,1位、7位、100位)。长度以比特为单位计量。
BER/DER 编码(Value字段) 非常简单Value字段直接就是字节序列本身。
例如:数据 0xAB, 0xCD, 0xEF 编码后就是 AB CD EF
更为复杂Value字段由两部分组成:
1. 一个初始字节:指明末尾有多少个未使用的位Unused_bits)。这个值在 0 到 7 之间。
2. 编码后的字节序列:包含实际比特数据的字节。最后一个字节中只有 (8 - Unused_bits) 个位是有效的。
直观比喻 整瓶整瓶的矿泉水(每瓶8两,你不能卖半瓶)。 可以按出售的散装矿泉水(最后那个瓶子可能没装满)。
主要用途 存储和处理自然以字节为单位的数据。
例如:哈希值(SHA-256)、对称密钥、加密后的密文、任意文件内容。
存储和处理天然以比特为单位的数据,或位标志(Flags)
例如:硬件寄存器(每个位有特定含义)、网络协议中的位头、权限位图。

编码示例详解

假设我们有一个简单的二进制数据。我们分别用 OCTET STRINGBIT STRING 来表示它。

原始数据(二进制表示): 10101011 11001101 11101111 (3个字节)
原始数据(十六进制表示): AB CD EF

作为 OCTET STRING 编码

  • Tag: 04 (表示OCTET STRING)
  • Length: 03 (后面有3个字节的数据)
  • Value: AB CD EF (直接就是数据本身)

完整的BER编码(十六进制): 04 03 AB CD EF

作为 BIT STRING 编码

现在,我们想用 BIT STRING 来存储完全相同的比特序列

  • Tag: 03 (表示BIT STRING)
  • Length: 04 (后面总共有 1 (初始字节) + 3 (数据字节) = 4 个字节)
  • Value 字段的构成:
    • 初始字节 (Unused_bits):我们的数据是24位,正好是3个完整的字节,没有未使用的位。所以 Unused_bits = 0
    • 数据字节:仍然是 AB CD EF

完整的BER编码(十六进制): 03 04 00 AB CD EF

注意开头的 00,它表示“所有位都被使用”。

更复杂的 BIT STRING 示例

假设我们有一个 11位 长的比特串:10101011 110 (前一个半字节 + 3位)

  • Tag: 03
  • Length: 03 (后面总共有 1 (初始字节) + 2 (数据字节) = 3 个字节)
  • Value 字段的构成:
    • 初始字节 (Unused_bits):我们需要用2个字节(16位)来存放11位数据。16 - 11 = 5个位是未使用的。所以 Unused_bits = 5
    • 数据字节
      1. 第一个字节存放前8位:10101011 -> 0xAB
      2. 第二个字节存放剩下的3位,并在末尾用5个 0 来填充110 + 00000 = 11000000 -> 0xC0。最后5位 00000 是填充,无效。

完整的BER编码(十六进制): 03 03 05 AB C0

解码器看到 Unused_bits=05,就知道最后一个字节 0xC0 (11000000) 中只有前3位 (110) 是有效数据,会自动忽略最后5位的填充。

总结与如何选择

场景 应选择的类型
处理哈希值、密钥、文件内容等天然以字节为单位的数据。 OCTET STRING
处理一个数据,其长度不是8的倍数(例如,13位)。 BIT STRING
表示一组开关/标志/布尔值,其中每个位都有独立含义(例如,权限集合)。 BIT STRING (可以在ASN.1中为其定义具名位,如 {read(0), write(1), execute(2)})
传输或存储来自网络协议或硬件寄存器的原始位图 BIT STRING

简单来说:如果你关心的是字节内容,用 OCTET STRING。如果你关心的是位级模式位标志,或者数据长度不是整数字节,用 BIT STRING


BER与DER的区别与共同点

核心关系

DER 是 BER 的一个子集。 所有有效的 DER 编码同时也是有效的 BER 编码,但反之则不成立。你可以把 DER 看作是 BER 的“严格模式”,它通过添加额外规则来确保对于任何给定的数据内容,只存在一种唯一的编码方式。


共同点

  1. 基础规则:两者都使用相同的 TLV(Tag-Length-Value)三元组 基本结构来编码所有数据。
    • T (Tag): 标识数据的类型(如 INTEGER, OCTET STRING, SEQUENCE)。
    • L (Length): 指明 V 字段的字节长度。
    • V (Value): 包含数据内容本身的字节。
  2. 编码能力:它们都能编码所有 ASN.1 定义的数据类型。
  3. 可读性:编码后的数据都是二进制字节流,通常以十六进制形式展示和分析。

区别(核心在于“唯一性”)

DER 在 BER 的基础上增加了额外的约束,以确保编码的唯一性。这些约束主要为了解决 BER 的灵活性在数字签名等安全场景中带来的问题(因为如果同一内容有多种编码,签名验证会失败)。

特性 BER (Basic Encoding Rules) DER (Distinguished Encoding Rules)
哲学 灵活。提供多种编码方式,只要符合语法即可。 严格。一种值只有一种编码方式。
长度字段 (L) 可以使用 不定长形式(长度字节设为 80,用两个 00 00 字节结束)。 必须使用确定长形式(最短可能的形式)。
布尔值 (BOOLEAN) 任何非零值(例如 FF)都可以表示 TRUE TRUE 必须 编码为 FF
位串 (BIT STRING) 末尾未使用的比特数可以是任何值,填充位可以是 0 或 1。 末尾未使用的比特数必须为 0,填充位必须为 0。
集合 (SET) SET 中的元素可以按任意顺序编码。 SET 中的元素必须按标签值的升序进行编码。
时间 (UTCTime) 可以使用本地时间加偏移量(如 +0800)。 必须使用 UTC 时间并以 Z 结尾。
主要用途 网络传输(效率优先,灵活)。 数字签名、数字证书(如 X.509)、安全数据存储(唯一性优先)。

示例数据与详细说明

让我们用一个简单的 ASN.1 结构来演示区别:

ASN.1 定义:

MyRecord ::= SEQUENCE {
id INTEGER,
flag BOOLEAN,
data OCTET STRING
}

数据值:

  • id: 300 (十六进制 0x012C)
  • flag: TRUE
  • data: 0x4869 (字符串 “Hi” 的 ASCII 字节)

BER 编码示例(多种可能)

BER 编码可能有多种正确形式,以下是两种常见的:

BER 编码可能性 1 (最直接的编码)

30 06           // SEQUENCE (Tag=0x30), Length=6 bytes
02 02 01 2C // INTEGER (Tag=0x02), Length=2, Value=0x012C (300)
01 01 FF // BOOLEAN (Tag=0x01), Length=1, Value=0xFF (TRUE)
04 02 48 69 // OCTET STRING (Tag=0x04), Length=2, Value="Hi"

完整Hex: 30 06 02 02 01 2C 01 01 FF 04 02 48 69

BER 编码可能性 2 (使用长形式长度)

长度字段 06 本身可以用更长的形式编码(虽然不必要,但符合 BER 规则)。

30 81 07        // SEQUENCE, Length=7 bytes (使用了长形式‘81’,表示后面1个字节代表长度)
02 02 01 2C
01 01 FF
04 02 48 69 // 注意:实际内容仍是7字节,但长度字段多用了1字节来描述‘07’

完整Hex: 30 81 07 02 02 01 2C 01 01 FF 04 02 48 69

BER 编码可能性 3 (使用不同的BOOLEAN值)

TRUE 也可以编码为 01 01 01 (任何非零值,如 0x01,都是合法的 TRUE)。

30 06
02 02 01 2C
01 01 01 // BOOLEAN Value=0x01 (也是TRUE)
04 02 48 69

完整Hex: 30 06 02 02 01 2C 01 01 01 04 02 48 69

以上三种编码对于 BER 来说都是正确的,它们解码后都会得到完全相同的数据值 (300, TRUE, "Hi")

DER 编码示例(唯一一种可能)

DER 的规则消除了所有灵活性,只允许一种编码方式。

DER 编码 (唯一合法的形式):

30 06           // SEQUENCE, 长度字段必须使用最短形式 (这里是06)
02 02 01 2C // INTEGER
01 01 FF // BOOLEAN 的值必须是 0xFF
04 02 48 69 // OCTET STRING

完整Hex: 30 06 02 02 01 2C 01 01 FF 04 02 48 69

为什么不能是其他形式?

  • 长度字段:必须使用最短形式。06 是最短的,所以不能使用 81 07
  • BOOLEANTRUE 必须编码为 FF,不能是 01 或其他非零值。
  • 元素顺序SEQUENCE 的元素顺序在定义时已固定,所以必须按 id, flag, data 的顺序编码。如果是 SET,DER 要求必须按标签号排序。

总结与类比

特性 BER DER
核心目标 灵活性、效率 唯一性、确定性
类比 同一句话的不同说法
“你好吗?”
“怎么样,你好吗?”
“你,好吗?”
(意思都一样)
官方印章或签名
只有一种唯一的、公认的写法。任何细微差别都会导致无效。
如何选择 用于网络传输协议(如 SNMP, LDAP),其中编码/解码效率或灵活性可能更重要。 用于数字签名数字证书(X.509)、区块链安全凭证,任何需要确保二进制数据绝对唯一的场景。

简单来说:需要签名或防篡改?用 DER。只是普通传输数据?BER 或其他更高效的编码规则(如 PER)可能更合适。


国际字母表5

IA5String 中使用的 国际字母表5(International Alphabet No. 5) 本质上就是全球标准化组织定义的 7位ASCII字符集 的一个官方名称。

具体内容与定义

国际字母表5(IA5) 由国际电信联盟(ITU-T)在其建议书 T.50 中定义。它的目的是为国际信息交换提供一个标准化的字符集。

其核心内容包括:

  1. 编码结构:它是一个 7位 编码的字符集。这意味着它最多可以定义 128 (2^7) 个字符。
  2. 字符布局:IA5 的字符编码表与 ANSI X3.4(即美国国家标准协会的 ASCII)完全一致。也就是说,在代码点 0 到 127 这个范围内,IA5 和 ASCII 是一字不差、完全相同的

因此,你可以完全地将 IA5String 理解为 “只能包含7位ASCII字符的字符串”

IA5 (ASCII) 字符集的具体内容

IA5/ASCII 的 128 个字符可以分为两大类:

1. 控制字符 (代码 0-31 和 127)

这些是不可打印字符,用于控制数据流、外设或格式化。例如:

  • NUL (0): 空字符
  • LF (10): 换行符 (\n)
  • CR (13): 回车符 (\r)
  • ESC (27): 退出符
  • DEL (127): 删除字符

2. 图形字符 (代码 32-126)

这些是可打印字符。

  • 空格符 (32):
  • 数字 (48-57): 0 1 2 3 4 5 6 7 8 9
  • 大写字母 (65-90): A B CX Y Z
  • 小写字母 (97-122): a b cx y z
  • 标点符号和特殊符号 (其余部分):
    • ! " # $ % & ' ( ) * + , - . /
    • : ; < = > ? @
    • [ \ ] ^ _ `
    • { | } ~

为什么这一点很重要?

在 ASN.1 和许多通信协议(如电子邮件、X.509 证书)中,选择 IA5String 而不是更通用的 UTF8String 是一个故意的约束

  • 保证互操作性:通过强制使用这个最小公分母字符集,可以确保所有系统,无论其本地字符编码如何,都能正确无误地解析和处理字符串数据。一个只理解 IA5 的老旧系统可以安全地处理 IA5String
  • 协议规范要求:许多 RFC 标准明确要求某些字段必须使用 IA5String。最典型的例子是电子邮件地址域名
    • 在 RFC 5280 (互联网证书规范) 中,证书的 emailAddress 属性被定义为 IA5String
    • 这是因为电子邮件地址的本地部分(@之前)和域名(@之后)的正式标准只允许使用有限的 ASCII 字符集(尽管现在有一些扩展)。

关键区别:IA5String vs. UTF8String

特性 IA5String UTF8String
字符集 仅限于 7位 ASCII (128个字符) 完整的 Unicode 字符集 (超过百万个字符)
编码 每个字符固定为 1个字节 一个字符可能占用 1到4个字节
用途 电子邮件、域名、老协议、需要严格兼容性的场景 现代应用、需要支持任何语言(中文、阿拉伯文、表情符号)的场景

示例与反例

  • 有效的 IA5String:

    • "hello@example.com"
    • "CN=Test, O=Company"
    • "1.2.3.4.5.6"
    • "2023-01-01"
  • 无效的 IA5String (会导致编码/验证错误):

    • "café" (字符 é 的代码点 233 超出了 7位范围)
    • "中文" (任何非拉丁字母的文字)
    • "€100" (欧元符号 不在 ASCII 中)

总结:IA5String 就是 ASN.1 对 7位 ASCII 字符集的正式称呼。 它通过限制字符集来保证最大程度的兼容性和确定性,常用于网络标识符(如邮件地址、域名)和传统系统中。对于需要国际化的文本,应使用 UTF8String