Python 新 ASN.1 API 预览:高性能与现代化解析

本文介绍了为 Python 开发的新 ASN.1 API,旨在解决现有库的性能、差异风险和现代化问题。该 API 使用 Rust 解析器提升性能,减少解析差异,并提供声明式数据类接口,适用于密码学和网络协议处理。

Sneak peek: A new ASN.1 API for Python

Some quick background on ASN.1

如果您曾使用过 Python 中的密码学、PKI 方案或低级网络编程,您很可能遇到过 ASN.1。ASN.1 支撑着每个 TLS 握手(通过 X.509 路径验证),为 LDAP、SNMP 和 3GPP 等核心互联网协议提供序列化层,并通常作为密码学原语和协议表示的通用语言。

ASN.1 的关键作用与其丰富多彩的安全历史相辅相成:ASN.1 编码规则的实现历来是内存损坏和拒绝服务漏洞的丰富来源。同样,ASN.1 在互联网协议的最底层存在,使得性能和无解析差异成为关键要求。

Python 有多个优秀的 ASN.1 实现(如 pyasn1、asn1 和 asn1tools),但这些通常属于后一类:纯 Python 编写使得性能成为问题,并且集成到使用其他 ASN.1 解析器的堆栈中(例如在 X.509 层)引入了差异风险。

我们正在改变这一点:在 Alpha-Omega 的资助下,我们正在为 PyCA Cryptography 构建一个 ASN.1 API,以解决 Python 生态系统当前的三个关键短板:

  • 性能:这个新 API 将使用纯 Rust ASN.1 解析器,提供接近本地的解析性能。
  • 差异减少:上述解析器已被 PyCA Cryptography 用于其 X.509 API。这将减少对 ASN.1 解析的“混合匹配”方法的需求,从而减少差异漏洞。
  • 现代化:新 API 将暴露一个声明式数据类风格的接口,充满类型提示,使其熟悉、符合习惯,并与类型检查器兼容。

例如,像这样的 ASN.1 定义:

1
2
3
4
5
6
7
Doohickies ::= SEQUENCE {
    tschotchkes       OCTET STRING,
    baubles           INTEGER,
    knickknacks       UTF8String,
    whatchamacallits  SEQUENCE OF OBJECT IDENTIFIER,
    gizmos            SET OF GeneralizedTime OPTIONAL
}

…将对应于以下 Python 代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from datetime import datetime

from cryptography.hazmat import asn1

@asn1.sequence
class Doohickies:
    tschotchkes: bytes
    baubles: int
    knickknacks: str
    whatchamacallits: list[asn1.ObjectIdentifier]
    gizmos: set[datetime] | None

doohickies = Doohickies.from_der(b"...")
print(doohickies.tschotchkes)
doohickies.to_der() # b"..."

这项工作是我们之前由 Sovereign Tech Fund 资助的 X.509 路径验证工作的逻辑延续。它反映了我们持续改进 Python 生态系统的承诺,特别是在密码学和供应链安全领域。

如果您有兴趣了解更多或资助类似工作,请联系我们!

Some quick background on ASN.1

ASN.1,或抽象语法记法一,是一种接口描述语言(IDL)。这是一种花哨的说法,表示它是一种以语言和平台无关的方式描述数据结构的语法。

令人困惑的是,ASN.1 本身不是序列化格式。相反,它定义了编码规则,这些规则又定义了在不同设置下 ASN.1 结构的序列化和反序列化。在实践中,ASN.1 与 Distinguished Encoding Rules(DER)同义。

在本文中,我们将“ASN.1”和“DER”视为可互换的。与其深入探讨两者的复杂性(Let’s Encrypt 对此有很好的覆盖),我们将专注于 DER 几十年来保持相关性的属性:

  • DER 是一种规范编码:给定 ASN.1 结构在 DER 中只有一种编码方式。换句话说,ASN.1 结构在 DER 中的编码是确定性的,并且可以在保留逐位相等性的情况下往返。
  • DER 相对紧凑:DER 定义了二进制格式,并且由于是规范的,禁止整数、布尔值和时间的非最小编码。
  • DER 是一种自描述和自定界的编码:给定的 DER 消息可以完全且可靠地解析,而无需事先参考模式或格式描述,除了 DER 本身的编码规则。

