【2049首发】ESNI细节讲解

By 霏艺Faye at 2020-04-06

先啰嗦一句,估计这是全网唯一一篇中文讲解ESNI的文章。其他语言有没有,我就不知道了。

先讲流程和细节:

Q1. SNI被加密了,用的是什么加密算法,加密密钥从哪里来?

A1.使用的加密算法是AEAD,具体是哪种AEAD,是协商出来的。密钥是通过PSK协商出来的。

Q2.如果是通过PSK协商,那么服务器的公钥是从哪里来的?

A2.通过DNS得到,具体来说是DNS的 TXT record。

Q3.发起DoH的时候,SNI有没有加密呢?

A3.没有,原因我下面解释

大致流程:

1.火狐通过DNS over HTTPS 发起 请求 服务器的ip地址,还有txt record

2.如果txt record里有公钥信息,说明服务器支持esni,用这个record协商出了aes的密钥

3.用aes密钥加密sni,拼到clienthello的esni字段

4.发送给服务器

2049, ESNI, 讲解, 首发, 细节


@v2rayuser 我解释下你的DoH发起的时候,有没有ESNI的问题。

首先DoH的地址,可以是这样的 https://1.1.1.1/dns-query

也是可以 https://dns.couldflare-dns.com/dns-query 这样的

如果是第一种情况,因为是IP地址,自然不存在SNI,也就不可能存在ESNI

如果是第二种情况,因为是域名,所以要先去查这个域名的具体IP,也就是用传统的DNS查找

在about:config配置页,输入 network.trr.bootstrapAddress 这个配置后面填入具体的DNS地址

完整过程就是火狐,先从network.trr.bootstrapAddress 配置的DNS地址,查找具体的IP,再发起TLS连接。这个时候,没有使用DoH,用的是传统的DNS,所以SNI没有加密

火狐使用ESNI,必须配合DoH,使用传统的DNS是无效

霏艺Faye at 2020-04-06
1

传统过程:

1.火狐向传统DNS请求 Google的IP地址

2.火狐连接得到的IP地址,并给出带SNI的ClientHello

3.正常浏览网页

DoH + ESNI过程

1.火狐通过DoH请求Google的IP地址,并得到TXT record

2.火狐根据TXT record得到公钥,和自己的私钥,计算得到AEAD的密钥

3.火狐用AEAD密钥加密了SNI

4.火狐连接Google的IP地址,并发起TLS握手,其中SNI字段被加密了

5.正常浏览网页

代码流程:

第一步

firefox\netwerk\dns\TRR.cpp 文件的 TRR::SendHTTPRequest() 发起了DoH请求,查询服务器的IP地址

同文件的TRR::On200Response(nsIChannel* aChannel)函数解析了DoH的应答,根据TXT字段,设置了公钥

第二步

firefox\security\nss\lib\ssl\tls13esni.c 文件的 SSLExp_SetESNIKeyPair 把得到的record记录,设置到 ss->esniKeys = keys; 里去,完成了服务器公钥的设置

第三步

firefox\security\nss\lib\ssl\ssl3con.c 文件的 tls13_SetupClientHello 开始组装ClientHello报文,和ESNI相关的这行

rv = tls13_ClientSetupESNI(ss);

firefox\security\nss\lib\ssl\tls13esni.c的tls13_ClientSetupESNI函数 调用 tls13_CreateKeyShare函数,得到

    ss->xtnData.esniPrivateKey = keyPair;
    ss->xtnData.esniSuite = suite;
    ss->xtnData.peerEsniShare = share;

第四步

firefox\security\nss\lib\ssl\ssl3ext.c 文件的 ssl_ConstructExtensions 开始组装clienthello的ext部分

重点是

rv = (*sender->ex_sender)(ss, &ss->xtnData, buf, &append);

其中 ex_sender 函数指针指向 static const ssl3ExtensionHandler clientHelloHandlers[] 的 ex_sender

因为我们关心的是ESNI,所以看 tls13_ServerHandleEsniXtn, 每种ext都有自己的ex_sender函数,挺方便扩展的

第五步

firefox\security\nss\lib\ssl\tls13exthandle.c 文件的tls13_ClientSendEsniXtn函数

aead = tls13_GetAead(ssl_GetBulkCipherDef(suiteDef));

得到了具体的aead算法加密函数,并调用 tls13_ComputeESNIKeys 得到了AEAD的密钥

    rv = aead(&keyMat, PR_FALSE /* Encrypt */,
              outBuf, &outLen, sizeof(outBuf),
              SSL_BUFFER_BASE(&sni),
              SSL_BUFFER_LEN(&sni),
              SSL_BUFFER_BASE(&aadInput),
              SSL_BUFFER_LEN(&aadInput));

最后调用aead函数加密sni,得到esni字符串outBuf,并完成最后的ext组装!

@张怀义 我讲完火狐的ESNI加密流程代码分析了

霏艺Faye at 2020-04-06
2

dig TXT 2049bbs.xyz +short

这条命令,可以查到 2049 的esni record

霏艺Faye at 2020-04-06
3

lisa大神,我明天入职报到,估计来不及看了

等我看完了,告诉你

张怀义 at 2020-04-06
4

https://blog.cloudflare.com/zh/encrypted-sni-zh/

张怀义 at 2020-04-06
5

我觉得讲的也很细,不过代码实现没讲。搜了下,的确没人讲解火狐esni代码实现的文章

张怀义 at 2020-04-06
6

@小二 麻烦把开头这句话删了,谢谢!

先啰嗦一句,估计这是全网唯一一篇中文讲解ESNI的文章。其他语言有没有,我就不知道了。

霏艺Faye at 2020-04-07
7

@立紗Lisa #1 我想请问为什么使用传统的DNS是无效的呢? 传统的查询也能查询txt记录呀! 如果在一个局域网内,路由器利用DOH查询无污染dns,并缓存结果,然后局域网其他机器用传统dns方式向路由器查询,为什么esni也不起作用呢?

我的2049 at 2020-04-09
8

@我的2049 #8 立纱大神,现在不在。我来回答吧。传统的DNS是UDP明文。存在中间人攻击把txt里的公钥换成攻击者的,然后esni对攻击者来说就是明文了。所以,rfc规范里明确要求,不能使用传统DNS来实现esni。

等Lisa回答。。。

张怀义 at 2020-04-09
9

@霏艺Faye #1 关于network.trr.bootstrapAddress好像讲错了, 官方解释是 by setting this field to the IP address of the host name used in “network.trr.uri”, you can bypass using the system native resolver for it. This avoids that initial (native) name resolve for the host name mentioned in the network.trr.uri pref. 就是直接把doh 服务器的IP 填在这里,这样就能避免doh 服务器被域名污染了.

我的2049 at 2020-05-28
10

@我的2049 #10 你理解错了

火狐先从 network.trr.uri 这里请求目的域名的IP

如果这个network.trr.uri 使用了域名

那么就用 network.trr.bootstrapAddress 去查 DoH的IP

network.trr.bootstrapAddress 这个是传统的DNS

霏艺Faye at 2020-05-29
11