这次写一个小程序来解析 X.509 证书的内容,主要涉及到 Base64 解码、ANS.1 结构的 DER 解码和 Object ID
🚀 代码: Github
X.509 证书结构描述
X.509 证书有多种常用的文件扩展名,代表着不同形式的数据编码以及内容
其中常见的有(来自 Wikipedia):
.pem
– DER编码的证书再进行Base64编码的数据.cer
,.crt
,.der
– 通常是DER二进制格式的,但 Base64 编码后也很常见。.p7b
,.p7c
– PKCS#7 SignedData structure without data, just certificate(s) or CRL(s).p12
– PKCS#12格式,包含证书的同时可能还有带密码保护的私钥.pfx
– PFX,PKCS#12 之前的格式(通常用 PKCS#12 格式,比如那些由IIS产生的 PFX 文件)
这里我主要解析的是DER
二进制格式经过 Base64 编码后的数据
其中证书的组成结构标准用ASN.1
来进行描述,有着不同的版本,其中V3
版本的基本结构如下(Wikipedia):
- 证书
- 版本号:标识证书的版本(版本 1、版本 2 或是版本 3)
- 序列号:标识证书的唯一整数,由证书颁发者分配的本证书的唯一标识符
- 签名算法:用于签证书的算法标识,由对象标识符加上相关的参数组成,用于说明本证书所用的数字签名算法
- 颁发者:证书颁发者的可识别名(DN)
- 证书有效期 :此日期前无效 - 此日期后无效
- 主体:证书拥有者的可识别名
- 主体公钥信息
- 公钥算法
- 主体公钥
- 颁发者唯一身份信息(可选项)
- 主体唯一身份信息(可选项)
- 扩展信息(可选项)
- 发行者密钥标识符:证书所含密钥的唯一标识符,用来区分同一证书拥有者的多对密钥
- 密钥使用:指明(限定)证书的公钥可以完成的功能或服务,如:证书签名、数据加密等
- CRL 分布点
- 私钥的使用期
- 证书策略:由对象标识符和限定符组成,这些对象标识符说明证书的颁发和使用策略有关
- 策略映射
- 主体别名:指出证书拥有者的别名,如电子邮件地址、IP 地址等
- 颁发者别名:指出证书颁发者的别名
- 主体目录属性:证书拥有者的一系列属性
- 证书签名算法
- 数字签名
数据结构
Base64
为了在互联网上传输,大多数证书都是采用 Base64 编码,以可打印字符的形式表示二进制数据流。
Base64 编码由 RFC 1421 和 RFC 2045 定义,一共有 64 个可打印字符,因此每六个比特表示为一个单元,以 64 种字符表示二进制数据。
解码
首先取出一组(4 个字符),按照其值在ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
中位置获得其代表的值,然后依次从高位到低位放入一个 24 位的缓冲区中,最后将其划分为三个八位的字符。
下面是维基百科上一个简单的例子
在 C++中,只需要一个简单的循环,每次以原数据的 4 个单元为一组,将其转换成 3 个八位的数据。
1 | byte text[len] = {0}; |
ANS.1
经过 Base64 解码后,就是证书中的内容了,按照标准,这一部分的内容使用ANS.1
表示。
ANS.1 是一套描述数据表示、编码、传输、解码的灵活的记法,是一套正式、无歧义和精确的规则。我们可以使用 ANS.1 描述独立于计算机硬件的对象结构。
对于 X.509 证书标准,存在以下的结构:
1 | Certificate ::= SEQUENCE { |
DER
证书中的 ANS.1 数据是以 DER 编码而来的。
具体的文档可以查看http://luca.ntop.org/Teaching/Appunti/asn1.html
DER 为 ANS.1 类型定义了一种唯一的编码方案。
对于每一种数据类型,其 DER 编码由以下四部分组成:
- 类型字段:tag(T)
- 长度字段:length(L)
- 值字段:value(V)
- 结束表示字段
类型字段
类型由一个字节(8 位)大小的数据表示
其中高两位(8-7)表示了 TAG 的类型:
00
: universal01
: application10
: context-specific11
: private
第六位表示是否位结构化类型:
0
: 简单类型1
:结构类型
剩余的五位(5-1)就是表示具体的类型
比较常用的类型有
Type | Tag number (decimal) | Tag number (hexadecimal) |
---|---|---|
INTEGER |
2 | 02 |
BIT STRING |
3 | 03 |
OCTET STRING |
4 | 04 |
NULL |
5 | 05 |
OBJECT IDENTIFIER |
6 | 06 |
SEQUENCE and SEQUENCE OF |
16 | 10 |
SET and SET OF |
17 | 11 |
PrintableString |
19 | 13 |
T61String |
20 | 14 |
IA5String |
22 | 16 |
UTCTime |
23 | 17 |
长度字段
长度字段,有两种编码格式。
若长度值小于等于 127,则用一个字节表示,bit8 = 0, bit7-bit1 存放长度值
若长度值大于 127,则用多个字节表示,可以有 2 到 127 个字节。第一个字节的第 8 位为 1,其它低 7 位给出后面该域使用的字节的数量,从该域第二个字节开始给出数据的长度,高位优先。
还有一种特殊情况,这个字节为0x80
,表示数据块长度不定,由数据块结束标识结束数据块。
值字段
根据不同类型,值字段可以表示不同的数据,如 Ascii 字符、16 进制字符串、OID
Object ID
在证书中的各种值中,比较常见的就是Object ID
(对象标识符)
对象标识符是由国际电信联盟(ITU)和 ISO/IEC 标准化的标识符机制,用于表示对一个对象、概念或者事务的全球化的一种标识。
OID 是由一连串以点.
分隔的数字组成,
比如:1.3.6.1.4.1.343
表示的是
- 1: ISO assigned OIDs(ISO 指定的 OID)
- 1.3 : ISO Identified Organization (ISO 认证组织)
- 1.3.6: US Department of Defense (美国国防部)
- 1.3.6.1 :Internet (因特网)
- 1.3.6.1.4: Internet Private (互联网专用)
- 1.3.6.1.4.1 :IANA-registered Private Enterprises (私营企业)
- 1.3.6.1.4.1.343 :Intel Corporation (英特尔公司)
不同的 OID 表示的信息可以在网上查到 https://www.alvestrand.no/objectid/1.3.6.1.4.1.343.html
在 X.509 证书里面,证书颁发者的地区、国家就是使用 OID 来表示的
转换
那么我们如何将二进制数据转换成 OID 标识呢?
在 X.509 的值的二进制数据中,一个 OID 表示的值如下
00101010 10000110 01001000 10000110 11110111 00001101 00000001 00000001 00001011
将其分为 8 位大小的块,每一块的第一位表示是否为结束块
如果是 1 就和后面的连在一起
如果是 0 就是一个独立的块
得到以下的 7bit 的 6 个块
0101010 00001101001000 000011011101110001101 0000001 0000001 0001011
然后化为 10 进制
42 840 113549 1 1 11
对第一位X
进行特殊处理,使其变为两个位
- 第一位:
min([X/40], 2)
- 第二位:
X - 40 * 第一位
因此42 = 40 * 1 + 2
,可以分解为 1 和 2,然后得到以下的 OID
1.2.840.113549.1.1.11
在网上查找数据库,得出以下数据
1.2.840.113549.1.1.11 - sha256WithRSAEncryption
- 1.2.840.113549.1.1 - PKCS-1
- 1.2.840.113549.1 - PKCS
- 1.2.840.113549 - RSADSI
- 1.2.840 - USA
- 1.2 - ISO member body
- 1 - ISO assigned OIDs
- Top of OID tree
因此,这个标识符表示这个证书使用的加密算法为sha256WithRSAEncryption
在 C++里面,只要了解其结构,就可以很简单将二进制数据转换成 OID 字符串
1 | String title = ""; |
解析
下面对我的网站www.zhenly.cn
的证书数据逐一解析
1 | 30 82 05 93 // SEQUENCE类型(30), 长度的长度2(82),长度1427(0593) Certificate:: |
C 语言源代码
代码仓库: Github
读取文件
首先读取证书文件,将其的
-----BEGIN CERTIFICATE-----
和-----END CERTIFICATE-----
之中的 Base64 编码读取到字符串中
1 | // main.cpp |
Base64 解码
1 | // x509.cpp |
ANS.1 解码
解析
1 | parseANS(text, 0, len); |
这里使用递归的方式解析将二进制数据根据 ANS.1 中的长度将其划分为不同的部分进行解析
1 | typedef struct { |
显示数据
我们把一些证书用到的 OID 存储到 MAP 中,然后显示出其数据的表示的意义
1 | void printTime(string timeStr) { |
编译运行输出结果
用这个的程序解析几大网站的 X.509 证书:
Github
Baidu
Other
解析多个网站信息,程序都可以正常运行