糟糕的公钥把戏:
我一直对 SSL/TLS 有点恼火。仅仅为了上线一个公开网站,每个人都得走一遍复杂的 “ACME” 流程。更糟的是,证书每隔几个月就过期,所以这不是一劳永逸的事:即使有自动化,维护证书也需要小心确保 ACME 客户端始终能访问 DNS 记录或 Web 服务器。
……而且整个系统甚至根本行不通,因为证书是通过明文连接签发的,而受信任的证书颁发机构会用像 “4” 这样非常安全的密钥。
已有抗议式 SSL 实现的先例,但我确实想加密 Web 流量。被动窃听是真实风险……但阻止它并不需要整套证书机制:因此,最好的选择是一张抗议证书。
证书中我直接控制的唯一部分是公钥。因此,获得一张糟糕证书的最简单方法是使用一个虚荣私钥,例如 313333333337(最极客的素数之一)。然而,虽然生成的公钥会很弱,但除非有人尝试破解它,否则没人会知道。
好的抗议应该显而易见:
# openssl s_client -connect maurycyz.com:443 < /dev/null 2> /dev/null \
| sed -n /BEGIN/,/END/p | grep -zE '////.?*////' --color=yes
-----BEGIN CERTIFICATE-----
MIIFAjCCA+qgAwIBAgISBkOkuuyaN1ypD3PAPrLMyT7tMA0GCSqGSIb3DQEBCwUA
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
EwNZUjIwHhcNMjYxMTA5MTcyOTQ2WhcNMjcwMjA3MTcyOTQ1WjAXMRUwEwYDVQQD
EwxtYXVyeWN5ei5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCg
JyPf37Sj6PvU06m5////You+dont+actually+have+to+pick+random+privat
e+keys//No+one+checks+these+things//A+certificate+authority+got+
away+with+the+number+4+for+a+year/////DSfCAfXcsJfBpnHekS0qnf5YLl
c54VuUNRVINcDkEwJDwyB96+xE/uXhtiGdk8ty1PF6jciLEYCJBHcmqOa5pOyV8m
63yR3NE6hCQlaSLQQDo96Sb0/10aC5LqE7KwEskrP/qeCzT7vDN7fh+dXOGs+VMw
eJvTAb0xHJpxhSK05i7DAgMBAAGjggIqMIICJjAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUBKBfDbwf
dCnKeU83pMTWyFrJBwAwHwYDVR0jBBgwFoAUQBUtJnntMiCe35pyHdYyH4EMgQww
MwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzAChhdodHRwOi8veXIyLmkubGVuY3Iu
b3JnLzAnBgNVHREEIDAegg4qLm1hdXJ5Y3l6LmNvbYIMbWF1cnljeXouY29tMBMG
A1UdIAQMMAowCAYGZ4EMAQIBMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly95cjIu
Yy5sZW5jci5vcmcvMTEwLmNybDCCAQsGCisGAQQB1nkCBAIEgfwEgfkA9wB1AExj
3JjlnB2riPYeij3ero+rRKM3e1+blMP7oZz8wb4mAAABocGRbw4AAAQDAEYwRAIg
HShWaEI2nVJEizSI6sJ/jnLaABZ3aTKoUL3pLo1uOgsCIEn+XiT2WHhm9LGN6kug
O9R7Z4q0HiFA4RZ7tIMJQH5XAH4AjspHC6zeavOiBrCkeoS3Rv4fxr+VPiXmm07k
AkjzxugAAAGhwZFyfQAIAAAFAEE0hdwEAwBHMEUCIDSms4Vf0llcvcldJd2Ool8i
AiUQQlS0Zzbej+7Y1aR7AiEA79Ll+t6inu1PkUIgzNTXkE0S20VHYEYel5fkdKSA
VTQwDQYJKoZIhvcNAQELBQADggEBAFi3A4fcjj4UKa9MFz1a8DWc+Jya2gJng13m
2UFdWCaqsN7FJWQEjRiJ0npynmDnA3VMyjswUMHAMx4d8G3lHNPfZWGsCgpjVa/d
IEoeZ6gTYjFa/Ovb64m4CcuQkm0GxcyAqMPs348e6Px32iU1R6MnV4UEUosnSPeK
4ocuI3SoR9LHxX/yHOKcYTKLpIMwMdaAmWt0N7l59GOb8o+dz3R7cxtVV/lrZReZ
/YPEAYRiNi7f+jNI1JCp0i8wXUb9NN7WlbmcFDecDG/++cRpAkoIk4m/LJindsuu
kErrVZ4yLqB0Hvm+uNvWxr9y/qKFTYw9POx1oaZJO/SwXZdDBeY=
-----END CERTIFICATE-----
首先,让我们看看 RSA 的数学框架。要生成密钥,选取两个大素数(p 和 q),将它们相乘得到模数(n)。选取一个小数 e 作为公开指数。
RSA 公钥由模数(n)和公开指数组成:
# Encryption requires knowing e (public)
cyphertext = plaintext^e mod n
私钥包含模数和私有指数(d),其中 d 选为 e 模 n 的乘法逆元。d 具体是如何找到的并不重要,但这个过程需要知道 p 和 q。
# Decryption requires knowing d (pivate)
= cyphertext^d mod n
= plaintext^e^d mod n
= plaintext^(e*d) mod n
= plaintext^1 mod n
= plaintext
可怕的公钥技巧
好,回到虚荣密钥。e 的标准取值是 65537(0x10001),因为置位的比特数少能让加密更快。事实上,我用的 CA 会拒绝为非标准公开指数的密钥签名,所以我没法在那上面玩什么花样。
模数 n 要么是 2,048 位,要么是 4,096 位「用户可控」比特,因此它是藏消息的理想位置。不过,要用这张证书,我得找到两个大素数,它们的乘积等于这个虚荣模数。
我的方法是先随机挑两个素数,算出得到的 n 值。然后修改 n 的字节,把消息引入到 base64 编码中。当然,修改后的 n 不再是 p 和 q 的乘积,但这可以通过把 q 设为 n/p 来修正。
得到的 q 值极不可能是素数(甚至不可能是整数),所以我把它取整,并找到理想 q 之后的那个素数:这通常只需要把 q 的值改动几百。由于 p 和 q 大小相近,q 的小改动只会影响乘积的最低一半:
58174124 * 918491824 = 53432457262362176
(incrementing until the next prime...)
58174124 * 918491843 = 53432458367670532
只要消息嵌入在 n 的上半部分,它很可能不受损坏地保留下来。原来的 p 和新选出的 q 现在可以按常规方式构造一个可用的 RSA 私钥。
从这里开始,就只是让证书颁发机构给密钥签名的问题了:
# Single domain
openssl req -new -key priv.pem -subj '/C=US/CN=example.com' > cert_request
certbot certonly --manual --csr cert_request
# Wildcard
cat > config<<EOF
[ req ]
req_extensions = req_ext
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = *.example.com
DNS.2 = example.com
EOF
openssl req -new -key priv.pem -config config -subj '/C=US/CN=example.com' > cert_request
certbot certonly --manual --csr cert_request
结果是一张可用的、受信任的证书,其公钥里藏了一条稍长的消息。续期只需重新提交 CSR 并重复 ACME 挑战,这可以自动化。
Base64 缺少大部分标点符号,但它提供了完整的 ASCII 字母表,而 ~150(RSA-2048)或 ~320(RSA-4096)个可用字符为玩笑提供了充足的空间。另一个选择是针对大多数浏览器证书查看器中显示的 base16:这会提供多得多的空间(2 字符/字节,而不是 0.75),但需要谨慎用词。
至于安全性,这个技巧显然降低了私钥的熵。不过它应该仍然安全:几乎相同的方法曾被当作一种压缩形式考虑过。如果模数的上半部分被固定为一个众所周知的值,它就不必作为密钥的一部分被发送或存储。
通过除法可以实现大约 1/2 的缩减,但最先进的技术可以固定密钥的 2/3,把尺寸缩减到标准 RSA 的 1/3,同时保持相近的安全水平……不过我应当指出,这类技巧在很大程度上已被放弃,转而使用其他能直接使用小密钥的密码系统。
对 q 的操作可能产生彼此非常接近的素数,这使得分解 n 并恢复私钥成为可能。非常小的 d 值也可能是个问题。两者都不太可能发生,但我的脚本并不检查不安全的取值。
不过,无论密钥安全性如何,它对于公共网站应该完全没问题:现代 TLS 提供前向保密,所以即使私钥泄露,窃听也需要中间人攻击。
相关:
- https://tom7.org/httpv/httpv.pdf: [⭐ 世界上最糟糕的 TLS 服务器 ]
- /projects/badcert/gen.py: 生成虚荣密钥的脚本
- https://rya.nc/cert-tricks.html: 更多虚荣证书
- https://doi.org/10.1007/978-3-540-79104-1_9: 真正的密码学家对固定 n 一部分的看法