这些属性自然适用于 Web 开发人员所称的“渐进增强”:使用 DER 的应用程序可以解码它关心的特定结构,同时跳过它不关心的结构,仅解码它们的长度以跳到下一个。

DER 支持任意精度整数:DER 中的 INTEGER 类型在功能上大小无约束,这使得它适用于表示密码学设置中经常出现的大数字(例如素数)。

总之,这些属性使 DER 在密码学、网络和电信设置中非常流行。

更准确地说,它在这些设置的内部非常流行:ASN.1 用于保护世界 TLS 流量的 X.509 证书,广泛用于 PEM 编码格式,并为互联网的许多低级协议层提供描述和序列化。

Motivating an ASN.1 library for Python

您可能会合理地问:为什么 Python 需要这个?

毕竟,大多数 Python 开发人员不会每天接触 ASN.1,而那些接触的人大多以预定义的方式(如 X.509 证书)进行。为什么生态系统需要对 ASN.1 的通用支持?

这个问题的答案是,无论好坏,有许多情况下 Python 开发人员需要在 X.509 和其他知名格式和协议的“标准”形状之外进行 ASN.1 编码和解码。

这可以在 Sigstore 生态系统中看到:Sigstore 主要是一个普通的 RFC 5280 风格的 PKI,但它也包括一些自定义 X.509 扩展用于其自身目的。例如,Sigstore 日志条目的摘录显示以下扩展:

1
2
3
4
5

Runner Environment: github-hosted
Source Repository URI: https://github.com/pypa/sampleproject
Source Repository Ref: refs/heads/main
Source Repository Owner URI: https://github.com/pypa

如果我们想从 Python 中使用这些(例如,为了根据策略验证 Sigstore 证书),我们需要提取它们:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from cryptography import x509

