背景
前几周,协助同事解决了SM2软签名的需求,其流程可参考终于解决了!!! 基于GmSSL的SM2签名算法及思路分享。
但是在解决这个问题的过程中,让我想起了一些不好的回忆:曾经在大众项目中,也接触过椭圆曲线算法签名。其中因为平台下发的公钥格式,由于双方理解不一致,导致最终调试很久,并且自己也处于懵懵懂懂的状态。
在协助同事解决软签名的过程中,也遇到了类似问题:证书内容与公钥私钥的关系,如何从.pem
格式文件中,提取我们需要的公私钥。
而本文的目的就是介绍.pem
文件中的数据格式以及如何解析。作为上层应用开发者可能并不需要了解,知道如何调用接口即可。但是对于我而言,这就是我心中的痛,所以我一定要了解:如何从.pem
格式的私钥证书中,获取公钥、私钥信息。
有兴趣的朋友不妨可以了解一下。本文将以下列私钥文件举例描述:
xieyihua@xieyihua:~/GmSSL-master$ cat prikey.pem
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgtJNqd4cc2GtzhhR/
Up73/B5aPXainNVCT3m9Pz09m2ygCgYIKoEcz1UBgi2hRANCAATX3mX8KCvFzcPM
7vB3Ys62UoJFNxreM0RwDxTCBJ00UhnfLTN347ELOQpxeCuWG5spGyacvvlnehN3
nNwtem+V
-----END PRIVATE KEY-----
xieyihua@xieyihua:~/GmSSL-master$
基础内容学习
在分析上述文件前,我们需要了解以下内容。
pem 与 der
- PEM:Privacy-Enhanced Mail,隐私增强邮件。PEM格式文件后缀通常为".pem"、“.cer”、“.crt”、“.key”,后缀名并不会影响 PEM 格式文件的解析。
- DER:Distinguished Encoding Rules,可分辩编码规则。DER格式文件后缀通常为 “.der” 和 “.cer”,后缀名并不会影响 DER 格式文件的解析。
其中pem
和der
的关系:
ASN.1 ----->(序列化)-----> DER -----(Base64编码)-----> PEM
即:对ASN.1
进行序列化后,得到一个二进制串,这就是DER
格式。再将DER
格式数据进行base64编码,加上PEM
特有的头、尾(本文中-----BEGIN PRIVATE KEY-----
和-----END PRIVATE KEY-----
),即可得到私钥文件内容。
由上可知,我们只需要逆向解析,得到DER
格式数据,按照ASN.1
的规则进行解析即可了解文件中的各字段内容。
初识 ANS.1
在现代通信和信息技术领域,数据的精确表示和高效传输是至关重要的。ASN.1(Abstract Syntax Notation One)作为一种国际标准化组织(ISO)和国际电信联盟(ITU)制定的国际标准,提供了一种抽象的方法来描述、编码、解码和传输数据。ASN.1的核心组成部分之一是BER(Basic Encoding Rules),它定义了一种将ASN.1数据结构转换为二进制形式的编码规则。
而DER是BER的特殊版本,主要应用于安全领域中,对一些场景具有唯一性,但是解析规则都是一样的。
BER编码基本格式
BER(Basic Encoding Rules)是一种用于描述ASN.1(Abstract Syntax Notation One)数据的编码规则。BER编码使用一种TLV(Type-Length-Value)的结构方法编码。
即,BER编码的基本结构由以下三个部分组成:
- 类型域(Type)
- 长度域(Length)
- 内容域(Value)
其中类型(Type)部分又有三部分组成:
- 标签类型(Class)
- 构造类型(P/C)
- 标签号(Tag)
其中数据域(Value)也存在两种情况:
- 单一的数据类型,如
int
和Double
类型; - 复杂的自定义类型结构,对于一种数据结构中包含了其它数据结构,往往就是一些简单类型。类似C语言中的结构体。如下:
类型域
类型域(Type)用于标识数据的类型和类别。类型域编码包含三个部分:类(Class)、构造类型(PC, Primitive/Constructed)、和标签号(Tag Number)。下面详细说明这三个部分的编码:
类型域字节的格式如下:
分析:
标签类型有四种,分别是:
- Universal(通用类别):值为 00。这类数据类型是由ASN.1标准定义的,如整数、布尔值、位串等。它们是通用的,可以在任何上下文中使用。
- Application(应用类别):值为 01。这类数据类型是特定应用程序定义的,它们在特定的应用上下文中具有特殊的含义。
- Context-specific(上下文相关类别):值为 10。这类数据类型通常用于结构化数据中的标记字段,它们的含义依赖于特定的上下文或结构。
- Private(私有类别):值为 11。这类数据类型是为私有的或组织特定的用途而定义的,它们在标准中没有定义,通常由个别组织内部使用。
构造类型有两种,分别是:
- 原始类型(Primitive):值为0。用于表示ASN.1中的基本数据类型,如INTEGER、OCTET STRING、BOOLEAN等
- 构造类型(Constructed):值为1。用于表示ASN.1中的复合数据类型,如
SEQUENCE
、SET
、SEQUENCE OF
、SET OF
等。或创建复杂的数据结构。
标签号的作用表示具体的数据类型或结构。它的取值范围有两种:
- 简单标签号(0~30)。对于标签号在 0 到 30 之间的情况,直接在类型域的第4-8位表示。例如:
通用类(Universal)布尔类型(Boolean):0000 0001,即 0x01
应用类(Application)整数类型(Integer):0100 0010,即 0x42
- 复杂标签号(>=31)。对于标签号大于等于 31 的情况,第4-8位全为 1(即 0b11111),并且标签号以基于 7 位的块形式在后续字节中表示,每个字节的最高位为 1,表示后续有更多字节,最后一个字节的最高位为 0。例如:
标签号 31:0b1111 1111 0011 1111,即 0x1F 0x1F
标签号 128:0b1111 1111 1000 0001 0000 0000,即 0x1F 0x81 0x00
ASN.1 已定义的标签号如下:
类型 | 标签号 |
---|---|
BIT STRING | 00000011(0x03) |
BOOLEAN | 00000001(0x01) |
INTEGER | 00000010(0x02) |
Null | 00000101(0x05) |
对象标识符 | 00000110(0x06) |
八进制字符串 | 00000100(0x04) |
BMPString | 00011110(0x1E) |
IA5String | 00010110(0x16) |
PrintableString | 00010011(0x13) |
TeletexString | 00010100(0x14) |
UTF8String | 00001100(0x0C) |
SEQUENCE | 00110000(0x30) |
序列 | 00110000(0x30) |
SET | 00110001(0x31) |
SET OF | 00110001(0x31) |
长度域
在 BER(Basic Encoding Rules)编码中,长度域用于指示随后的值域(Value)的长度。长度域的编码有主要两种形式:短形式和长形式。下面是对这两种形式的详细说明:
- 短形式
短形式用于表示长度小于 128 字节(即 0 到 127)的情况。在这种形式中,长度域仅占一个字节。该字节的最高位(第八位)为 0,低七位表示长度的值。例如:
若长度为 5,则长度域为 0000 0101(即 0x05)。
若长度为 127,则长度域为 0111 1111(即 0x7F)。
- 长形式
长形式用于表示长度大于等于 128 字节的情况。在这种形式中,长度域的第一个字节的最高位(第八位)为 1,低七位表示后续长度字节的个数。例如:
若长度为 128,则长度域为 1000 0001(表示后续有 1 个字节)加上 1000 0000(表示长度为 128),即 0x81 0x80。
若长度为 300,则长度域为 1000 0010(表示后续有 2 个字节)加上 0000 0001 0010 1100(即 300),即 0x82 0x01 0x2C。
内容域
在 BER(Basic Encoding Rules)编码中,内容域(Value)包含实际的数据信息,其编码方式取决于数据的类型。不同数据类型有不同的编码规则。以下是一些常见数据类型的编码方式:
- 布尔型(BOOLEAN)
布尔型值使用一个字节表示:TRUE 编码为 0xFF、FALSE 编码为 0x00;
- 整型(INTEGER)
整型值以大端顺序(高字节在前)编码,使用最少的字节数来表示值。如果最高有效位为 1,则需要在前面加一个 0x00 以避免符号扩展。例如:
0 编码为 0x00
127 编码为 0x7F
128 编码为 0x00 0x80
-1 编码为 0xFF
- 位串(BIT STRING)
位串由一个初始字节和实际数据组成。初始字节表示未使用的位数。实际数据按字节顺序排列。例如:
0x01101011(假设全用)编码为 0x00 0x6B
0x01101010(未使用1位)编码为 0x01 0x6A
- 字符串(OCTET STRING)
字符串(八位字节串)按字节顺序直接编码。例如:
"Hello" 编码为 0x48 0x65 0x6C 0x6C 0x6F
- NULL
NULL 值没有内容,其长度为 0。因此,NULL 值的编码只是标记和长度,值为空。例如:
NULL 编码为 0x05 0x00
- 对象标识符(OBJECT IDENTIFIER)
对象标识符使用变量长度编码。前两个节点由 (X * 40) + Y
公式表示,后续节点使用基于 7 位的块形式编码,最高位为 1 表示有后续字节。例如:
1.2.840.113549 编码为 0x2A 0x86 0x48 0x86 F7 0x0D(1*40 + 2 = 42, 840 = 0x86 0x48, 113549 = 0x86 0xF7 0x0D)
- 序列(SEQUENCE)
序列包含一个或多个元素,每个元素按其类型编码,然后依次排列。例如,一个包含一个整数和一个字符串的序列:
整数:42 编码为 0x02 0x01 0x2A
字符串:"Hi" 编码为 0x04 0x02 0x48 0x69
整个序列编码为:0x30(标记) 0x07(长度) 0x02 0x01 0x2A(整数) 0x04 0x02 0x48 0x69(字符串)
分析
通过以上基础概念的了解,我们可以尝试分析pem
文件了。
- 查看
pem
文件内容,通过base64反编码,获取der数据。在线base64编解码
即der数据为:
308193020100301306072A8648CE3D020106082A811CCF5501822D047930770201010420B4936A77
871CD86B7386147F529EF7FC1E5A3D76A29CD5424F79BD3F3D3D9B6CA00A06082A811CCF5501822D
A14403420004D7DE65FC282BC5CDC3CCEEF07762CEB6528245371ADE3344700F14C2049D345219DF
2D3377E3B10B390A71782B961B9B291B269CBEF9677A13779CDC2D7A6F95
- 根据BER编码格式进行解析
因为数据域是构造类型,因此需要再解析,以此往复,最终解析格式如下:
30 类型域 构造类型
81 93 长度域 0x93 = 147
02 通用类型 INTEGER
01 长度为1
00 数据域
30 构造类型
13 长度19
06 通用类型 对象标识符
07 长度 7
2A8648CE3D0201 数据域
06 通用类型 对象标识符
08 长度 8
2A811CCF5501822D 数据域
04 通用类型 八进制字符串
79 长度 121
30 构造类型
77 长度 119
02 通用类型 INTEGER
01 长度 1
01 数据域
04 通用类型 八进制字符串
20 长度 32
B4936A77871CD86B7386147F529EF7FC1E5A3D76A29CD5424F79BD3F3D3D9B6C 数据域
A0 构造类型
0A 长度 10
06 通用类型 对象标识符
08 长度 8
2A811CCF5501822D 数据域
A1 构造类型
44 长度 68
03 通用类型 BIT STRING
42 长度 66
0004D7DE65FC282BC5CDC3CCEEF07762CEB6528245371ADE3344700F14C2049D345219DF2D3377E3B10B390A71782B961B9B291B269CBEF9677A13779CDC2D7A6F95 数据域
- 根据各BER编码格式解析各数据域(建议自己手动计算一遍),得到:
---0 //指定私钥信息(PrivateKeyInfo)结构中的版本号
------1.2.840.10045.2.1 // 公钥对象标识符
-------1.2.156.10179.1.301 //SM2 算法对象标识符
-------1 //椭圆曲线(Elliptic Curve,简称 EC)私钥结构的版本号
-------B4936A77871CD86B7386147F529EF7FC1E5A3D76A29CD5424F79BD3F3D3D9B6C//私钥
----------1.2.156.10179.1.301//SM2 算法对象标识符
----------04D7DE65FC282BC5CDC3CCEEF07762CEB6528245371ADE3344700F14C2049D345219DF2D3377E3B10B390A71782B961B9B291B269CBEF9677A13779CDC2D7A6F95 //公钥,其中首字节04 并不是公钥信息,而是表示随后的数据是一个非压缩的点
至此,心中一阵舒畅。终于解决了心中的不快。还请注意,该pem私钥文件,并没有加密。所以能够解析出实际数据内容
总结
详细介绍了如何从.pem格式的私钥证书中提取公钥和私钥信息。.pem文件是私钥证书的常见格式,而der格式是.pem文件中数据的基本格式。文章通过介绍pem和der的关系、ASN.1、BER编码格式等基础知识,帮助读者理解如何解析.pem文件中的数据。
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途