mitmproxy.certs 模块深度解析:mitmproxy 证书签发与动态伪造证书的 API 设计
本文围绕 mitmproxy 的证书子模块 mitmproxy.certs 展开:它是 mitmproxy 能够实时解密 TLS 流量的密码学核心,负责首次启动时生成 CA、按需伪造(dummy)目标站点的服务器证书、维护 CRL,并通过 CertStore 完成证书的缓存与过期回收。读完本文,你可以完整掌握该模块公开 API 的语义与参数,理解 mitmproxy “运行时在线签发证书”的实现链路,并能在自己的 addon 中直接复用 Cert、dummy_cert、CertStore 等构件。
模块定位:为什么 mitmproxy 需要一个证书模块
mitmproxy 是一个支持 TLS 的交互式中间人代理。它要解密客户端与服务器之间的加密流量,前提是客户机信任 mitmproxy 内置的证书颁发机构(CA),并由该 CA 为每一个被访问的域名即时签发一张“伪造”的服务器证书。mitmproxy/certs.py 就是这个能力的完整实现,对外暴露四部分 API:
| API | 类型 | 职责 |
|---|---|---|
Cert |
类 | 对 cryptography 库 x509.Certificate 的轻量封装,提供 PEM 读写、指纹、有效期、SAN 等常用访问器 |
create_ca() |
函数 | 生成 mitmproxy 的根 CA(RSA 私钥 + CA 证书) |
dummy_cert() / dummy_crl() |
函数 | 用 CA 私钥即时签发站点证书 / 生成空 CRL |
CertStore |
类 | 内存证书库:加载/创建 CA 文件、按域名匹配或生成证书、缓存过期回收 |
该模块被 addons/tlsconfig.py、addons/cut.py、net/tls.py、proxy/layers/tls.py 等 TLS 握手路径直接调用,是所有拦截场景的密码学底座。
有效期常量:CA 十年、叶证书 199 天、CRL 7 天
模块顶部的四个常量决定了所有签发对象的时效(mitmproxy/certs.py#L40-L44):
CA_EXPIRY = datetime.timedelta(days=10 * 365) # CA 有效期 10 年
CERT_EXPIRY = datetime.timedelta(days=199) # 伪造的叶证书有效期 199 天
CRL_EXPIRY = datetime.timedelta(days=7) # CRL 有效期 7 天
CERT_VALIDITY_OFFSET = datetime.timedelta(days=-2) # 有效期整体回拨 2 天
两个设计细节值得注意:
- 源码注释说明 CA 有效期“不能设得太长”,而
CERT_VALIDITY_OFFSET = -2 天意味着所有证书都会回拨 2 天生效,以容忍客户端时钟不准的情况。因此create_ca中 CA 的生效/失效时间均写入为now + CERT_VALIDITY_OFFSET与now + CERT_VALIDITY_OFFSET + CA_EXPIRY(mitmproxy/certs.py#L252-L253)。 dummy_cert生成的站点证书同样套用该偏移(mitmproxy/certs.py#L342-L343),199 天的有效期保证了频繁换发的伪造证书始终在合理窗口内,测试用例test_validity_period在 test/mitmproxy/test_certs.py 中对这一行为做了验证。
Cert 类:x509.Certificate 的薄封装与访问器
Cert 实现 serializable.Serializable 接口(mitmproxy/certs.py#L64),构造函数只接受一个 x509.Certificate 对象并做 isinstance 断言。它把 cryptography 的证书对象包装为 mitmproxy 内部统一的证书表示,并提供以下 API:
序列化与格式转换
from_pem(data) / to_pem():PEM 字节序列的读写,分别对应x509.load_pem_x509_certificate与public_bytes(serialization.Encoding.PEM);from_state(state) / get_state() / set_state(state):实现Serializable协议,把 PEM 字节作为序列化状态,因此Cert可以随 flow 一起被存入 dump 文件;to_cryptography():取出内部的x509.Certificate,供 pyOpenSSL 场景使用(如TlsConfig中use_certificate(entry.cert.to_cryptography()),见 addons/tlsconfig.py#L251);from_pyopenssl() / to_pyopenssl():与 pyOpenSSL 的OpenSSL.crypto.X509互转,其中to_pyopenssl已标记@deprecated,提示改用to_cryptography。
身份与属性访问器
| 属性/方法 | 返回 | 说明 |
|---|---|---|
fingerprint() |
bytes |
SHA-256 指纹,同时被用作 __eq__ 的判等依据 |
issuer / subject |
list[tuple[str, str]] |
通过 _name_to_keyval 把 RFC 4514 名字拆为 (key, value) 列表 |
cn / organization |
str | None |
从 subject 中取 Common Name / Organization Name |
altnames |
x509.GeneralNames |
SubjectAlternativeName 扩展;缺失时返回空 GeneralNames |
crl_distribution_points |
list[str] |
CRL 分发点 URL(只保留 URI 类型项) |
serial |
int |
证书序列号,TlsConfig 用它拼接 CRL 路径 |
notbefore / notafter |
datetime |
优先使用 not_valid_*_utc 新 API,并对 cryptography < 42 做回退 |
has_expired() |
bool |
用 UTC 当前时间比较 notafter |
is_ca |
bool |
读取 BasicConstraints 扩展的 ca 标志,无扩展时返回 False |
keyinfo |
tuple[str, int] |
公钥类型与位长,例如 ("RSA", 2048);EC 键返回曲线名 |
public_key() |
公钥对象 | 直接透传内部证书 |
altnames 的容错设计值得注意:当证书没有 SAN 扩展时不抛异常而是返回空的 GeneralNames,这使得后续按 SAN 做证书匹配的逻辑(CertStore.get_cert)可以对“裸证书”安全遍历。
create_ca():根 CA 的生成细节
create_ca(organization, cn, key_size) 返回 (rsa.RSAPrivateKey, x509.Certificate)(mitmproxy/certs.py#L232-L281)。其构建流程体现了对 RFC 5280 合规性的讲究:
- 用
rsa.generate_private_key(public_exponent=65537, key_size=key_size)生成 RSA 私钥,key_size来自选项key_size(默认 2048,见 mitmproxy/options.py#L227-L232); - subject/issuer 均为
CN=<cn>, O=<organization>(自签名); - 写入四个扩展:
BasicConstraints(ca=True),critical=True —— 使其成为合法 CA;ExtendedKeyUsage(SERVER_AUTH),critical=False;KeyUsage(key_cert_sign=True, crl_sign=True),其余标志全部置 False,critical=True —— 即该 CA 只能签证书和 CRL,不能做数字签名/加解密;SubjectKeyIdentifier.from_public_key(...),critical=False;
- 用 SHA-256 以 CA 私钥自签名完成。
TlsConfig.configure 在检测到 certs/confdir/key_size/cert_passphrase 变更时会重建 CertStore,并对过期 CA 发出警告(addons/tlsconfig.py#L471-L493),这就是“删除 ~/.mitmproxy 后 CA 自动重新生成”机制的落点。
dummy_cert():运行时伪造站点证书
dummy_cert(privkey, cacert, commonname, sans, organization=None, crl_url=None)(mitmproxy/certs.py#L314-L404)是拦截 HTTPS 时的核心签发函数。参数语义:
privkey:CA 的 RSA 私钥;cacert:CA 证书(issuer 直接取自cacert.subject);commonname:写入 subject 的 CN。注意源码中有硬性限制——只有长度小于 64 的 CN 才会写入 subject,否则 subject 中只保留 organization(若提供);sans:Iterable[x509.GeneralName],即 DNS 名 / IP 地址对象;crl_url:可选,写入 CRLDistributionPoints 扩展。
几个容易忽略的实现要点:
1. SAN 的 critical 标志遵循 RFC 5280 §4.2.1.6:当 CN 有效时 SAN 为 non-critical;当 subject 实质为空(CN 缺失或过长)时 SAN 被标记为 critical,见 mitmproxy/certs.py#L356-L360。
2. AuthorityKeyIdentifier 必须复制 issuer 的 SKI,而不是重算:源码注释解释(mitmproxy/certs.py#L362-L382)——若用 from_issuer_public_key() 重算会得到 SHA-1 摘要,而 issuer 的 SKI 若是 RFC 7093 截断 SHA-256(如 cert-manager >=1.18 / Go >=1.25 为 FIPS 140-3 合规的默认值)就会产生 “authority and subject key identifier mismatch”,被严格链验证器(Python ssl、Go crypto/x509)拒绝。因此实现优先调用 from_issuer_subject_key_identifier(),仅当 issuer 没有 SKI 时才回退到按公钥推导。test/mitmproxy/test_certs.py 中的 test_aki_copies_issuer_ski_non_sha1 与 test_aki_falls_back_when_issuer_has_no_ski 正是针对这两条路径的回归测试。
3. 叶证书刻意不写 SKI:注释指出 CA 与叶证书 SKI 相同时会导致 Windows SChannel 异常行为(引用 issue #6494),而 RFC 5280 允许叶证书省略 SKI,故直接跳过(mitmproxy/certs.py#L383-L386)。
4. SAN 的兼容性转换 _fix_legacy_sans:mitmproxy 10.1 及更早版本中 SAN 是字符串列表,现在要求 GeneralNames。该函数对旧式字符串列表发出 DeprecationWarning,并按规则转换:能解析为 IP 地址的字符串转 x509.IPAddress,其余做 IDNA 编码后转 x509.DNSName(mitmproxy/certs.py#L284-L311)。
签发同样使用 SHA-256,并返回 Cert 包装对象。
dummy_crl():空 CRL 的用途
dummy_crl(privkey, cacert) 生成一个空的 CRL(DER 编码返回):issuer 取自 CA,last_update 为当前时间(含偏移)、next_update 为 7 天后(CRL_EXPIRY),并附一个无实际意义的 CRLNumber(1000)(mitmproxy/certs.py#L407-L428)。
它的存在服务于 upstream_cert 特性:当 mitmproxy 嗅探到上游真实证书带有 CRL 分发点时,TlsConfig.get_cert 会把原始 URL 的路径替换为 “魔法令牌” /mitmproxy-<CA serial>.crl,写入伪造证书;客户端若真的去拉取该 CRL,TlsConfig.request 钩子会匹配路径后缀并以 application/pkix-crl 直接返回这份空 CRL(addons/tlsconfig.py#L582-L644)。这使伪造证书在“看起来完整”的同时不需要维护真正的吊销基础设施。
CertStore:内存证书库与缓存策略
CertStore(mitmproxy/certs.py#L446)是运行时证书管理的入口,其类型定义揭示了两类条目:
TCustomCertId = str # 手动证书,即 --certs 选项
TGeneratedCertId = tuple[Optional[str], x509.GeneralNames] # (common_name, sans) 生成的证书
TCertId = Union[TCustomCertId, TGeneratedCertId]
构造与磁盘布局
CertStore.from_store(path, basename, key_size, passphrase) 按约定查找 path/mitmproxy-ca.pem 与 path/mitmproxy-dhparam.pem:CA 文件不存在时先调 create_store 生成(mitmproxy/certs.py#L516-L529)。create_store 产出的文件集与 docs/src/content/concepts/certificates.md 的表格一致:
| 文件 | 内容 |
|---|---|
mitmproxy-ca.pem |
私钥(TraditionalOpenSSL PEM)+ CA 证书拼接 |
mitmproxy-ca.p12 |
密钥+证书的 PKCS12(Windows 设备) |
mitmproxy-ca-cert.pem |
仅证书(大多数非 Windows 平台分发用) |
mitmproxy-ca-cert.cer |
与 pem 同内容的 Android 习惯扩展名 |
mitmproxy-ca-cert.p12 |
仅证书的 PKCS12 |
mitmproxy-dhparam.pem |
预置 DH 参数 |
两个安全细节:写私钥文件时通过 umask_secret() 上下文管理器把 umask 临时按位或上 0o77,保证私钥仅属主可读(mitmproxy/certs.py#L547-L560);DH 参数不在线生成(“太慢”),而是把 openssl dhparam 的结果内嵌为模块级常量 DEFAULT_DHPARAM,load_dhparam 在文件缺失时先落盘再经 pyOpenSSL 的 BIO 解析(mitmproxy/certs.py#L46-L61、#L492-L514)。
另外 from_files 支持带私钥链的 CA 文件:当 PEM 中包含多于 1 张证书时,整个 ca_file 被标记为 chain_file,解析出的证书列表成为 default_chain_certs(mitmproxy/certs.py#L531-L545),即支持“自定义 CA + 中间链”的场景。load_pem_private_key 则封装了容错:私钥未加密而用户却传了 passphrase 时静默地按无密码重试(mitmproxy/certs.py#L731-L741)。
自定义证书加载:add_cert_file / add_cert
add_cert_file(spec, path, passphrase)(mitmproxy/certs.py#L617-L650)处理 --certs [domain=]path 选项传入的 PEM 文件,规则为:
- 优先从同一文件解析私钥;解析失败则回退到 CA 私钥,并校验其公钥与证书公钥一致,不一致抛
ValueError; - 若证书与私钥同时存在但不匹配,同样抛错;
- 读取完整证书链(叶证书在前,中间证书紧随其后),链解析失败时降级为单证书并告警;
- 若发现加载的是 CA 而非叶证书,记录一条“疑似配置错误”的 warning 并指向证书文档。
add_cert(entry, *names) 的注册逻辑把条目按三类名字键入 self.certs 字典:证书自身的 CN、每个 SAN 的字符串值、以及调用方显式传入的名字(对应 --certs domain=... 中的 domain)(mitmproxy/certs.py#L652-L662)。
证书查找与生成:get_cert
get_cert(commonname, sans, organization=None, crl_url=None)(mitmproxy/certs.py#L681-L728)是 TLS 握手路径上的热函数。它构造一个“候选键”序列并做最长前缀优先匹配:
- CN 的全部通配形式(
asterisk_forms:www.example.com→[www.example.com, *.example.com, *.com],单星号*本身除外); - 每个 SAN 的全部通配形式;
- 兜底通配键
"*"(对应--certs *=cert.pem); - 精确键
(commonname, sans)元组。
命中缓存则直接返回 CertStoreEntry;未命中则调用 dummy_cert 现场签发,以 (commonname, sans) 元组入缓存,并登记进过期队列。
缓存的回收由 expire 与 STORE_CAP = 100 控制:每次签发都把条目追加到 expire_queue,队列超过 100 时弹出最旧条目并从 certs 字典中剔除(mitmproxy/certs.py#L486-L490)。也就是说内存中最多保留约 100 张最近使用过的伪造证书,这是典型的 LRU 近似策略。测试 test_expire 验证了淘汰行为,test_asterisk_forms 用参数化用例覆盖了通配形式生成。
模块在代理主流程中的调用链
从源码结构看,mitmproxy.certs 与主流程的耦合集中在 TlsConfig addon:
TlsConfig.configure在confdir/certs/key_size/cert_passphrase变化时执行CertStore.from_store(expanded confdir, "mitmproxy", key_size, cert_passphrase),随后解析ctx.options.certs的spec(domain=path或裸path,裸路径等价于*=path)逐个add_cert_file(addons/tlsconfig.py#L471-L516);- 客户端发起 TLS 时,
tls_start_client调用self.get_cert(context)决定 CN/SAN/Organization/CRL 地址:优先复用上游嗅探证书的身份(upstream_cert选项),叠加客户端 SNI 与服务器地址,最后交给certstore.get_cert; - 拿到
CertStoreEntry后,把entry.cert.to_cryptography()与entry.privatekey装进 pyOpenSSL 连接对象,同时使用entry.chain_file与certstore.dhparams完成服务端 TLS 上下文(addons/tlsconfig.py#L210-L271);QUIC 路径quic_start_client以同样方式取证书(#L379-L418)。
此外 utils/magisk.py 也调用 CertStore.from_store,为 Android 生成包含 CA 的 Magisk 模块包。
首次启动生成的 CA 需要通过 addons/onboardingapp 分发到客户端:该 addon 依据 CONF_BASENAME 输出 mitmproxy-ca-cert.{ext} 等文件供用户浏览器下载(addons/onboardingapp/init.py#L54),这正是 create_store 写出多格式 CA 文件的下游消费者。
API 使用示例与测试参照
在 addon 或脚本中直接使用本模块的典型姿势:
from pathlib import Path
from mitmproxy import certs
from mitmproxy.options import CONF_BASENAME
# 1. 复用 mitmproxy 的 CA 存储(~/.mitmproxy)
store = certs.CertStore.from_store(Path.home() / ".mitmproxy", CONF_BASENAME, key_size=2048)
# 2. 按域名取证书:命中缓存返回旧证书,否则用 CA 现场签发
from cryptography.x509 import DNSName
entry = store.get_cert(commonname="example.com", sans=[DNSName("example.com")])
# 3. 读取属性 / 序列化
cert: certs.Cert = entry.cert
print(cert.cn, cert.keyinfo, cert.has_expired())
pem: bytes = cert.to_pem()
验证行为时可以直接对照 test/mitmproxy/test_certs.py:其中 TestCertStore 覆盖 test_from_store_with_passphrase(加密私钥加载)、test_add_cert_overrides(自定义证书覆盖)、test_add_cert_is_ca(误配 CA 告警)等场景;TestDummyCert 相关用例则覆盖 AKI/SKI 兼容、多值 SAN、CRL 分发点解析等边界。
小结
mitmproxy.certs 以不到八百行的规模实现了 mitmproxy 的完整密码学基础设施:Cert 提供统一证书表示,create_ca/dummy_cert/dummy_crl 保证签发结果满足 RFC 5280/7093 的严格校验(尤其是 AKI/SKI 一致性与 SAN critical 规则),CertStore 则以“通配前缀匹配 + 100 条 LRU 缓存”的策略平衡了命中率与内存占用。理解这一模块,也就理解了 mitmproxy 能够在毫秒级为任意域名呈现“合法”TLS 证书的全部原理,也为二次开发(自定义 CA、自定义站点证书、CRL 行为调整)提供了明确的扩展入口。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
