delphij's Chaos

选择chaos这个词是因为~~实在很难找到一个更合适的词来形容这儿了……

19 Mar 2024

postfix 的 SNI 支持与 gmail 的兼容问题

今天在家里的票务系统上修改某个票的状态(该操作会出发点一封邮件)时, 我正好另一个窗口开着邮件服务器的日志,观察到一些奇怪的现象:

Mar 19 20:17:12 XXXXXX postfix/smtp[XXXXX]: certificate verification failed for 
  gmail-smtp-in.l.google.com[2607:f8b0:4023:1c03::1a]:25: self-signed certificate
Mar 19 20:17:12 XXXXXX postfix/smtp[XXXXX]: Untrusted TLS connection established
  to gmail-smtp-in.l.google.com[2607:f8b0:4023:1c03::1a]:25: TLSv1.3 with cipher
  TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS
  (2048 bits) server-digest SHA256
Mar 19 20:17:12 XXXXXX postfix/smtp[XXXXX]: XXXXXXXXXX: Server certificate not verified
Mar 19 20:17:13 XXXXXX postfix/smtp[XXXXX]: certificate verification failed for
  gmail-smtp-in.l.google.com[142.250.142.26]:25: self-signed certificate
Mar 19 20:17:13 XXXXXX postfix/smtp[XXXXX]: Untrusted TLS connection established
  to gmail-smtp-in.l.google.com[142.250.142.26]:25: TLSv1.3 with cipher TLS_AES_256_GCM_SHA384
  (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256
Mar 19 20:17:13 XXXXXX postfix/smtp[XXXXX]: XXXXXXXXXX: Server certificate not verified
Mar 19 20:17:13 XXXXXX postfix/smtp[XXXXX]: certificate verification failed for
  alt1.gmail-smtp-in.l.google.com[142.250.115.27]:25: self-signed certificate
Mar 19 20:17:13 XXXXXX postfix/smtp[XXXXX]: Untrusted TLS connection established
  to alt1.gmail-smtp-in.l.google.com[142.250.115.27]:25: TLSv1.3 with cipher
  TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS
  (2048 bits) server-digest SHA256
Mar 19 20:17:13 XXXXXX postfix/smtp[XXXXX]: XXXXXXXXXX: Server certificate not verified
Mar 19 20:17:14 XXXXXX postfix/smtp[XXXXX]: certificate verification failed for
  alt1.gmail-smtp-in.l.google.com[2607:f8b0:4023:1004::1b]:25: self-signed certificate
Mar 19 20:17:14 XXXXXX postfix/smtp[XXXXX]: Untrusted TLS connection established to
  alt1.gmail-smtp-in.l.google.com[2607:f8b0:4023:1004::1b]:25: TLSv1.3 with cipher
  TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS
  (2048 bits) server-digest SHA256
Mar 19 20:17:14 XXXXXX postfix/smtp[XXXXX]: XXXXXXXXXX: Server certificate not verified
Mar 19 20:17:14 XXXXXX postfix/smtp[XXXXX]: Verified TLS connection established to
  alt2.gmail-smtp-in.l.google.com[2607:f8b0:4003:c15::1b]:25: TLSv1.3 with cipher
  TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA
  (prime256v1) server-digest SHA256
Mar 19 20:17:15 XXXXXX postfix/smtp[XXXXX]: XXXXXXXXXX: to=<XXXXXXX@gmail.com>,
  relay=alt2.gmail-smtp-in.l.google.com[2607:f8b0:4003:c15::1b]:25, delay=2.8,
  delays=0.06/0.1/2/0.66, dsn=2.0.0, status=sent (250 2.0.0 OK  XXXXXXXXXX
  XXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.XX - gsmtp)

这里需要补充说明一下背景。由于 gmail 很早就实现了 TLS,因此我在 postfix 中配置了强制 TLS,具体做法是这样的:

# 我的服务器的向外连接时使用的 TLS 证书文件,用于说明自己的身份
smtp_tls_cert_file = /usr/local/etc/ssl/XXX.crt
smtp_tls_key_file = /usr/local/etc/ssl/XXX.key

# 系统认可的CA根证书(来自 Mozilla 的 nss 里的 CA root),用于在需要时验证对方证书
smtp_tls_CAfile = /usr/local/share/certs/ca-root-nss.crt

# 协商时只允许 high 级别的加密
smtp_tls_ciphers = high

# 允许对方 DNSsec 签名的 DANE 记录
smtp_tls_security_level = dane

# 分域名的TLS策略
smtp_tls_policy_maps = hash:/usr/local/etc/postfix/maps/tls_policy

然后我的 tls_policy_maps 配置大致是这样的:

gmail.com               verify
yahoo.com               verify
outlook.com             verify
google.com              verify
.google.com             verify
googlemail.com          verify
.googlemail.com         verify
[...]
freebsd.org             dane-only
.freebsd.org            dane-only

这是因为主流的邮件服务提供商普遍没有启用 DANE,但他们的 TLS 证书配置通常是正确的。 FreeBSD.org 以及一些其他机构正确配置了 DANE,因此使用更强的 dane-only (要求对方必须使用 DANE 记录)策略。 对于其他域名,则使用上面的 dane 策略:有 DANE 记录则使用 DANE 进行验证,没有的话则尝试 TLS 并记录验证结果,如果失败则用明文传送。

总体上,上述策略的目标是避免发送邮件给无关的第三方邮件服务器,具体来说:

  • 在向主流邮件服务提供商发送邮件时,验证其 TLS 证书确实与 mx 记录中的主机名匹配,并且由某一受信赖的 CA 签发
  • 对于我认识的启用了 DANE 的域名,验证其证书与 DANE 指定的匹配。
  • 对于我不认识但是启用了 DANE 的域名,验证其证书与 DANE 指定的匹配。
  • 对于其他域名,尝试 TLS 并记录验证结果,如果失败则用明文传送。

从现象上看,发生错误的原因是在给 gmail.com 发信时,邮件系统与 gmail.com 的 MX 进行 TLS 握手并要求对方提供证书,而出于某种原因该证书是自签名的。gmail.com 没有启用 DANE, 由于我指定了这些域名使用更强的 verify 而不是推荐的 may,因此系统拒绝向这些 MX 投递邮件。 直到最后, alt2.gmail-smtp-in.l.google.com 提供了一个可以验证的证书, 因此邮件最终投递成功。

提高 postfix 的 smtp TLS 日志级别:

# postconf -e smtp_tls_loglevel=2
# postfix reload

然后再次尝试投递,发现:

Mar 19 20:43:04 XXXXXX postfix/smtp[XXXXX]: gmail-smtp-in.l.google.com[173.194.65.27]:25:
  depth=0 verify=0 subject=/OU=No SNI provided; please fix your client./CN=invalid2.invalid

具体来说,这个证书的内容是:

-----BEGIN CERTIFICATE-----
MIIDfDCCAmSgAwIBAgIJAJB2iRjpM5OgMA0GCSqGSIb3DQEBCwUAME4xMTAvBgNV
BAsMKE5vIFNOSSBwcm92aWRlZDsgcGxlYXNlIGZpeCB5b3VyIGNsaWVudC4xGTAX
BgNVBAMTEGludmFsaWQyLmludmFsaWQwHhcNMTUwMTAxMDAwMDAwWhcNMzAwMTAx
MDAwMDAwWjBOMTEwLwYDVQQLDChObyBTTkkgcHJvdmlkZWQ7IHBsZWFzZSBmaXgg
eW91ciBjbGllbnQuMRkwFwYDVQQDExBpbnZhbGlkMi5pbnZhbGlkMIIBIjANBgkq
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzWJP5cMThJgMBeTvRKKl7N6ZcZAbKDVA
tNBNnRhIgSitXxCzKtt9rp2RHkLn76oZjdNO25EPp+QgMiWU/rkkB00Y18Oahw5f
i8s+K9dRv6i+gSOiv2jlIeW/S0hOswUUDH0JXFkEPKILzpl5ML7wdp5kt93vHxa7
HswOtAxEz2WtxMdezm/3CgO3sls20wl3W03iI+kCt7HyvhGy2aRPLhJfeABpQr0U
ku3q6mtomy2cgFawekN/X/aH8KknX799MPcuWutM2q88mtUEBsuZmy2nsjK9J7/y
hhCRDzOV/yY8c5+l/u/rWuwwkZ2lgzGp4xBBfhXdr6+m9kmwWCUm9QIDAQABo10w
WzAOBgNVHQ8BAf8EBAMCAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
MA8GA1UdEwEB/wQFMAMBAf8wGQYDVR0OBBIEELsPOJZvPr5PK0bQQWrUrLUwDQYJ
KoZIhvcNAQELBQADggEBALnZ4lRc9WHtafO4Y+0DWp4qgSdaGygzS/wtcRP+S2V+
HFOCeYDmeZ9qs0WpNlrtyeBKzBH8hOt9y8aUbZBw2M1F2Mi23Q+dhAEUfQCOKbIT
tunBuVfDTTbAHUuNl/eyr78v8Egi133z7zVgydVG1KA0AOSCB+B65glbpx+xMCpg
ZLux9THydwg3tPo/LfYbRCof+Mb8I3ZCY9O6FfZGjuxJn+0ux3SDora3NX/FmJ+i
kTCTsMtIFWhH3hoyYAamOOuITpPZHD7yP0lfbuncGDEqAQu2YWbYxRixfq2VSxgv
gWbFcmkgBLYpE8iDWT3Kdluo1+6PHaDaLg2SacOY6Go=
-----END CERTIFICATE-----

翻译成便于人类理解的内容:

$ openssl x509 -in /tmp/saved.cer -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            90:76:89:18:e9:33:93:a0
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: OU = "No SNI provided; please fix your client.", CN = invalid2.invalid
        Validity
            Not Before: Jan  1 00:00:00 2015 GMT
            Not After : Jan  1 00:00:00 2030 GMT
        Subject: OU = "No SNI provided; please fix your client.", CN = invalid2.invalid
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:cd:62:4f:e5:c3:13:84:98:0c:05:e4:ef:44:a2:
                    a5:ec:de:99:71:90:1b:28:35:40:b4:d0:4d:9d:18:
                    48:81:28:ad:5f:10:b3:2a:db:7d:ae:9d:91:1e:42:
                    e7:ef:aa:19:8d:d3:4e:db:91:0f:a7:e4:20:32:25:
                    94:fe:b9:24:07:4d:18:d7:c3:9a:87:0e:5f:8b:cb:
                    3e:2b:d7:51:bf:a8:be:81:23:a2:bf:68:e5:21:e5:
                    bf:4b:48:4e:b3:05:14:0c:7d:09:5c:59:04:3c:a2:
                    0b:ce:99:79:30:be:f0:76:9e:64:b7:dd:ef:1f:16:
                    bb:1e:cc:0e:b4:0c:44:cf:65:ad:c4:c7:5e:ce:6f:
                    f7:0a:03:b7:b2:5b:36:d3:09:77:5b:4d:e2:23:e9:
                    02:b7:b1:f2:be:11:b2:d9:a4:4f:2e:12:5f:78:00:
                    69:42:bd:14:92:ed:ea:ea:6b:68:9b:2d:9c:80:56:
                    b0:7a:43:7f:5f:f6:87:f0:a9:27:5f:bf:7d:30:f7:
                    2e:5a:eb:4c:da:af:3c:9a:d5:04:06:cb:99:9b:2d:
                    a7:b2:32:bd:27:bf:f2:86:10:91:0f:33:95:ff:26:
                    3c:73:9f:a5:fe:ef:eb:5a:ec:30:91:9d:a5:83:31:
                    a9:e3:10:41:7e:15:dd:af:af:a6:f6:49:b0:58:25:
                    26:f5
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment, Certificate Sign
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Subject Key Identifier: 
                BB:0F:38:96:6F:3E:BE:4F:2B:46:D0:41:6A:D4:AC:B5
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        b9:d9:e2:54:5c:f5:61:ed:69:f3:b8:63:ed:03:5a:9e:2a:81:
        27:5a:1b:28:33:4b:fc:2d:71:13:fe:4b:65:7e:1c:53:82:79:
        80:e6:79:9f:6a:b3:45:a9:36:5a:ed:c9:e0:4a:cc:11:fc:84:
        eb:7d:cb:c6:94:6d:90:70:d8:cd:45:d8:c8:b6:dd:0f:9d:84:
        01:14:7d:00:8e:29:b2:13:b6:e9:c1:b9:57:c3:4d:36:c0:1d:
        4b:8d:97:f7:b2:af:bf:2f:f0:48:22:d7:7d:f3:ef:35:60:c9:
        d5:46:d4:a0:34:00:e4:82:07:e0:7a:e6:09:5b:a7:1f:b1:30:
        2a:60:64:bb:b1:f5:31:f2:77:08:37:b4:fa:3f:2d:f6:1b:44:
        2a:1f:f8:c6:fc:23:76:42:63:d3:ba:15:f6:46:8e:ec:49:9f:
        ed:2e:c7:74:83:a2:b6:b7:35:7f:c5:98:9f:a2:91:30:93:b0:
        cb:48:15:68:47:de:1a:32:60:06:a6:38:eb:88:4e:93:d9:1c:
        3e:f2:3f:49:5f:6e:e9:dc:18:31:2a:01:0b:b6:61:66:d8:c5:
        18:b1:7e:ad:95:4b:18:2f:81:66:c5:72:69:20:04:b6:29:13:
        c8:83:59:3d:ca:76:5b:a8:d7:ee:8f:1d:a0:da:2e:0d:92:69:
        c3:98:e8:6a

Hmm… 所以问题出在我没有正确告知对方自己尝试连接的 SNI 名字。仔细读了一下 postfix 的 文档, 发现 postfix 出于兼容性考虑(坦率地讲,如果做 verify 的话,我其实希望尽量不兼容配置有问题的服务器), 在没有 DANE 的时候是不发出 SNI 名字的:

Some SMTP servers use the received SNI name to select an appropriate certificate
chain to present to the client. While this may improve interoperability with such
servers, it may reduce interoperability with other servers that choose to abort
the connection when they don't have a certificate chain configured for the requested
name. Such servers should select a default certificate chain and continue the
handshake, but some may not. Therefore, absent DANE, no SNI name is sent by default.

所以解法就是让 postfix 在 verify 的时候提供一个 SNI 名字,具体来说是把上面 tls_policy 中的 verify 替换为 verify servername=hostname (相当于指定 smtp_tls_servername=hostname, 但仅对这几个域名生效。 hostname 的意思是使用 DNS 解析得到的 MX 的主机名),并重建 tls_policy 的 hash db。

这之后发送邮件到 gmail.com 就正常了。

根据日志,似乎 gmail 是今天早上太平洋时间 06:00 到 09:17 部署的这个新的变动 (对于不认识或没有提供 SNI 域名的客户端,在 TLS 握手时送出一个 CN=invalid2.invalid 的自签名证书)。

我暂时还不太理解这么做的好处是什么。客户端发出 SNI 名字时, 服务器使用的是一个采用 P-256 的公钥的证书,而客户端没有这么做时(postfix 的默认配置如此, 大部分人恐怕也不会像我这样吃饱了撑的配置成 verify,因此很可能根本不会注意到这个问题), 服务器使用的是一个 2048-bit RSA 的公钥的自签名证书, 而通常的观点认为 P-256 大致相当于 3072-bit RSA。