【2049首发】ESNI细节讲解
先啰嗦一句,估计这是全网唯一一篇中文讲解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.发送给服务器
@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是无效的
传统过程:
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加密流程代码分析了
dig TXT 2049bbs.xyz +short
这条命令,可以查到 2049 的esni record
lisa大神,我明天入职报到,估计来不及看了
等我看完了,告诉你
https://blog.cloudflare.com/zh/encrypted-sni-zh/
我觉得讲的也很细,不过代码实现没讲。搜了下,的确没人讲解火狐esni代码实现的文章
@霏艺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 服务器被域名污染了.