Newshot
Account

骇人的公钥花招

Translation(model translation, not the source's own text)

糟糕的公钥把戏:

我一直对 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 一部分的看法

The translation is machine-generated and individual terms may be off. Nothing is rewritten or cut; the original is authoritative.

Published by Lobsters
This site only summarises and aggregates; copyright stays with the original author. Headlines and summaries follow the source — where they differ, the original wins.
Read the original ↗