本文由Jacob Hoffman-Andrews发布在Let's Encrypt博客。
Let's Encrypt最近推出了SCT嵌入证书。 此功能允许浏览器检查证书是否已提交给证书透明度日志。 作为发布的一部分,我们彻底审查了证书中签名证书时间戳(SCT)的编码与相关规范相符。 在这篇文章中,我将深入细节。 您将了解有关X.509,ASN.1,DER和TLS编码的更多信息,并提及相关的RFC。
证书透明度提供了三种将SCT传送到浏览器的方法:在TLS扩展中,在装订的OCSP中或嵌入证书中。 我们选择实施嵌入方法,因为它仅适用于让我们无需额外工作即可加密订阅者。 在SCT嵌入方法中,我们向一组CT日志提交带有毒扩展的“precertificate”,并取回SCT。 然后,我们根据先验证书颁发真实证书,并进行两项更改:删除毒扩展,并将先前获得的SCT添加到另一个扩展中。
给定证书,我们首先查找SCT列表扩展。 根据CT(RFC 6962第3.3节),SCT列表的扩展OID是1.3.6.1.4.1.11129.2.4.2
。 OID(对象ID)是一系列整数,分层分配和全局唯一。 它们广泛用于X.509中,例如用于唯一标识扩展。
我们可以下载一个示例证书,并使用OpenSSL查看它(如果您的OpenSSL是旧的,它可能不会显示详细信息):
$ openssl x509 -noout -text -inform der -in Downloads/031f2484307c9bc511b3123cb236a480d451
...
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1(0)
Log ID : DB:74:AF:EE:CB:29:EC:B1:FE:CA:3E:71:6D:2C:E5:B9:
AA:BB:36:F7:84:71:83:C7:5D:9D:4F:37:B6:1F:BF:64
Timestamp : Mar 29 18:45:07.993 2018 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:44:02:20:7E:1F:CD:1E:9A:2B:D2:A5:0A:0C:81:E7:
13:03:3A:07:62:34:0D:A8:F9:1E:F2:7A:48:B3:81:76:
40:15:9C:D3:02:20:65:9F:E9:F1:D8:80:E2:E8:F6:B3:
25:BE:9F:18:95:6D:17:C6:CA:8A:6F:2B:12:CB:0F:55:
FB:70:F7:59:A4:19
Signed Certificate Timestamp:
Version : v1(0)
Log ID : 29:3C:51:96:54:C8:39:65:BA:AA:50:FC:58:07:D4:B7:
6F:BF:58:7A:29:72:DC:A4:C3:0C:F4:E5:45:47:F4:78
Timestamp : Mar 29 18:45:08.010 2018 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:46:02:21:00:AB:72:F1:E4:D6:22:3E:F8:7F:C6:84:
91:C2:08:D2:9D:4D:57:EB:F4:75:88:BB:75:44:D3:2F:
95:37:E2:CE:C1:02:21:00:8A:FF:C4:0C:C6:C4:E3:B2:
45:78:DA:DE:4F:81:5E:CB:CE:2D:57:A5:79:34:21:19:
A1:E6:5B:C7:E5:E6:9C:E2
现在让我们进一步深入一点。 该扩展在证书中如何表示? 证书以ASN.1表示,通常表示用于表示数据结构的语言和用于对它们进行编码的一组格式。 最常见的格式DER是标签长度值格式。 也就是说,要编码一个对象,首先你写下一个代表其类型的标签(通常是一个字节),然后写下一个数字来表示该对象有多长,然后记下对象内容。 这是递归的:一个对象可以包含多个对象,每个对象都有自己的标签,长度和值。
关于DER和其他标签长度值格式的一个很酷的事情是,你可以在一定程度上解码它们而不知道它们是什么意思。 例如,我可以告诉你,0x30表示数据类型“SEQUENCE”(一个结构,按照ASN.1的术语),而0x02表示“INTEGER”,然后给你这个十六进制字节序列进行解码:
30 06 02 01 03 02 01 0A
解码为:
SEQUENCE
INTEGER 3
INTEGER 10
用这个JavaScript ASN.1解码器自己尝试一下。 但是,如果没有相应的ASN.1架构(或“模块”),则不知道这些整数表示什么。 例如,如果你知道这是DogData的一部分,模式是:
DogData ::= SEQUENCE {
legs INTEGER,
cutenessLevel INTEGER
}
你会知道这是指可爱度为10的三条腿小狗。
我们可以将这些知识应用于我们的证书。 作为第一步,将上述证书转换为xxd -ps <Downloads / 031f2484307c9bc511b3123cb236a480d451
。 然后,您可以将结果复制并粘贴到lapo.it/asn1js(或使用此便捷链接)。 您还可以运行openssl asn1parse -i -inform der -in Downloads / 031f2484307c9bc511b3123cb236a480d451
以使用OpenSSL的解析器,该解析器在某些方面不易使用,但更易于复制和粘贴。
在解码的数据中,我们可以找到OID 1.3.6.1.4.1.11129.2.4.2
,表示SCT列表扩展。 根据RFC 5280,第4.1节,定义了一个扩展:
Extension ::= SEQUENCE {
extnID OBJECT IDENTIFIER,
critical BOOLEAN DEFAULT FALSE,
extnValue OCTET STRING
-- contains the DER encoding of an ASN.1 value
-- corresponding to the extension type identified
-- by extnID
}
此处,我们可以找到extnID
。“critical”的部分被省略了,因为它的值为默认值(false)。extnValue
拥有一个OCTET STRING
类型,其标签是“0x04”。OCTET STRING在此处的意思是“此处省略一大串字符”,而这些字符包含更多的DER。这是X.509中处理参数化数据的一种相当常见的模式。 例如,这允许定义一个扩展结构,而无需提前知道未来扩展可能需要承载的所有结构的值。 它很像是C语言里的一个void*
的数据结构,又或者是Go的interface{}
。
extnValue
:
04 81 F5 0481F200F0007500DB74AFEECB29ECB1FECA3E716D2CE5B9AABB36F7847183C75D9D4F37B61FBF64000001627313EB19000004030046304402207E1FCD1E9A2BD2A50A0C81E713033A0762340DA8F91EF27A48B3817640159CD30220659FE9F1D880E2E8F6B325BE9F18956D17C6CA8A6F2B12CB0F55FB70F759A419007700293C519654C83965BAAA50FC5807D4B76FBF587A2972DCA4C30CF4E54547F478000001627313EB2A0000040300483046022100AB72F1E4D6223EF87FC68491C208D29D4D57EBF47588BB7544D32F9537E2CEC10221008AFFC40CC6C4E3B24578DADE4F815ECBCE2D57A579342119A1E65BC7E5E69CE2
这是标记“0x04”,意思是OCTET STRING
,后面是“0x81 0xF5”,意思是“该字符串是245字节长”(0x81前缀是可变长度数字编码的一部分)。
根据RFC 6962的第3.3节,“通过将SignedCertificateTimestampList结构编码为ASN.1 OCTET STRING
并将结果数据作为X.509v3证书扩展插入到TBSCertificate中,可以将获得的SCT直接嵌入最终证书中”。
有了OCTET STRING还不够,你需要移除这个标记及长度来获取值:
04 81 F2 00F0007500DB74AFEEC...
此处还有一个0x04,但长度更短些。为什么我们将一个OCTET STRING
嵌入另一个? 这是因为RFC 5280要求extnValue
的内容是有效的DER,但是SignedCertificateTimestampList不是使用DER进行编码(更多内容在一分钟内)。 因此,通过RFC 6962,SignedCertificateTimestampList被包装在OCTET STRING
中,该OCTET STRING
被另一个OCTET STRING
(extnValue)包裹。
一旦我们解码了第二个OCTET STRING
,就会获得以下内容:
00F0007500DB74AFEEC...
“0x00”并不是DER中的有效标记,而是TLS编码。这是RFC 5246的第4部分(TLS 1.2 RFC)中的规定。像ASN.1一样,TLS编码既有定义数据结构的方法,也有对这些结构进行编码的方法。 TLS编码与DER的不同之处在于没有标签,并且长度仅在必要时对可变长度数组进行编码。 在编码结构中,字段的类型由其位置决定,而不是由标签决定。 这意味着TLS编码的结构比DER结构更紧凑,但是如果不知道相应的模式就不能处理它们。 例如,以下是RFC 6962的第3.3节中的顶级架构:
嵌入在OCSP扩展或X509v3证书扩展中的ASN.1 OCTET STRING的内容如下:
opaque SerializedSCT<1..2^16-1>;
struct {
SerializedSCT sct_list <1..2^16-1>;
} SignedCertificateTimestampList;
“SerializedSCT”是一个不透明的字节字符串,它包含序列化的TLS结构。
很快,我们找到了这些可变长度数组中的一个。 这样一个数组的长度(以字节为单位)总是由长度足够大的字段来表示,以保持最大数组大小。 sct_list
的最大大小是65535字节,所以长度字段是两个字节宽。前两个字节是“0x00 0xF0”,或者是十进制的240。换句话说,这个sct_list
将有240个字节。
现在我们知道第一个SerializedSCT从0075
开始。SerializedSCT本身就是一个可变长度的字段,这一次包含了不透明的字节。像SignedCertificateTimestampList一样,它的最大尺寸为65535字节,所以我们拉开前两个字节,发现第一个SerializedSCT长度为0x0075(117十进制)。整个编码用十进制表示如下:
00DB74AFEECB29ECB1FECA3E716D2CE5B9AABB36F7847183C75D9D4F37B61FBF64000001627313EB19000004030046304402207E1FCD1E9A2BD2A50A0C81E713033A0762340DA8F91EF27A48B3817640159CD30220659FE9F1D880E2E8F6B325BE9F18956D17C6CA8A6F2B12CB0F55FB70F759A419
使用RFC 6962第3.2节中定义的TLS编码结构进行解码,得到如下内容:
enum { v1(0), (255) }
Version;
struct {
opaque key_id[32];
} LogID;
opaque CtExtensions<0..2^16-1>;
...
struct {
Version sct_version;
LogID id;
uint64 timestamp;
CtExtensions extensions;
digitally-signed struct {
Version sct_version;
SignatureType signature_type = certificate_timestamp;
uint64 timestamp;
LogEntryType entry_type;
select(entry_type) {
case x509_entry: ASN.1Cert;
case precert_entry: PreCert;
} signed_entry;
CtExtensions extensions;
};
} SignedCertificateTimestamp;
翻译一下,是这个样子的:
# Version sct_version v1(0)
00
# LogID id (aka opaque key_id[32])
DB74AFEECB29ECB1FECA3E716D2CE5B9AABB36F7847183C75D9D4F37B61FBF64
# uint64 timestamp (milliseconds since the epoch)
000001627313EB19
# CtExtensions extensions (zero-length array)
0000
# digitally-signed struct
04030046304402207E1FCD1E9A2BD2A50A0C81E713033A0762340DA8F91EF27A48B3817640159CD30220659FE9F1D880E2E8F6B325BE9F18956D17C6CA8A6F2B12CB0F55FB70F759A419
要理解“数字签名结构”,我们需要回到RFC 5246的第4.7节。 它说:
一个数字签名的元素被加密成一个结构DigitallySigned:
struct {
SignatureAndHashAlgorithm algorithm;
opaque signature<0..2^16-1>;
} DigitallySigned;
在7.4.1.4.1节中:
enum {
none(0), md5(1), sha1(2), sha224(3), sha256(4), sha384(5),
sha512(6), (255)
} HashAlgorithm;
enum { anonymous(0), rsa(1), dsa(2), ecdsa(3), (255) }
SignatureAlgorithm;
struct {
HashAlgorithm hash;
SignatureAlgorithm signature;
} SignatureAndHashAlgorithm;
我们有“0x0403”,它对应于sha256(4)和ecdsa(3)。 接下来的两个字节“0x0046”告诉我们“不透明签名”字段的长度,即70个字节的十进制数。 为了解码签名,我们参考RFC 4492第5.4节,其中说:
数字签名的元素被编码为一个不透明的向量<0..2 ^ 16-1>,其内容是对应于以下ASN.1表示法的DER编码。
Ecdsa-Sig-Value ::= SEQUENCE {
r INTEGER,
s INTEGER
}
经过两层TLS编码后,我们现在回到了ASN.1的地盘! 我们将剩余的字节解码为包含两个INTEGER的SEQUENCE。于是整个扩展解码如下:
# Extension SEQUENCE - RFC 5280
30
# length 0x0104 bytes (260 decimal)
820104
# OBJECT IDENTIFIER
06
# length 0x0A bytes (10 decimal)
0A
# value (1.3.6.1.4.1.11129.2.4.2)
2B06010401D679020402
# OCTET STRING
04
# length 0xF5 bytes (245 decimal)
81F5
# OCTET STRING (embedded) - RFC 6962
04
# length 0xF2 bytes (242 decimal)
81F2
# Beginning of TLS encoded SignedCertificateTimestampList - RFC 5246 / 6962
# length 0xF0 bytes
00F0
# opaque SerializedSCT<1..2^16-1>
# length 0x75 bytes
0075
# Version sct_version v1(0)
00
# LogID id (aka opaque key_id[32])
DB74AFEECB29ECB1FECA3E716D2CE5B9AABB36F7847183C75D9D4F37B61FBF64
# uint64 timestamp (milliseconds since the epoch)
000001627313EB19
# CtExtensions extensions (zero-length array)
0000
# digitally-signed struct - RFC 5426
# SignatureAndHashAlgorithm (ecdsa-sha256)
0403
# opaque signature<0..2^16-1>;
# length 0x0046
0046
# DER-encoded Ecdsa-Sig-Value - RFC 4492
30 # SEQUENCE
44 # length 0x44 bytes
02 # r INTEGER
20 # length 0x20 bytes
# value
7E1FCD1E9A2BD2A50A0C81E713033A0762340DA8F91EF27A48B3817640159CD3
02 # s INTEGER
20 # length 0x20 bytes
# value
659FE9F1D880E2E8F6B325BE9F18956D17C6CA8A6F2B12CB0F55FB70F759A419
# opaque SerializedSCT<1..2^16-1>
# length 0x77 bytes
0077
# Version sct_version v1(0)
00
# LogID id (aka opaque key_id[32])
293C519654C83965BAAA50FC5807D4B76FBF587A2972DCA4C30CF4E54547F478
# uint64 timestamp (milliseconds since the epoch)
000001627313EB2A
# CtExtensions extensions (zero-length array)
0000
# digitally-signed struct - RFC 5426
# SignatureAndHashAlgorithm (ecdsa-sha256)
0403
# opaque signature<0..2^16-1>;
# length 0x0048
0048
# DER-encoded Ecdsa-Sig-Value - RFC 4492
30 # SEQUENCE
46 # length 0x46 bytes
02 # r INTEGER
21 # length 0x21 bytes
# value
00AB72F1E4D6223EF87FC68491C208D29D4D57EBF47588BB7544D32F9537E2CEC1
02 # s INTEGER
21 # length 0x21 bytes
# value
008AFFC40CC6C4E3B24578DADE4F815ECBCE2D57A579342119A1E65BC7E5E69CE2
您可能会注意到一件令人惊讶的事情:在第一个SCT中,r
和s
长度为20个字节。 在第二个SCT中,它们都是21个字节长,并且具有前导零。 DER中的整数是二进制补码,所以如果最左边的位被设置,它们被解释为负值。 由于r
和s
是正数,如果最左边的位是1,则必须添加一个额外的字节,以使最左边的位可以为0。
以上是笔者对编码证书的一些解释,推荐一本书“A Layman’s Guide to a Subset of ASN.1, BER, and DER.”有兴趣的读者可以参阅并了解更多的背景知识。
脚注1:上文提到的“毒扩展”在RFC 6962第3.1节中定义。
Precertificate是由添加一个特殊的毒扩展(OID '1.3.6.1.4.1.11129.2.4.3'组成的,其extnValue OCTET STRING包含ASN.1的空数据(0x05 0x00)
换句话说,它是一个空的扩展,其唯一目的是确保证书处理器不接受先验证书作为有效证书。 该规范通过在扩展中设置“关键”位来确保这一点,从而确保不识别扩展的代码将拒绝整个证书。 真正被扩展名识别为毒药的代码也会拒绝证书。
脚注2:0-127的长度由单个字节(简写)表示。 为了表达更长的长度,使用更多的字节(长形式)。 第一个字节的高位(0x80)被设置为区分长形和短形式。 剩余的位用于表示要读取的长度多少个字节。 例如,0x81F5表示“这是长形式,因为长度大于127,但仍然只有一个字节的长度(0xF5)来解码。”
稿源:Let's Encrypt
不深度解读是什么样的
详细,专业的解读
其实,最关键的一点没有说清楚,那就是stc的签名是怎么验证的,它的验证公钥是从哪里获取的?