Tor源码分析七 — 握手协议
本节主要讲述Tor系统中所用到的握手协议。握手协议分三层:TCP握手;TLS握手;Tor握手。其中Tor握手又分为三个层次:OR握手;链路建立;流建立。
TCP的三次握手我想应该学计算机方向的朋友无人不知了,所以此处就略去。而TLS是SSL的升级版本,其握手过程与SSLv3几乎一致。同时由于TLS根据客户端的不同握手选择,会有些许握手过程中的差别,我们希望大家能够找到TLS相关的书籍翻阅。此处就不再过多的叙述TLS的握手过程。
本文的重心着眼于描述Tor握手全过程,更甚者是描述整个Tor协议。协议的主要内容包括协议使用的主要数据结构,连接的建立和初始化,连接的协商,链路管理,流管理以及流量控制等部分。在本文描述完全之后,大家会有个对Tor系统中结点之间如何进行交流的一个比较完整的印象。当然,可以说本文中几乎全部内容都来自tor-spec.txt,也就是Tor Protocol Specification。有兴趣的朋友可以参看原文,原文必定比笔者讲的要详细清楚。
1. 系统概述
Tor是一个用以保证用户匿名性的分布式网络。其主要的服务对象是用户所使用的基于TCP连接的应用。在使用Tor系统之时,客户端会选择一系列的结点建立Tor链路。链路中的结点,只知道其前继结点和后继结点,不知道链路中的其他结点信息。这样从某种程度上保证了坏结点存在对链路匿名性的破坏。链路中的数据流是以Cell数据包的形式进行传输的。Cell数据包的负载符合洋葱路由的特性,即由原点出发的负载被多层加密,传输过程中被层层解密;而由出口结点返回的数据在传输过程中层层加密,到原点后由原点一次性解密。而在这个加解密过程中所使用到的密钥,则是由Tor协议提供机制来协商生成的。下面先简要描述系统中所使用到的最常见的密钥:
1)Identity Key(非对称密钥的公钥):系统中的结点均有其自身所对应的ID密钥,该密钥的作用是用以证书签名或标识路由身份。实际上,在前面的文章中我们也提到过,Tor结点根据自身的身份不同,会使用不同的Identity Key。但是,要说明的是,在目录服务器已注册的结点,不可以随意更改其Identity Key,否则就无法正常提供服务。
2)Onion Key(非对称密钥的公钥):系统中提供中继服务的结点,即OR结点,均有一个比ID密钥更短期的一周一换的洋葱密钥。该密钥的主要作用是用来保护OP与OR之间进行DH密钥交换协议的前半部分信息(g^x)的安全性。利用Onion Key,OP与OR之间可以教安全的实行DH密钥交换协议从而协商出洋葱对称密钥。
3)Connection Key(对称密钥):洋葱对称密钥,是OP与各个OR之间协商的临时对称密钥,用来完成上文中叙述的层层加解密功能。
另外,由于结点之间的通信都是由TLS连接来保障的,所以必定会在TLS那个层次中有一对保障TLS通信安全的对称密钥。这并非Tor系统所关心的密钥,所以略去。
注:这个部分的说明和Tor Protocol Specification之中的说明或许有所不同,大家请自行参照更多其它材料来进行分析。最终目标就是理解整个Tor系统的密钥组织结构及其使用。下面会再对整个系统的密码学相关过程做更详细的分析。
上图中给出了Tor系统建立链路的整个过程,下面我们对它进行深入的分析。
1. Alice(OP)开启Tor系统之后,需要建立第一条可用链路,她选择了OR1作为链路第一跳,并与OR1建立起了TLS连接;(TLS的建立与上层系统关系极小)
2. Alice成功与OR1建立TLS连接之后,他们之间的通信就是被TLS连接保障的加密通信。此时,Alice开始执行Tor握手协议第一步:OR握手;(OR握手的过程图中并未给出,而此过程与TLS握手密切相关,我们可以暂且认为OR握手就包含于TLS握手之中)
3. Alice成功与OR1完成OR握手之后,他们相互之间交换了彼此的信息。此时,Alice开始执行Tor握手协议第二步:链路建立;
1)Tor协议中的协议数据交换使用的是固定长度的Cell,以下为Cell的简单框图:
实际上现有版本的Cell结构已经发生了些许改变,但是此处我们用最原始的Cell结构进行说明。下面我们用稍微形式化的方式来描述上述结构:
Cell ::= Control Cell | Relay Cell
Control Cell ::= Control Header || Control Payload Length = 2B + 1B + 509B = 512B
Control Header ::= CircID || Control CMD Length = 2B + 1B = 3B
Control Payload ::= Control DATA Length = 509B
Control CMD ::= padding | create | created | relay | destory Length = 1B
Relay Cell ::= Control Header || Relay Header || Relay Payload Length = 3B + 11B + 498B = 512B
Relay Header ::= StreamID || Digest || Len || Relay CMD Length = 2B + 6B + 2B + 1B = 11B
Relay Payload ::= Relay DATA Length = 498B
Relay CMD ::= data | extend | extended | truncate | truncated | begin | connected | end | teardown | sendme | drop Length = 1B
上述这些定义是第一版本的定义,与框图中的定义相符。而现用的Tor系统中的Cell框图在类型上和格式上都稍有区别。此处并没有给出框图,只是对原来的Relay Header的改变做一定说明。因为由于Relay Header的修改,Tor源代码中的Stream ID控制部分变得更加简化。
Relay Header v2 ::= Relay CMD || ‘Recognized’ || StreamID || Digest || Length Length = 1B + 2B + 2B + 4B + 2B = 11B
由此可知,后续版本的Relay头部有了位置的变化,同时缩短了Digest的长度,增加了’Recognized’字段。我们会知道,’Recognized’字段是极其重要的字段。
下面我们利用先前定义的格式来具体地描述链路建立过程与其中的数据传递细节。
2)Alice与OR1建立链路
首先,Alice通过查找先前获得的路由信息表来获取OR1的Onion Key 1,后边简称OK1。利用OK1,Alice构造如下的Cell传送到OR1,试图开启链路:
Control Cell (Create c1) = CircID || Control CMD || Control Payload
CircID = Random picked CircID (随机选取的数字a1)
Control CMD = create
Control Payload = AE(OK1, g^x1) || padding if neccessary (用公钥OK1加密的DH密钥交换协议参数g^x1,再串联上填充字节)
而后,OR1检查收取到的Cell,发现是命令为create的Control Cell。他记录下对应的CircID a1,用自己的OK1私钥解密并取出DH密钥交换协议参数g^x1,同时选取DH密钥交换协议第二部分参数g^y1,计算出DH密钥交换协议生成的对称密钥SK1,构造如下的Cell发送回Alice,试图告诉Alice链路可以成功开启:
Control Cell (Created c1) = CircID || Control CMD || Control Payload
CircID = a1
Control CMD = created
Control Payload = g^y1 || H(SK1) || padding if neccessary (明文形式的DH密钥交换协议参数g^y1,串联上DH密钥交换协议生成的密钥SK1的哈希值,最后再串联上必要的填充字节)
其后,Alice根据接收Cell的CircID a1能够确认她收到的是OR1的回复。她检测Cell的命令是created,则得知链路创建成功,于是就取出DH密钥交换协议的第二部分参数g^y1,计算出DH密钥交换协议生成的对称密钥SK1。利用该SK1,Alice就可以有效地构造洋葱数据了。
3)Alice与OR2建立链路
这个部分确切的说应该是Alice通过OR1与OR2建立链路。若是仅仅的Alice与OR2建立链路,那么链路的建立过程与上述的过程完全一致,没有差别。所以这个小部分说的是Alice实现链路的拓展。
首先,Alice明确自己要通过OR1实现链路的拓展,并且选定了OR2作为拓展链路的目标结点,那么此时,她构造如下的Cell发送给OR1,要求OR1为其实现链路拓展的目标:
Relay Cell (Extend e1) = CircID || Control CMD || SE(SK1, Relay Header || Relay Payload) (利用对称密钥SK1对数据进行的对称加密)
CircID = a1
Control CMD = relay
Relay Header = Relay CMD || ‘Recognized’ || StreamID || Digest || Length
Relay CMD = extend
StreamID = 0 (其实随意什么值均可,extend命令下的StreamID没有实际用处)
Relay Payload = OR2 || AE(OK2, g^x2) || padding if neccessary
其后,OR1接收到了上述Cell,打开Cell头部看到CircID a1,并且是个Relay Cell,就用之前与Alice生成的对称密钥SK1对负载部分进行解密。解密之后,认出了Relay Header中的’Recognized‘,此时发现,OR1被要求做的工作是extend。于是,OR1查看Relay Payload找到需要拓展链路的目标为OR2,取出AE(OK2, g^x2)。之后就像Alice与OR1建立链路的过程一样,OR1发起与OR2建立链路的过程。构造的Cell如下:
Control Cell (Create c2) = CircID || Control CMD || Control Payload
CircID = Random picked CircID (随机选取的数字a2,关联a1与a2)
Control CMD = create
Control Payload = AE(OK2, g^x2) || padding if neccessary (Alice传递而来的协议数据,再串联上填充字节)
而后,OR2的操作与上轮中OR1反馈Alice建立链路的操作一模一样。OR2返回数据之后,OR1将数据用对称密钥SK1加密,返回Alice。构造如下形式的Cell:
Relay Cell (Extended e1) = CircID || Control CMD || SE(SK1, Relay Header || Relay Payload) (利用对称密钥SK1对数据进行的对称加密)
CircID = a1
Control CMD = relay
Relay Header = Relay CMD || ‘Recognized’ || StreamID || Digest || Length
Relay CMD = extended
StreamID = 0 (其实随意什么值均可,extended命令下的StreamID没有实际用处)
Relay Payload = g^y2 || H(SK2) || padding if neccessary
最后,Alice拿到数据包,对应上CircID,解密发现识别成功,并且数据命令为拓展链路成功,于是计算出Alice与OR2的对称密钥SK2。
4. 至此,Alice想要建立的链路建立完毕。此时,Alice开始Tor握手连接第三步:流建立;最后发送数据。
Alice在建立完链路之后,她希望向外发送请求数据。但是,她需要先在给定链路中开启一个数据流。也就是说她需要选定一个数据流的出口位置,是OR1,还是OR2?图中假定她所选定的数据流出口为OR2,则她此时需要向OR2发送命令,要求OR2开启对应的流为其服务。那么她需要构造如下的命令包:
Relay Cell (begin b1) = CircID || Control CMD || SE(SK1, SE(SK2, Relay Header || Relay Payload)) (利用对称密钥SK1对数据进行的对称加密)
CircID = a1
Control CMD = relay
Relay Header = Relay CMD || ‘Recognized’ || StreamID || Digest || Length (OR1看不到)
Relay CMD = begin (OR1看不到)
StreamID = s1 (OR1看不到)
Relay Payload = request (OR1看不到)
OR1收取数据之后,经过解密之后,发现无法辨认’Recognized’。因为此时’Recognized’还被一层对称加密所包裹。所以他断定此Cell的目的地并非自己,于是找到a1对应的CircID a2,发送到对应目标OR2。构造的数据如下:
Relay Cell (begin b1) = CircID || Control CMD || SE(SK2, Relay Header || Relay Payload) (利用对称密钥SK1对数据进行的对称加密)
CircID = a2
Control CMD = relay
Relay Header = Relay CMD || ‘Recognized’ || StreamID || Digest || Length
Relay CMD = begin
StreamID = s1
Relay Payload = request(host, port)
OR2收到了上述数据,解密发现被认出,获取begin命令,则其开启向目标服务器的TCP连接。重要的是,他记录了该StreamID与其目标服务器之间的对应关系。之后,只要来次该CircID,StreamID的数据,全部一致性抛出给对应的目标服务器。数据回传的过程与数据传出的过程类似,应用数据传输的过程与上述传输的过程类似,所以下面就将其他部分的说明略去。
2. TLS握手与OR握手
在系统概述部分我们也简单提到了TLS握手与OR握手之间的密切关系,但是到底是如何的关系呢?又为什么要有OR握手这么奇怪的协议呢?我们知道,TLS握手的过程中,通信双方交换的信息并非只为了协商出最后的通信对称密钥,还有更多的信息。例如说,TLS握手的过程中,通信双方需要确定各自使用的版本,需要明确是否在和正确的对象进行交换,还需要交换写时间戳等信息。这些信息的交换,使得TLS握手是双方能够确信他们之间交互的可行性,可信性和安全性。OR握手,其实也有与TLS握手的目标差不多,只不过他是出于Tor系统OR层上的。也就是说,OR握手的目的,是确信自己与一个Tor系统内的中继服务器进行交换,同时交换各自版本信息,验证身份,再交换些许额外信息。因为TLS握手与OR握手的所处层次还是有差别的,所以两者不能相互替代。
我们说,OR握手是建立在TLS握手基础上的。Tor系统中双方在通信TLS握手完成之后,立即进行相对应的OR握手。而在这个过程中,使用到的TLS握手方式也是不一定相同的。Tor系统会根据版本的不同,而实行不同的握手方式,下面我们就来详细地描述整个过程。
1)概要
Tor系统中主机之间所使用到的协议为TLS/SSLv3。所有版本的Tor系统的实现,都必须能够支持SSLv3的密码工具集”SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA”,同时应该能够支持TLS的密码工具集”TLS_DHE_RSA_WITH_AES_128_CBC_SHA”。
Tor系统中,TLS握手的完成有三种方式:”certificate-up-front”;”renegotiation”;”in-protocol”。
2)三种方式细则
“certificate-up-front” (a.k.a “the v1 handshake”):连接的发起者通常会发送包含两个证书的证书链。这两个证书分别为用短期连接公钥加密的X.509证书与其自签名的包含其自身identity key的X.509证书。连接的接收者通常也会发送类似的证书链。这种握手方式下,连接的发起者只允许使用固定的密码工具集:(TLS握手初期有发起者给定,详见TLS握手)
TLS_DHE_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_RSA_WITH_AES_128_CBC_SHA
SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA
“renegotiation” (a.k.a “the v2 handshake”):连接的发起者不再发送任何证书,而连接的接受者发送一个单独的连接证书。一旦TLS连接完成,连接双方利用包含两个证书的证书链重新协商连接。这种握手方式下,连接的发起者至少要使用比上述固定的密码工具集多一种方式的工具集。一般情况下,我们要求使用该握手方式的连接发起者至少使用扩大的固定密码工具集,包括20多种密码套件(包括上述三种),我们称之为”Fixed Ciphercuite List”。如果发起者使用扩大的固定密码工具集,那么接受者仅能挑选固定密码工具集中的密码套件;如果发起者使用比扩大的固定密码工具集还大的工具集,那么接受者有理由相信连接发起者支持所有他所声称的密码套件。
”in-protocol” (a.k.a “the v3 handshake”):与v2握手方式几乎一致,只是通信双方没有再使用证书链重新协商连接,而是在单证书中包含了些限制来指示双方需要使用的握手版本为v3以上版本,并且使用了比较特别的重新协商的方式。这些证书中的限制包括:
* The certificate is self-signed
* Component other than “commonName” is in the subject or issuer DN of the certificate
* The commonName of the subject or issuer ends with a suffix other than “.net”
* The certificate’s public key modules is longer than 1024 bits
特殊的重新协商方式是三次单向交互(*代表客户端希望自身验证时需要发出的信息):
Client Server
VERSION —————>
VERSION
CERT
<————— AUTH-CHALLENGE
NETINFO
NETINFO
CERT* —————>
AUTHENTICATE*
3. 流量控制
Tor系统包含了三个层次的流量控制:OR层;Circuit层;Stream层。这里只做简要描述,就不再贴出代码进行分析。笔者今天心力交瘁= =。下面为基本层次结构:
Stream Layer
--------------------
Circuit Layer
--------------------
OR Layer
--------------------
TLS Layer
在这三层的流量控制之中,OR层的流量控制利用的是令牌桶机制,而另外两层所用的是类似简化的滑动窗口机制。
1)令牌桶机制
令牌桶机制的大致原理是,系统维护一个令牌桶,每隔一段时间往里添加令牌,而每个需要发出的消息需要抓取到一个令牌之后才可以发出。也就是说,发出消息消耗令牌,而每隔一段时间,系统会补充令牌。这样,通过控制令牌桶中令牌的数量,来限制消息发出的速度和流量。在稳定状态下,消息流出的速度至多是系统填充令牌的速度。而系统填充令牌的速度是可以通过配置文件进行配置的,所以也就是说,稳定时消息流出的最大速度也是可配置的。我们可以在代码中很容易地找到关于控制令牌数的全局变量。他们分别是控制经过自身的全部数据的读写令牌桶,以及控制以自身为传递结点的数据的读写令牌桶:
int global_read_bucket; /**< Max number of bytes I can read this second. */
int global_write_bucket; /**< Max number of bytes I can write this second. */
/** Max number of relayed (bandwidth class 1) bytes I can read this second. */
int global_relayed_read_bucket;
/** Max number of relayed (bandwidth class 1) bytes I can write this second. */
int global_relayed_write_bucket;
系统在收到或发出相应数据的时候,会对令牌桶内令牌数进行删减,利用函数connection_buckets_decrement()。系统每隔refilltime时间便对令牌桶进行填充,利用函数connection_bucket_refill_helper()。我们知道refill时间是可以有配置文件或命令行参数进行配置的,他的默认值为100msec。
2)简化滑动窗口机制
Circuit层与Stream层的流量控制机制则要稍微复杂些。滑窗机制的大致过程是:一开始系统维护一个滑窗,允许消息发送者发送滑窗容量多的消息。随着消息的发送,滑窗容量减小,最终会使得滑窗容量降为零。此时,消息发送方需要等待消息接收方给其发来一个继续发送消息的请求。接收到该请求之后,消息发送方增加适当大小的滑窗容量。于是,消息发送方又可以继续发送消息,知道窗口容量用完。当然,在窗口未降为零时,消息发送方自然也是可以返回要求消息发送方继续发消息的请求,这样可以保持滑窗始终有容量。
Tor系统中Circuit层与Stream层使用的滑窗由circuit_t与edge_connection_t两个结构体中的package_window和deliver_window来表示。我们此处先针对Circuit层的情况进行分析,因为他与Stream层的处理方式一模一样,但是又有些特别之处。大致的框图结构如下:
1. —–> OP package window ======================> EXIT deliver window —–>
2. <—– OP delivery window <====================== EXIT package window <—–
上述描述图中,1中为应用请求数据进入OP主机,并从OP主机流出,经过Tor链路,到达EXIT主机,最终流出Tor网络的过程;2中为应用响应数据进入EXIT主机,并从EXIT主机流出,经过Tor链路,达到OP主机,最终被返回到指定应用程序的过程。我们来讨论这两个在Tor系统中最普遍的过程执行之中的窗口变化情况。
针对1过程,数据从OP主机流出,耗费了其打包窗口的容量,例如从1000降到900;数据流入EXIT主机并流出网络,耗费了其传递窗口的容量,例如从1000降到900;此时,EXIT传递窗口的大小下降到了一定的值,但他觉得他还可以处理更多数据,于是就向OP发送sendme请求,告诉OP他可以接收更多数据;请求发送出之前,EXIT就先增加自己的传递窗口大小,例如从900增加到1000;请求发出之后,被OP收到,OP则相应地增加自己的打包窗口大小,例如从900增加到1000。
针对2过程,其实完全是1过程进行了反向而已。在此过程中,数据主要由EXIT发往OP,而命令sendme是由OP发往EXIT。
注意:
i)整个过程之中,没有提到中间OR结点。实际上,中间结点在Circuit层与Stream层不做任何流量控制的处理,他们只负责传递消息。相应的在代码中会发现,所有的关于流量控制的部分,都是在自身的应用数据要发出之前,以及接收到的Relay数据被识别之后进行的。所以,这两个部分根本和OR中间结点没有关系。因为OR中间结点无法插入自身应用数据,更不可能识别任何Relay数据。
ii)OP端需要为每一条链路的每个可能出口准备窗口记录;EXIT需要为每一条链路准备窗口记录。我们研究整个系统的时候会发现,链路建立完成之后,OP可以随意选择链路之中的出口,以建立数据流。所以,对于OP来说,他所进行的端到端链路流量控制必须依照链路数据出口或入口结点进行控制。具体的说,OP的链路流量控制,利用的是结构体crypt_path_t。origin_circuit_t结构体中含有包含链路中所有结点的crypt_path_t双向循环列表,以此可以很容易地管理package_window和deliver_window两个变量。对于EXIT来说,他只需要处理好每个链路内的package_window和deliver_window变量即可,这两变量在circuit_t结构体内也能找到。
iii)Stream层的流量控制与Circuit层几乎一致,package_window和deliver_window在edge_connection_t结构体内,其他不再多说。
4. 额外的说明
本文中进行的介绍,加入了许多笔者自己的分析,大家可以先看Tor Protocol Specification,之后根据自己的想法与笔者的相比较。当然必然存在错误,请大家批评指正。同时,此文中删去了很多关于消息格式,各种包的格式等具体细则,请大家参看tor_spec.txt。
未完待续