尾声
上节中我们已经找到问题的罪魁祸首: ASN1 对 INTEGER 类型的编解码出现问题
google 一下,查到一篇文件《ASN.1/BER/DER 编码子集入门指南》,其中关于 INTEGER 的部分,综述如下
INTEGER 类型表示任意的整数。INTEGER 值可以为正数、负数或 0,具有任意大小,采用简单定长编码
在 X.509 证书中 INTEGER 类型用于表示证书的版本号和序列号
具体编码上,INTEGER 的内容 V 用补码表示(记得TLV格式吗),并使用最少的字节,例如数值 0 编码为一个字节: 0x00
其完整的 ASN1 编码就是 02 01 00,共 3 个字节(TLV)
在我们碰到的问题中,证书序列号是 00 00 a2 42 4a a2 6a 51 df -- 恰恰不是简单定长编码(多了个 00),这就是症结所在
OpenSSL 0.9.8e 版本在对该字段进行编/解码过程中,严格按此规范执行,导致最后验证出错
我们可以再举个例子直接佐证:对问题证书运行 openssl x509 命令(0.9.8e),将其格式由 DER 转换为 PEM
转换得到的证书 Windows 将报“证书可能已损坏,或已被改动。”错误
查看新证书序列号,果然,被截为 00 a2 42 4a a2 6a 51 df
如果运行较新版本的 openssl x509 命令(比如 1.0.0e), 转换后的证书则没有问题
个人认为,作为序列号的 INTEGER, 不必考虑在数值上的正负,事实上,只要考虑其编码的二进制表示就可以。
在 0.9.8.e 版本中,这个简单的道理没有被采纳,反而在序列号的 INTEGER 编/解码上弄得很复杂,最终栽了跟头。
为什么 OpenSSL 1.0.0e 可以正常处理具有特殊序列号的证书?
其实思路很简单 -- Encode 为 DER 时不再折腾,直接使用先前 Decode 时保存的 DER 编码(来自文件)
下面我们简单讨论下 1.0.0e 版本的实现,与 0.9.8e 版本相比,它在结构 X509_CINF 中新增成员 enc, 如下

1 typedef struct x509_cinf_st 2 { 3 ASN1_INTEGER *version; /* [ 0 ] default of v1 */ 4 ASN1_INTEGER *serialNumber; 5 X509_ALGOR *signature; 6 X509_NAME *issuer; 7 X509_VAL *validity; 8 X509_NAME *subject; 9 X509_PUBKEY *key; 10 ASN1_BIT_STRING *issuerUID; /* [ 1 ] optional in v2 */ 11 ASN1_BIT_STRING *subjectUID; /* [ 2 ] optional in v2 */ 12 STACK_OF(X509_EXTENSION) *extensions; /* [ 3 ] optional in v3 */ 13 ASN1_ENCODING enc; // 1.0.0e 版本中新增 14 } X509_CINF; 15 16 typedef struct ASN1_ENCODING_st 17 { 18 unsigned char *enc; /* DER encoding */ -- DER 编码缓冲区 19 long len; /* Length of encoding */ 20 int modified; /* set to 1 if 'enc' is invalid */ 21 } ASN1_ENCODING;
程序在 d2i_X509 函数中调用 asn1_enc_save 函数, 后者会直接保存证书信息(tbsCertificate 部分)的 DER 编码,如下

1 int asn1_enc_save(ASN1_VALUE **pval, const unsigned char *in, int inlen, 2 const ASN1_ITEM *it) // it 指向 X509_CINF_it 中返回的静态变量,见下面 3 { 4 ASN1_ENCODING *enc; 5 enc = asn1_get_enc_ptr(pval, it); 6 static ASN1_ENCODING *asn1_get_enc_ptr(ASN1_VALUE **pval, const ASN1_ITEM *it) 7 { 8 const ASN1_AUX *aux; 9 if (!pval || !*pval) 10 return NULL; 11 12 aux = it->funcs; 13 // 0.9.8e 版本中 funcs 成员指向 0 14 // 1.0.0e 版本中则指向 X509_CINF_aux(新增变量, 不再列出, 请查看定义), 见下面的对比 15 // const ASN1_ITEM * X509_CINF_it(void) { 16 // static const ASN1_ITEM local_it = { 17 // 0x1, 16, X509_CINF_seq_tt, sizeof(X509_CINF_seq_tt) / sizeof(ASN1_TEMPLATE), 18 // 0/&X509_CINF_aux, sizeof(X509_CINF), "X509_CINF" 19 // }; 20 // return &local_it; 21 // } 22 23 if (!aux || !(aux->flags & ASN1_AFLG_ENCODING)) 24 return NULL; // 0.9.8e 从此处返回 -- aux == 0 25 return offset2ptr(*pval, aux->enc_offset); // 1.0.0e 返回 X509_CINF.enc 成员的地址 26 } 27 28 if (!enc) // 0.9.8e: 返回 -- enc == 0, 后续的 asn1_template_ex_d2i() 在调用 asn1_ex_c2i 时出错 29 return 1; 30 31 // 1.0.0e: 走到此处 32 // 直接保存 DER 编码到 enc->enc 缓冲区, DER 编码在后面的 asn1_enc_restore 中恢复 33 // 对于证书验证而言,涉及的 DER 编码是 tbsCertificate 部分,对应的数据结构是 X509_CINF 34 if (enc->enc) 35 OPENSSL_free(enc->enc); 36 enc->enc = OPENSSL_malloc(inlen); 37 if (!enc->enc) 38 return 0; 39 memcpy(enc->enc, in, inlen); 40 enc->len = inlen; 41 enc->modified = 0; 42 43 return 1; 44 }
下面是保存证书 DER 编码的堆栈
> asn1_enc_save
ASN1_item_ex_d2i
asn1_template_noexp_d2i
asn1_template_ex_d2i
ASN1_item_ex_d2i
ASN1_item_d2i
d2i_X509
下面是恢复证书 DER 编码的堆栈
> asn1_enc_restore
ASN1_item_ex_i2d
asn1_item_flags_i2d
ASN1_item_i2d
ASN1_item_verify
X509_verify
有兴趣的读者可以去查看代码实现,并追踪调试。
最后,回答一下我们在前面提出的问题:这个证书是怎么得到的?
事实上,这是笔者在 N 年前碰到的一个问题,当时碰到一张证书,其序列号以两个连续的 0x00 开头
刚好用 OpenSSL 0.9.8e 验证,遇到签名失败提示(幸好当时没出 1.0.0e 版本,否则就看不到这个系列了:-/ )
现在完成的这个系列,算是纪念一下那段经历吧。
这张问题证书,就是在前期“从数学到密码学”系列中的证书 sslclientcert 基础上, 手工修改了序列号的一个字节,即
把 00 9d a2 42 4a a2 6a 51 df 改为 00 00 a2 42 4a a2 6a 51 df, 然后再计算新的签名值,最后拼装得到的一个手工打造版证书。
在解决每一个具体问题的过程中,都应该用心去寻找扩展为一类问题的通用“解决方案”。
这也是每个真正的程序员所应该追求的。
至此,本系列全部结束。