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

• 本文约 1909 字,阅读大致需要 4 分钟 | Security | #Postfix | #Email | #SMTP | #TLS | #SNI | #Gmail | #Debug

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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,具体做法是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 我的服务器的向外连接时使用的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配置大致是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
-----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-----

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
$ 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名字的:

1
2
3
4
5
6
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。