FW:HTTPS连接最初的若干毫秒
原文:http://www.infoq.com/cn/articles/HTTPS-Connection-Jeff-Moser
HTTPS连接最初的若干毫秒
作者 Jeff Moser 译者 马国耀 发布于 2009年9月23日 上午12时28分
当你在浏览了一个网站上面的商品之后,点击“继续并结帐”时会发生什么?本文即将对(浏览器)与Amazon建立安全连接的整个过程中最初的若干毫秒进行分析。当你点击继续按钮时一个新的页面将被加载:
在短暂的220毫秒内,发生了很多有趣的事情,Firefox修改了地址栏的颜色,并在其右下角放置了一个锁状的图标。在我最喜爱的网络工具Wireshark以及略微修改的Firefox调试版的帮助下,我们可以对正在发生的事情看个究竟。
根据RFC 2818协议的规定,Firefox明白“https”意味着它应该连接Amazon.com的443端口:
大多数人将HTTPS和SSL(Secure Sockets Layer)联系起来,SSL是Netscape公司在90年代中期发明的。随着时间的推移这种说法就渐渐变得不准确了。由于Netscape失去了市场份额,它将SSL的维护工作移交给因特网工程任务组(IETF)。第一个后Netscape版本被重新命名为安全传输层协议(TLS),TLS1.0是在1999年1月份发布的。由于TLS诞生都10年了, 所以真正的“SSL”传输其实是几乎见不到。
Client Hello
TLS将所有的网络传输打包成不同的“记录”类型。我们看到从浏览器出来的第一个字节是一个十六进制(hex)字节0x16=22,这说明它是一个“握手”记录:
后面的两个字节是0x0301,意味着它的版本是3.1,事实上TLS1.0就是SSL3.1。
“握手”记录被分解成若干消息。第一个就是“Client Hello”消息(0x01)。这里面有很重要的几点:
- 随机数:
- 前面的四个字节是当前的协调世界时(Coordinated Universal Time,UTC),它的格式是Unix时间戳,也就是从1970年1月1日起到此刻所经历的秒数。在本例中的该数字是0x4a2f07ca。跟随其后是28字节的随机数,它将在后面使用。
- 会话标识(session id):
- 在这里它是空值或者是null。如果在几秒前该浏览器曾连接过Amazon.com,它就可能继续使用前面的会话,而不需要重新执行整个“握手”过程。
- 密码套件:
- 它是浏览器所支持的密码算法的一个列表。最上面的是一个很强大的组合“TLS_ECDHE_ECDSA_与_AES_256_CBC_SHA,下面还有该浏览器支持的另外33个选择。如果现在不明白它们的含义也不必担心,后面你将会发现Amazon并没有选择最上面的强大组合。
- server_name extension:
- 通过它告诉Amazon.com,浏览器正要连接https://www.amazon.com/。这样做非常方便,因为TLS“握手”发生在所有的HTTP传输之前 。HTTP协议中有一个“Host”头, 这就允许了Internet托管公司出于成本的考虑将上百个网站绑定在同一IP 地址上。传统意义上SSL要求每一个地址有一个不同的IP,而这个扩展就使得服务器能够正确响应浏览器所请求的(服务器)证书。最后注意一点,对于 IPv4的地址这一扩展的有效期大概能再多一周左右。
Server Hello
对应于Client Hello,Amazon.com也返回一个“握手”记录,它是两个庞大的数据包(2,551字节)。该记录的版本号是0x0301,也就意味这Amazon支持我们使用的TLS1.0版本的请求。这个记录分成三个子消息,他们包含着一些有趣的信息。
- "Server Hello" 消息(2):
- 我们得到服务器返回的四字节的Unix时间戳以及28字节的随机数,它也将在后面使用(译注:注Clien Hello是发了一个随机数,这两个随机数将用于产生最后的对称密钥)。
- 一个32字节的会话标识,有了它,随后重连服务器就不需要再执行一个完整的握手过程了。
- 从我们提供的34个可选的密码套件中,Amazon选择的套件“TLS_RSA_WITH_RC4_128_MD5”(0x0004)。也就是说它将使用“RSA” 公钥算法来验证证书以及交换密钥,用RC4加密算法对数据进行加密,使用MD5哈 希算法来校验消息内容。后面会对它们做详细介绍。我个人认为Amazon选择这个密码套件的原因是出于自私的考虑(译注:这里说“自私”的意思 是,Amazon出于性能的考虑,降低了安全的强度)。这个套件是这34个套件中消耗CPU最少的一个,因此Amazon选择它就是为了节省一些CPU来 处理更多的用户连接。还有一个不大可能的原因是他们要特别地向Ron Rivest致敬,这个套件中所用到的三个算法都是他发明的。
- 证书消息(11):
这个巨大的消息占据了2464个字节,它是一个证书,客户端用它对Amazon进行认证。它并不是什么花哨的玩意,通过浏览器就可以看到它的大部分内容:
- “Server Hello 结束”消息(14):
这是一个0字节的消息,它告诉客户端“hello”过程已经完成,也就意味着服务端将不验证客户端的证书。
校验证书
浏览器必须要确定它是否要信任Amazon.com,在这里是通过证书判断的。它检查Amazon的证书并且检查现在的时间是否在证书的有效期之内,该有效期规定的是“不能早于”2008年8月26日且“不能晚于”2009年8月27日。此外,它还要确保该证书的公钥已经被授权在进行密钥交换的过程中使用。
为什么我们应该相信这个证书呢?
在证书上附有一份“签名”,这个签名事实上是一个big-endian(译注:Big-Endian和Little-Endian是一种二进位资料储存或传输的格式,Big-Endian的最高位字节在最前头,而Little-Endian的最高位字节在最后面)格式的长整型数:
任何人都有可能发给我们这些字节,为什么我们要相信这个签名呢?为了回答这个问题,有必要先到这里mathemagic land去速成一些相关知识:
穿插:简短的,不那么可怕的RSA向导
有时候人们想知道数学和编程有何联系。证书就是应用数学的一个非常实际的例子。Amazon的证书告诉我们应该用RSA算法来校验其签名。RSA是MIT的教授Ron Rivest,Adi Shamir和Len Adleman在70年代创造出来的,他们三人创造了一个巧妙的方法将2000多年来 数学发展的思想汇合起来形成了一个漂亮而简单的算法:
首先选择两个很大的质数“p”和“q”,并对他们求积得到“n=p*q”。接下来,取一个较小的“e”作为指数,它用作“加密指数”,而对e的进行特殊的逆反函数计算所得到的“d”作为“解密指数”。然后将“n”和“e”公开出去,而对“d”要保密,对于“p”和“q”你可以把它们扔掉,也可以像“d”一样保密起来。真正重要的要记住“d”和“e”是相互的逆反。
现在,如果你有一些消息,那么你只需要将该消息的字节翻译成一个数“M”,若要对这个消息进行“加密”形成“密文”的话,你就这么计算:
C ≡ Me (mod n)
它意思是先求“M”的“e”次方,然后对它应用模数“n”求余。举个例子,11AM +3 hours ≡ 2 (PM)(mod 12 hours)。接收者知道(解密用的)“d”,而“d”可以对已加密的消息进行反转并还原消息:
Cd ≡ (Me)d ≡ Me*d ≡ M1 ≡ M (mod n)
另一件有意思的事情是拿着“d”的人可以对一个文档进行签名,其做法是文档“M”求“d”次幂然后应用模数“n”求余数“S”:
Md ≡ S (mod n)
这种做法成立的原因是“签名者”将“S”,“M”,“e”以及“n”公开出去,任何人都可以通过这样一个简单的计算来验证“S”是否由“签名者”所签(译 注:因为每个“e”都是某个签名者所独有的,而“e”和“d”是成对出现的,所以签名者用自己的“d”签名,只能拿这个“d”所对应的“e”才能还原消息,这就到了签名的作用):
Se ≡ (Md)e ≡ Md*e ≡ Me*d ≡ M1 ≡ M (mod n)
像RSA这样的公钥密码算法经常被称之为“非对称”算法,因为加密的密钥(本例子中的“e”)和解密的密钥是不相同的(本例子中是“d”)。对所有的数应用“mod n”的目的是使攻击者不可能使用简单的技术(如过去我们使用的对数)破解它。RSA的神奇之处是你可以很快计算/加密C ≡ Me (mod n),而在不知道“d”的情况下计算/解密Cd ≡ M (mod n)是非常困难的。如前面所说,“d”来自于对“n”的因子分解,而“n”来自于“p”和“q”,求解“d”是一件复杂的事情。
签名验证
在现实世界里使用RSA时应该谨记的重要的一点是,前文提到的所有数字都应是很大的数,只有这样才能保证,即使用目前最好的算法也 很难攻破上述算法。到底多大才算很大的数呢?Amazon.com的证书是由“VeriSign Class 3 Secure Server CA.” 签名的。从这个证书中我们可以看出,VeriSign所使用的模数“n”是一个2048比特长的数字,表示成10进制数的长度是617位:
1890572922 9464742433 9498401781 6528521078 8629616064 3051642608 4317020197 7241822595 6075980039 8371048211 4887504542 4200635317 0422636532 2091550579 0341204005 1169453804 7325464426 0479594122 4167270607 6731441028 3698615569 9947933786 3789783838 5829991518 1037601365 0218058341 7944190228 0926880299 3425241541 4300090021 1055372661 2125414429 9349272172 5333752665 6605550620 5558450610 3253786958 8361121949 2417723618 5199653627 5260212221 0847786057 9342235500 9443918198 9038906234 1550747726 8041766919 1500918876 1961879460 3091993360 6376719337 6644159792 1249204891 7079005527 7689341573 9395596650 5484628101 0469658502 1566385762 0175231997 6268718746 7514321
(如果你要试图从这个“n”中找到“p”和“q”那就祝你好运,假如你能找到他们,那么你能产生像真的一样的VeriSign证书了)
VeriSign的“e”的值是2^16 + 1 = 65537。当然,他们对相应的“d”保密,也许被放在一个安全的硬件设备上,由视网膜扫描机和武装部队保护着。在签名之前,VeriSign要通过现实世界的“握手”来验证Amazon.com声明的证书内容,包括审查他们的很多商务文档。一旦VeriSign对这些文档满意了,他们使用SHA-1哈希算法对包含所有声明的证书进行计算获得一个哈希值。在Wireshark中看到整个证书的样子如下图“signedCertificate”部分所示:
上面的说法(整个证书如同signedCertificate所示)说有点用词不当,因为证书的实际含义是签名者将要进行签名的部分,而不是已经包含了签名信息的字节。
实际的签名,也就是“S”,是Wireshark包中简单地称为“encrypted”的部分(见上图)。如果我们求“S”的“e”次幂(“e”是VeriSign公开的),然后在对该结果取模“n”并得到余数,那么我们得到的“已解密”十六进制签名是:
0001FFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF FFFFFFFF00302130 0906052B0E03021A 05000414C19F8786 871775C60EFE0542 E4C2167C830539DB
基于PKCS #1 v1.5标准, 最开始的字节是“00”,它“保证了加密块被转换成整数之后,大小不会超过模数”。第二个字节“01”说明这是一个私钥操作(如,它是一个签名)。后面是 一大堆“FF”,用他们填充这个结果,使之足够大。填充字节以“00”结束,后面跟的是"30 21 30 09 06 05 2B 0E 03 02 1A 05 00 04 14",PKCS #1 v2.就是这样来指定SHA-1哈希算法。最后的20个字节是对“signedCertificate”部分的SHA-1哈希摘要。
因为解密的值有着正确的格式并且最后的字节和我们独立计算得到的哈希值相同,我们可以认为这就是由拥有“VeriSign Class 3 Secure Server CA”私钥的人所签名的,而我们隐含地信任只有VeriSign知道这个私钥“d”。
但我们为什么要信任它呢?因为在信任链上已经没有更高级别了。
最上层的“VeriSign Class 3 Public Primary Certification Authority”是自签名的。这个证书自从网络安全服务(Network Security ServicesNSS)库中certdata.txt的1.4版本之后就已经被嵌入Mozilla的产品中作为一个隐含的受信证书。它是由NetScape的Robert Relyea在2000年9月6日登记进去的,当时他还留下以下评注:
"用这个框架来收集其他网络安全服务,它包括这一个“实时的”certdata.txt ,里面存放了那些我们已经获得许可权可以推向开源的证书(当我们从所有者那里获得许可权之后,会有更多的证书加入这个文档中)”
由于该证书的有效期范围是从1996年1月28日到2028年8月1日,所以这个决定是意义深远的。
Ken Thompson在他的“Reflection on Trusting Trust”中解释的非常好,你最终不得不隐式地信任某人。在这个问题上别无选择。本例中我们隐含地信任Robert Relyea做了个好的选择,我们也希望Mozilla的内建证书策略对其他的内建证书也是合理的。
这里要记住的一点是所有这些证书和签名都简单地用来形成一个信任链。在公共网络中,在你还未上任何网站之前,Firefox就已经隐含地信任了VeriSign的根证书。在一个公司内,你可以创建你自己的根证书CA,并将它安装在每个人的电脑上。
或者,你还可以付钱给VeriSign这样的公司并把所有的证书信任链的工作交给它。证书用来通过可信的第三方(这里是VeriSign)建立信任 关系。如果你可以安全地和别人共享密钥,比如“在某人耳边轻声告诉他一长串密码”,那么你就可以使用预共享密钥(PSK,Pre-shared Kay)认证来建立信任关系。TLS对此也有扩展,比如TLS-PSK,以及我本人所喜欢的TLS的安全远程密码扩展。然而,这些扩展没能被广泛地使用和支持,所以他们通常是不切实际的。此外,这些方法增加了负担,它们要求必须通过安全的方式交换密钥,而这比通过TLS来建立信任连接的负担更重(要不然我们何不都这么用呢?)。
我们要做的最后的检查是验证证书上的主机名是否是我们期待访问的。 Nelson Bolyard 在 SSL_AuthCertificate 函数中的注释是这样解释的:
/* 证书是没问题的。这是SSL连接的客户端。
*现在要检查证书中的名字和所期待的主机名是否一致。
*注意:这是我们防范中间人(Man-In-The-Middle)攻击的唯一途经*/
这个检查防范了中间人攻击,因为我们隐含地相信拥有证书的人不会干坏事,例如,只有它真是Amazon.com,它才会对一个证书签名说它是Amazon.com。如果一个攻击者能够通过类似 DNS缓存破坏的技术修改我们的DNS服务器,你可能被蒙蔽了,以为自己正访问正确的网站(如Amazon.com),因为你的浏览器的地址栏看起来就是这个网站。这最后的检查就隐含地信任证书的授权机构可以防止坏事的发生。
Pre-Master Secret
现在我们已经验证了Amazon.com的那些声明,并且知道其公开的加密指数“e”和模数“n”。任何在网络上侦听的人当然也知道这两个数(这点 很明显,因为我们是通过Wireshark获取它们的,别人也可以)。现在我们要创建一个攻击者或偷听者不能辨别的随机密钥,这件事做起来不像听起来那么简单。在1996年,研究者发现Netscape Navigator1.1仅使用三个数据源来作为他们的伪随机数生产器的种子(PRNG),它们是:当前时间,进程号和父进程号。研究显示,这些“随机”源并不那么随机,而且相对比较容易被猜出来。
由于其他的所有东西都是从这三个“随机”源产的,在1996年当时的机器上可能只需25秒中就可以“攻破”SSL“安全”。如果你还不相信寻找随机数是件困难的事,只要问问Debian OpenSSL维护者就知道了。如果随机数搞乱了,那么基于它之上的所有安全都是可疑的。
在Windows上,用于加解密的随机数是通过调用CryptGenRandom函数对来自125个源的抽样数据求hash值产生的。Firefox将该函数产生的随机数和它自己的函数产生的比特码一起作为伪随机数生成器的种子。
虽然这个48字节的“pre-master secret”随机数并不直接使用,但是保证其私密性是非常重要的,因为很多东西都是由它而来的。毫无疑问,Firefox让我们很难看到这个数。我不得不编译了一个调试版本并设置SSLDEBUGFILE和SSLTRACE两个环境变量才能看到它。
在本次会话中,SSLDEBUGFILE中显示的这个pre-master secret是这样的:
4456: SSL[131491792]: Pre-Master Secret [Len: 48]
03 01 bb 7b 08 98 a7 49 de e8 e9 b8 91 52 ec 81 ...{...I.....R..
4c c2 39 7b f6 ba 1c 0a b1 95 50 29 be 02 ad e6 L.9{......P)....
ad 6e 11 3f 20 c4 66 f0 64 22 57 7e e1 06 7a 3b .n.? .f.d"W~..z;
需要指出的是这并非全是随机数, 按约定,最前面的两个字节是TLS版本(03 01)。
密钥交换
现在要把这个密钥传给Amazon.com。由于Amazon选择的是“TLS_RSA_WITH_RC4_128_MD5”,我们将使用RSA算 法来做这件事。输入消息可以是正好48字节的pre-master secret,但是公钥密码学标准(PKCS)#1,RFC1.5 要求用一些随机数来填充这个数使其长度等于模数的长度(1024比特,或者128字节)。这样做就使得攻击者更难找出我们的pre-master secret了,另外,万一有人做傻事,比如重用相同的密钥,它还为我们提供最后一层保护。通过这样一填充,即使我们重用密钥,偷听者在网络上两次看到的数也是不一样的。
又,因为Firefox让我们很难看到这些随机数,我不得不在填充函数中加入一些调试语句才能看到发生的事情:
在这次会话中,填充后的消息如下所示:
00 02 12 A3 EA B1 65 D6 81 6C 13 14 13 62 10 53 23 B3 96 85 FF 24 FA CC 46 11 21 24 A4 81 EA 30 63 95 D4 DC BF 9C CC D0 2E DD 5A A6 41 6A 4E 82 65 7D 70 7D 50 09 17 CD 10 55 97 B9 C1 A1 84 F2 A9 AB EA 7D F4 CC 54 E4 64 6E 3A E5 91 A0 06 00 03 01 BB 7B 08 98 A7 49 DE E8 E9 B8 91 52 EC 81 4C C2 39 7B F6 BA 1C 0A B1 95 50 29 BE 02 AD E6 AD 6E 11 3F 20 C4 66 F0 64 22 57 7E E1 06 7A 3B
Firefox拿这个数进行计算“C ≡ Me (mod n)”,得到的数是我们在客户密钥交换记录中所看到的
最后Firefox发出最后一条没加密的消息,一个“Change Cipher Spec”记录:
Firefox使用这种方式通知Amazon,它将使用前面协商好的密钥来加密下一条消息。
生成Master Secret
如果前面所有事情都正确完成,则双方(且只有这双方,客户端和服务端)现已知道这个48字节的pre-master secret。从Amazon的角度来看这里有一个很小的信任问题:pre- master secret只包含客户端生成的比特位,他们没有考虑到服务端或者我们前面所提到的那些随机数(译注:Client hello和Server hello过程中产生的随机数)。我们将通过计算“master secret”来解决这个问题。根据规约,应该这么计算:
master_secret = PRF(pre_master_secret, "master secret", ClientHello.random + ServerHello.random)
“pre_master_secret”是客户端之前发送的;“master secret”用的是一个字符串的ASCII值(例如:“6d 61 73 74 65 72 ...”)。然后我们将本文最开始看到的ClientHello和ServerHello发送的随机数拼接起来。
PRF是一个“伪随机数函数”,这个函数很聪明,在规约中也有定义。它使用基于哈希的消息验证码(HMAC)的MD5和SHA-1两种哈希函数将密钥,ASCII字符以及我们给的种子结合起来。对每个哈希函数发送一半的输入。说它聪明的原因是即使面对MD5和SHA-1的弱点,它的防攻击能力还很强。这个过程可以自我反馈并不停地循环,而且我们要多少字节就能生成多少。
依照这个过程,我们获得以下48字节的“master secret”:
4C AF 20 30 8F 4C AA C5 66 4A 02 90 F2 AC 10 00 39 DB 1D E0 1F CB E0 E0 9D D7 E6 BE 62 A4 6C 18 06 AD 79 21 DB 82 1D 53 84 DB 35 A7 1F C1 01 19
多个密钥的生成
现在双方都有了“master secrets”,规约描述了我们如何生成会话所需的所有的密钥,我们需要使用PRF函数来创建一个“key block”,然后从这个块中提取所需的密钥:
key_block = PRF(SecurityParameters.master_secret, "key expansion", SecurityParameters.server_random + SecurityParameters.client_random);
“key_block”被用来提取以下密钥:
client_write_MAC_secret[SecurityParameters.hash_size]
server_write_MAC_secret[SecurityParameters.hash_size]
client_write_key[SecurityParameters.key_material_length]
server_write_key[SecurityParameters.key_material_length]
client_write_IV[SecurityParameters.IV_size]
server_write_IV[SecurityParameters.IV_size]
由于这里使用的是序列密码而非分组密码(如高级加密标准AES),所以不需要初始向量(Initialization Vectors IV),而只是双方各需要一个16字节(128比特)的消息验证码(Message Authentication Code MAC),这是因为指定的MD5哈希摘要大小是16字节。另外,RC4加密算法使用的16字节的密码也是双方都需要的。最后,我们需要key block中的2*16 + 2*16 = 64个字节:
运行PRF,我们得到:
client_write_MAC_secret = 80 B8 F6 09 51 74 EA DB 29 28 EF 6F 9A B8 81 B0
server_write_MAC_secret = 67 7C 96 7B 70 C5 BC 62 9D 1D 1F 4A A6 79 81 61
client_write_key = 32 13 2C DD 1B 39 36 40 84 4A DE E5 6C 52 46 72
server_write_key = 58 36 C4 0D 8C 7C 74 DA 6D B7 34 0A 91 B6 8F A7
准备加密!
客户端发出的最后一条“握手”消息是“Finished message”。 这个消息非常巧妙,它不仅能证明没有人篡改了握手过程,还能证明我们确实知道密钥。客户端将所有的“握手”消息放入一个 “handshake_messages”缓存区,然后使用伪随机函数,“client finished”字符串以及MD5和SHA-1哈希运算后“handshake_messages”计算出12字节的“verify_data”:
verify_data = PRF(master_secret, "client finished", MD5(handshake_messages) + SHA-1(handshake_messages)) [12]
我们在这个结果的前面加上一个记录头字节“0x14”指明“完成”和长度字节“00 00 0c”指明我们将发送12字节的“verify data”。然后,像所有接下来的加密消息一样, 我们要确保解密后的内容没有被篡改。因为选择使用的密码套件是TLS_RSA_WITH_RC4_128_MD5,所以我们将使用MD5哈希函数。
有些人一听到MD5就感到恐慌,因为它存在一些弱点,我原先也很不提倡使用它。然而,TLS很聪明,他并不直接使用MD5,而使用它的HMAC版本。TLS是这样使用MDB进行计算的:
HMAC_MD5(Key, m) = MD5((Key ⊕ opad) ++ MD5((Key ⊕ ipad) ++ m)
(⊕指的是异或(XOR),++指的是拼接,“opad”是一串“5c 5c ... 5c”字节,“ipad”是另一串“36 36 ... 36”)。
这里我们对以下内容进行计算:
HMAC_MD5(client_write_MAC_secret, seq_num + TLSCompressed.type + TLSCompressed.version + TLSCompressed.length + TLSCompressed.fragment));
也许你已经看到,我们加入了一个序列号(“seq_num”)和明文消息(这里被称为TLSCompressed)的一些其他属性。序列号可以迷惑 攻击者,他可能会在中途把一个先前加密的消息插入。如果他这么干,则序列号一定和我们所期待的不一样,这就保护了我们不受攻击者们扔消息的攻击。
剩下就是加密消息了。
RC4加密算法
从前文已知双方协商的密码套件是TLS_RSA_WITH_RC4_128_MD5。这就意味着我们将使用Ron's Code #4 (RC4)对传输信息进行加密。Ron Rivest开发了基于256字节的密钥生成随机数的RC4加密算法。这个算法非常简单,以至于几分钟内你就可以记住它。
RC4从创建一个256字节的数组“S”开始,并从0到255对其进行填充。然后从“S”的第0位开始循环,将“S”和密钥中的字节进行混合,这样做是为了创建用于产生“随机”数的状态机。为了生成随机数,我们将“S”数组进行洗牌(译注:参考百度百科RC4)
图形化描述是这样的:
对一个字节进行加密,我们对伪随机字节和要加密的字节进行异或运算。记住将一个比特和1进行异或的话是使这个比特反转(译注:0^1=1, 1^1=0)。因为前面产生的是随机数,所以大约会有一半的比特码会被反转,这种随机的比特反转在加密数据时非常有效。你已经看到,这并不复杂,而且运行起来很快。我想这也许就是Amazon用它的原因吧。
回想一下,我们有“client_write_key”和“server_write_key”。这意味这我们需要创建两个RC4实例,一个用于加密浏览器发送的消息,另一个用于解密服务器返回的消息。.
“client_write”最前面的随机字节是“E 20 7A 4D FE FB 78 A7 33 ...”,如果我们用这些字节和未加密的消息头(经查证,该消息的字节为“4 00 00 0C 98 F0 AE CB C4 ...”)进行异或运算的话,我们将得到在Wireshark中看到的已加密的部分:
服务器做的事情几乎一样。它发出一条“Change Cipher Spec”消息,然后发出的“Finished Message”消息,这条消息包括所有的“握手”消息,以及解密的客户端发过来的“Finished Message”,这也向客户端表明了服务端可以正确解密客户端发过来的消息。
欢迎回到应用层!
现在,220毫秒过去了,我们最终为应用层准备好了,现在我们可以发送通过TLS层使用RC4的写实例加密过的普通HTTP消息,也可以解密服务端RC4写实例发过来的消息。此外,TLS层还会通过计算消息内容的HMAC_MD5哈希值来校验每一条消息是否被篡改。
至此,“握手”过程已经完成,现在TLS记录的content type变成23(0x17)。加密的数据流以“17 03 01”开始,这几个字节表明了记录类型和TLS版本,后面是包含了HMAC哈希的加密数据。
对如下明文的加密:
GET /gp/cart/view.html/ref=pd_luc_mri HTTP/1.1
Host: www.amazon.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.10) Gecko/2009060911 Minefield/3.0.10 (.NET CLR 3.5.30729)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
...
将得到下面这些字节:
剩下的唯一有趣的事是每个消息的序列号是依次递增的,现在是1(下一个消息将会是2,以此类推)。
服务端做的事情是一样的,不过它使用“server_write_key”这个密钥。我们看到它的响应消息,包括泄露秘密的应用数据头。
这个消息解密后得到:
HTTP/1.1 200 OK
Date: Wed, 10 Jun 2009 01:09:30 GMT
Server: Server
...
Cneonction: close
Transfer-Encoding: chunked
这是一个普通的HTTP返回消息,它包含一个非描述性“Server: Server”HTTP头和一个来自Amazon负载均衡器的“Cneonction: close”头,显然拼写有误。
TLS就在应用层之下。HTTP服务器上的软件可以就像发送非加密数据一样工作,而唯一的区别是它们(指软件)将数据写到一个库,由这个库统一做加密。OpenSSL是TLS的一个非常流行的开源库。
在双方发送和接受加密数据的过程中,链接一直保持活动状态,直到任何一端发出一条“closure alert”消息并接着关闭连接。如果在断开后的很短时间内重连,则可以重用前面协商好的密钥(前提是服务器还缓存着他们),这样就不需要公钥操作,否则,就需要重新执行一次完全的握手过程。
应用数据消息可以是任何信息,这点很重要。“HTTPS”特别的唯一原因是因为Web太流行了,还有其他基于TCP/IP的协议运行在TLS之上。例如,TLS也用于FTPS和SMTP的安全扩展。使用TLS肯定比你自己发明解决方案要好,另外,使用经得起仔细的安全分析的协议,你能从中获益。
……顺利完工!
TLS RFC中还包含很多我们这里遗漏的内容,它是一份可读性非常好的规范。我们覆盖的内容是对Firefox和Amazon之间的220毫秒的舞蹈进行观察所看到的一条路径。这个过程很大程度 上受Amazon在ServerHello消息中所选择的TLS_RSA_WITH_RC4_128_MD5密码套件的影响,这是一个合理的选择,因为它对速度的倾向比对安全的倾向更多一点。
如我们所看到的,如果有人能将Amazon的模数“n”分解成“p”,“q”,那么他就可以有效解密所有的“安全”流通,直到Amazon换掉它的证书。所以Amazon通过每年替换一次证书的方式来平衡这方面的担心。
其中有一个密码套件是“TLS_DHE_RSA_WITH_AES_256_CBC_SHA”,它使用Diffie-Hellman密钥交换算法,这个算法具有良好的 “正向安全”的特点。也就意味这如果某人破解了密码交换的数学原理,对于解密下一个会话也没有什么益处。这个算法的缺点是需要计算更大的数字,所以对于一个很忙的服务器来说计算成本更大。“高级加密标准”(AES)出现在我们提供的很多密码套件中。它与RC4 的不同在于它一次加密16字节的“数据块”而不是一个字节,由于它的密钥可以大到256比特,所以很多人认为它比RC4要更安全。
仅需220毫秒中,网络中两个端点连接到一起,提供了足够资料来信任对方,建立加密算法,并开始进行加密的信息传输。
我写了一个程序对文章中所提及的握手协议走了一遍。
关于作者
Jeff 是一个软件开发工程师,他在http://www.moserware.com/上有一个自己的博客。
注意:这篇文章最初发表在Moserware上面,也就是Jeff Moser的博客里。