raw_cert = b"""
-----BEGIN CERTIFICATE-----
MIIGoTCCBiigAwIBAgITFai+PDKak1xA1HLq0mskqhDV5zAKBggqhkjOPQQDAzA3
MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxHjAcBgNVBAMTFXNpZ3N0b3JlLWludGVy
bWVkaWF0ZTAeFw0yNDExMDYyMjM3MDdaFw0yNDExMDYyMjQ3MDdaMAAwWTATBgcq
hkjOPQIBBggqhkjOPQMBBwNCAARbx1Fse2Ln00On5aFaL+lHNGFYLaqeKDduplZD
PJS+w2PjYfNPL0g/n4sDWEQFZfyIExEWKulZ2GKNzAc0+SmUo4IFSDCCBUQwDgYD
VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBT/uSEI
XmQzuRkppWXrTKVkfZFJbzAfBgNVHSMEGDAWgBTf0+nPViQRlvmo2OkoVaLGLhhk
PzBhBgNVHREBAf8EVzBVhlNodHRwczovL2dpdGh1Yi5jb20vcHlwYS9zYW1wbGVw
cm9qZWN0Ly5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvaGVhZHMv
bWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVi
dXNlcmNvbnRlbnQuY29tMBIGCisGAQQBg78wAQIEBHB1c2gwNgYKKwYBBAGDvzAB
AwQoNjIxZTQ5NzRjYTI1Y2U1MzE3NzNkZWY1ODZiYTNlZDhlNzM2YjNmYzAVBgor
BgEEAYO/MAEEBAdSZWxlYXNlMCAGCisGAQQBg78wAQUEEnB5cGEvc2FtcGxlcHJv
amVjdDAdBgorBgEEAYO/MAEGBA9yZWZzL2hlYWRzL21haW4wOwYKKwYBBAGDvzAB
CAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHViY29udGVudC5jb20
MGMGCisGAQQBg78wAQkEVQxTaHR0cHM6Ly9naXRodWIuY29tL3B5cGEvc2FtcGxl
cHJvamVjdC8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbEByZWZzL2hlYWRz
L21haW4wOAYKKwYBBAGDvzABCgQqDCg2MjFlNDk3NGNhMjVjZTUzMTc3M2RlZjU4
NmJhM2VkOGU3MzZiM2ZjMB0GCisGAQQBg78wAQsEDwwNZ2l0aHViLWhvc3RlZDA1
BgorBgEEAYO/MAEMBCcMJWh0dHBzOi8vZ2l0aHViLmNvbS9weXBhL3NhbXBsZXBy
b2plY3QwOAYKKwYBBAGDvzABDQQqDCg2MjFlNDk3NGNhMjVjZTUzMTc3M2RlZjU4
NmJhM2VkOGU3MzZiM2ZjMB8GCisGAQQBg78wAQ4EEQwPcmVmcy9oZWFkcy9tYWlu
MBgGCisGAQQBg78wAQ8ECgwIMTQ4OTk1OTYwJwYKKwYBBAGDvzABEAQZDBdodHRw
czovL2dpdGh1Yi5jb20vcHlwYTAWBgorBgEEAYO/MAERBAgMBjY0NzAyNTBjBgor
BgEEAYO/MAESBFUMU2h0dHBzOi8vZ2l0aHViLmNvbS9weXBhL3NhbXBsZXByb2pl
Y3QvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWlu
MDgGCisGAQQBg78wARMEKgwoNjIxZTQ5NzRjYTI1Y2U1MzE3NzNkZWY1ODZiYTNl
ZDhlNzM2YjNmYzAUBgorBgEEAYO/MAEUBAYMBHB1c2gwWQYKKwYBBAGDvzABFQRL
DElodHRwczovL2dpdGh1Yi5jb20vcHlwYS9zYW1wbGVwcm9qZWN0L2FjdGlvbnMv
cnVucy8xMTcxMzAzODk4MS9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwGcHVi
bGljMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrje
PK3/h4pygC8p7o4AAAGTA5/X5AAABAMARzBFAiA6nYK0GxqVzJutrjrYA1bAIKHU
jGrsHMLrOJTTEUiERAIhAJZotATnSwlKt7C3Zwhx3fcSrhGfOakTlM2w+8qmltcj
MAoGCCqGSM49BAMDA2cAMGQCMB+ilsPgy4ynUG9GtqDEBqW8+ZqjX6LpuxQqjCr7
s4ytyt2ppFdgjrGrG1DY4nSZtQIwblrgq9t9izAMTkJeqhQBs2OUiyIJZipceD5v
AAE/Nfgd/9uK0MZAHFsLgalqOBl8
-----END CERTIFICATE-----
"""

cert = x509.load_pem_x509_certificate(raw_cert)

# 1.3.6.1.4.1.57264.1.16 corresponds to Source Repository Owner URI above
ext = cert.extensions.get_extension_for_oid(x509.ObjectIdentifier("1.3.6.1.4.1.57264.1.16")).value

ext.value # => b'\x0c\x17https://github.com/pypa'

正如我们所看到的,X.509 扩展的值本身是 DER 编码的,而 PyCA Cryptography 的 API(正确地)留给我们来解释它。

所以,我们需要某种 DER 解析器。幸运的是,Python 是一个成熟的生态系统,我们可以使用 pyasn1:

1
2
3
4
5
6
from pyasn1.codec.der.decoder import decode
from pyasn1.type.char import UTF8String

ext_value = decode(ext.value, UTF8String)[0].decode()

ext_value # => 'https://github.com/pypa'

现在我们有了内部扩展值,我们可以继续我们的生活了。

But why a new library?

但是等等:如果我们有 pyasn1,为什么我们需要一个新的 ASN.1 库?

这个答案有三方面,并且不是对 pyasn1 的批评(它是一个优秀的库,出色地履行了其角色):

  • 性能:Python 不是一种快速语言,而 pyasn1 是用纯 Python 编写的。Python 生态系统历来通过将性能敏感的代码放在本机扩展中来补偿这一点:首先是 C,但现在越来越多地是 Rust。通过利用 rust-asn1,我们可以在不离开 Python 舒适区的情况下接近本机代码的性能。
  • 差异减少:ASN.1 生态系统以异构著称,ASN.1 的实现在对 DER 严格要求的符合性上差异很大。

特别是,许多实现发现将 Postel 定律应用于传入的“DER”数据解析很诱人,允许非规范化的或完全畸形

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计