抢先看:一个用于 Python 的新 ASN.1 API

William Woodruff 2025 年 4 月 18 日 open-source, engineering-practice, cryptography

如果你曾经在 Python 中处理过密码学、PKI 方案或底层网络,你可能遇到过 ASN.1。ASN.1 是每个 TLS 握手的基础(通过 X.509 路径验证),为核心互联网协议(如 LDAP, SNMP3GPP)提供序列化层,并且通常作为密码学原语和协议表示的 通用语言 运行。

ASN.1 的关键作用与它丰富的安全历史相辅相成:ASN.1 编码规则的实现历来是 内存损坏和拒绝服务漏洞 的丰富来源。 类似地,ASN.1 在互联网协议的最底层出现,使得性能和缺乏 parser differentials 成为关键要求。

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

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

  1. 性能: 这个新的 API 将使用一个 pure Rust ASN.1 parser,从而为我们提供接近原生的解析性能。
  2. 差异化减少: 上面提到的解析器已经被 PyCA Cryptography 用于其 X.509 APIs。这将减少对 ASN.1 解析的“混合搭配”方法的需求,从而降低差异化漏洞的风险。
  3. 现代化: 新的 API 将公开一个声明性的 dataclasses 风格接口,该接口带有类型提示,使其熟悉、地道并与类型检查器兼容。

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

Doohickies ::= SEQUENCE {
  tschotchkes    OCTET STRING,
  baubles      INTEGER,
  knickknacks    UTF8String,
  whatchamacallits SEQUENCE OF OBJECT IDENTIFIER,
  gizmos      SET OF GeneralizedTime OPTIONAL
}

...将对应于以下 Python 代码:

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"..."

这项工作是我们之前在 X.509 路径验证 方面工作的逻辑延续,该工作由 Sovereign Tech Fund 资助。它反映了我们不断致力于改进 Python 生态系统,尤其是在密码学和供应链安全领域。

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

关于 ASN.1 的一些快速背景知识

ASN.1,或抽象语法表示法一,是一种 接口描述语言 (IDL)。 这是一种描述数据结构的语法,与语言和平台无关。

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

ASN.1 不同编码规则的实用可视解释 图 1:ASN.1 不同编码规则的实用可视解释

为了本文的目的,我们将“ASN.1”和“DER”视为可以互换的。 与其深入研究两者的复杂性(Let's Encrypt 对它们的解释非常出色),我们将重点关注 DER 的特性,这些特性使其在几十年中保持了相关性:

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

总而言之,这些属性使 DER 在加密、网络和电信设置中非常受欢迎。

更准确地说,它在每个设置的内部都非常受欢迎:ASN.1 用于表示保护世界 TLS 流量的 X.509 证书,广泛用于 PEM 编码 的格式,并为互联网的较低协议层提供描述和序列化。

激励 Python 的 ASN.1 库

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

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

对此的答案是,无论好坏,在 许多 情况下,Python 开发人员需要进行 ASN.1 编码和解码,而不是在 X.509 和其他众所周知的格式和协议的“标准”形状之外。

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

OIDC Issuer: https://token.actions.githubusercontent.com
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 证书),我们需要提取它们:

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
CAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29t
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 对应于上面的 Source Repository Owner URI
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(正确地)让我们来解释它2

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

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'

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

但是为什么是 新的 库?

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

对此的答案有三方面,而不是 抨击 pyasn1 (这是一个出色的库,可以出色地发挥其作用):

  1. 性能:Python 不是一种快速的语言,而 pyasn1 是用纯 Python 编写的。 Python 生态系统历来通过将性能敏感的代码放在原生扩展中来弥补这一点:起初是 C,但现在越来越多的是 Rust。 通过利用 rust-asn1,我们可以在不离开 Python 的舒适环境的情况下接近原生代码的性能。
  2. 差异化减少:ASN.1 生态系统以其异构而闻名,ASN.1 的实现方式在很大程度上与其对 DER 严格要求的遵守程度有所不同。

特别是,许多实现发现将 Postel's Law 应用于解析传入的“DER”数据很有吸引力,只要可以推断用户的意图,就允许不正确地规范化或完全畸形的数据。 这对协议演进和安全性产生了 不利影响:协议在未指定行为的压力下难以演进,并且 parser differentials 是重大安全事件的 一致来源

因此,减少给定代码库中单个格式的独立解析器的数量通常是一个合理的工程选择。 PyCA Cryptography 已经围绕 rust-asn1 构建,因此在新 ASN.1 库中使用完全相同的解析例程是有意义的。 3. 现代化dataclassesdataclass 风格的声明式 API 在 Python 生态系统中掀起了一场风暴,并且有充分的理由:它们是统一的,与类型检查器干净地集成3,并将类型定义为 代码 而不是 数据

pyasn1 有一个很棒的声明式 API,但是该 API 早于 dataclass 的概念,因此需要混合代码和数据来定义其类型。 在我们看来,使该 API 现代化至少与从 rust-asn1 创建一个新 API 一样困难,但没有性能和差异化减少的好处。

敬请关注更多信息

这只是一个先睹为快; 请关注此空间以获取更新!

我们仍处于这项工作的早期开发阶段; 我们的计划如下:

我们要感谢 Alpha-Omega 对这项工作的资助,以及 PyCA Cryptography 维护者们提供的支持和设计评审。

  1. 不幸的是,ASN.1 也广泛用于 Basic Encoding Rules(BER)。 与 DER 不同,BER 不是一种规范编码,并且历来是 PKI 生态系统中内存损坏和互操作性问题的根源。 ↩︎
  2. 这样做的原因是微妙的:X.509 本身说扩展的值只是一个 OCTET STRING(即,原始字节),而 RFC 5280 说 OCTET STRING 本身应该包含与扩展的 OID 对应的 ASN.1 值的 DER 编码。 请参阅 RFC 5280 4.1 了解确切的语言。 ↩︎
  3. 这在很大程度上要归功于 @typing.dataclass_transform,如 PEP 681 中介绍的那样。 ↩︎