首页
/ mitmproxy.certs 模块深度解析:mitmproxy 证书签发与动态伪造证书的 API 设计

mitmproxy.certs 模块深度解析:mitmproxy 证书签发与动态伪造证书的 API 设计

2026-09-05 20:25:52作者:龚格成

本文围绕 mitmproxy 的证书子模块 mitmproxy.certs 展开:它是 mitmproxy 能够实时解密 TLS 流量的密码学核心,负责首次启动时生成 CA、按需伪造(dummy)目标站点的服务器证书、维护 CRL,并通过 CertStore 完成证书的缓存与过期回收。读完本文,你可以完整掌握该模块公开 API 的语义与参数,理解 mitmproxy “运行时在线签发证书”的实现链路,并能在自己的 addon 中直接复用 Certdummy_certCertStore 等构件。

模块定位:为什么 mitmproxy 需要一个证书模块

mitmproxy 是一个支持 TLS 的交互式中间人代理。它要解密客户端与服务器之间的加密流量,前提是客户机信任 mitmproxy 内置的证书颁发机构(CA),并由该 CA 为每一个被访问的域名即时签发一张“伪造”的服务器证书。mitmproxy/certs.py 就是这个能力的完整实现,对外暴露四部分 API:

API 类型 职责
Cert cryptographyx509.Certificate 的轻量封装,提供 PEM 读写、指纹、有效期、SAN 等常用访问器
create_ca() 函数 生成 mitmproxy 的根 CA(RSA 私钥 + CA 证书)
dummy_cert() / dummy_crl() 函数 用 CA 私钥即时签发站点证书 / 生成空 CRL
CertStore 内存证书库:加载/创建 CA 文件、按域名匹配或生成证书、缓存过期回收

该模块被 addons/tlsconfig.pyaddons/cut.pynet/tls.pyproxy/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_OFFSETnow + CERT_VALIDITY_OFFSET + CA_EXPIRYmitmproxy/certs.py#L252-L253)。
  • dummy_cert 生成的站点证书同样套用该偏移(mitmproxy/certs.py#L342-L343),199 天的有效期保证了频繁换发的伪造证书始终在合理窗口内,测试用例 test_validity_periodtest/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_certificatepublic_bytes(serialization.Encoding.PEM)
  • from_state(state) / get_state() / set_state(state):实现 Serializable 协议,把 PEM 字节作为序列化状态,因此 Cert 可以随 flow 一起被存入 dump 文件;
  • to_cryptography():取出内部的 x509.Certificate,供 pyOpenSSL 场景使用(如 TlsConfiguse_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 合规性的讲究:

  1. rsa.generate_private_key(public_exponent=65537, key_size=key_size) 生成 RSA 私钥,key_size 来自选项 key_size(默认 2048,见 mitmproxy/options.py#L227-L232);
  2. subject/issuer 均为 CN=<cn>, O=<organization>(自签名);
  3. 写入四个扩展:
    • 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;
  4. 用 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(若提供);
  • sansIterable[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_sha1test_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.DNSNamemitmproxy/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:内存证书库与缓存策略

CertStoremitmproxy/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.pempath/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_DHPARAMload_dhparam 在文件缺失时先落盘再经 pyOpenSSL 的 BIO 解析(mitmproxy/certs.py#L46-L61#L492-L514)。

另外 from_files 支持带私钥链的 CA 文件:当 PEM 中包含多于 1 张证书时,整个 ca_file 被标记为 chain_file,解析出的证书列表成为 default_chain_certsmitmproxy/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 文件,规则为:

  1. 优先从同一文件解析私钥;解析失败则回退到 CA 私钥,并校验其公钥与证书公钥一致,不一致抛 ValueError
  2. 若证书与私钥同时存在但不匹配,同样抛错;
  3. 读取完整证书链(叶证书在前,中间证书紧随其后),链解析失败时降级为单证书并告警;
  4. 若发现加载的是 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 握手路径上的热函数。它构造一个“候选键”序列并做最长前缀优先匹配:

  1. CN 的全部通配形式(asterisk_formswww.example.com[www.example.com, *.example.com, *.com],单星号 * 本身除外);
  2. 每个 SAN 的全部通配形式;
  3. 兜底通配键 "*"(对应 --certs *=cert.pem);
  4. 精确键 (commonname, sans) 元组。

命中缓存则直接返回 CertStoreEntry;未命中则调用 dummy_cert 现场签发,以 (commonname, sans) 元组入缓存,并登记进过期队列。

缓存的回收由 expireSTORE_CAP = 100 控制:每次签发都把条目追加到 expire_queue,队列超过 100 时弹出最旧条目并从 certs 字典中剔除(mitmproxy/certs.py#L486-L490)。也就是说内存中最多保留约 100 张最近使用过的伪造证书,这是典型的 LRU 近似策略。测试 test_expire 验证了淘汰行为,test_asterisk_forms 用参数化用例覆盖了通配形式生成。

模块在代理主流程中的调用链

从源码结构看,mitmproxy.certs 与主流程的耦合集中在 TlsConfig addon:

  1. TlsConfig.configureconfdir/certs/key_size/cert_passphrase 变化时执行 CertStore.from_store(expanded confdir, "mitmproxy", key_size, cert_passphrase),随后解析 ctx.options.certsspecdomain=path 或裸 path,裸路径等价于 *=path)逐个 add_cert_fileaddons/tlsconfig.py#L471-L516);
  2. 客户端发起 TLS 时,tls_start_client 调用 self.get_cert(context) 决定 CN/SAN/Organization/CRL 地址:优先复用上游嗅探证书的身份(upstream_cert 选项),叠加客户端 SNI 与服务器地址,最后交给 certstore.get_cert
  3. 拿到 CertStoreEntry 后,把 entry.cert.to_cryptography()entry.privatekey 装进 pyOpenSSL 连接对象,同时使用 entry.chain_filecertstore.dhparams 完成服务端 TLS 上下文(addons/tlsconfig.py#L210-L271);QUIC 路径 quic_start_client 以同样方式取证书(#L379-L418)。

此外 utils/magisk.py 也调用 CertStore.from_store,为 Android 生成包含 CA 的 Magisk 模块包。

mitmproxy 内置证书安装应用(mitm.it)界面,用于在各平台分发 CA 证书

首次启动生成的 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 行为调整)提供了明确的扩展入口。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384