HTTPS连接的前几毫秒

2020-10-21 18:14:59

在花了几个小时阅读狂热的评论后,鲍勃确信自己的托斯卡纳全脂牛奶和…加仑食品都是“去结账”

在飞逝的220毫秒内,发生了很多有趣的事情,让Firefox改变了地址栏的颜色,并在右下角设置了一个锁。在我最喜欢的网络工具Wireshark和略微修改的Firefox调试版本的帮助下,我们可以确切地看到正在发生的事情。

根据RFC2818的协议,Firefox知道“https”意味着它应该连接到Amazon.com的端口443:

大多数人将HTTPS与由Netscape在90年代中期创建的SSL(安全套接字层)联系在一起。随着时间的推移,这一点正变得越来越不正确。由于网景失去了市场份额,SSL的维护工作转移到了互联网工程任务组(IETF)。第一个后Netscape版本更名为传输层安全(TLS)1.0,于1999年1月发布。考虑到TLS已经存在10年了,很少能看到真正的“SSL”流量。

TLS将所有流量包装在不同类型的“记录”中。我们看到浏览器的第一个字节是十六进制字节0x16=22,这意味着这是一个“握手”记录:

接下来的两个字节是0x0301,表示这是版本3.1记录,这表明TLS 1.0实质上是SSL 3.1。

握手记录被分成几条消息。第一个是我们的“Client Hello”消息(0x01)。这里有几件重要的事情:

随机:有四个字节表示Unix纪元格式的当前协调世界时(UTC),这是自1970年1月1日以来的秒数。在本例中,为0x4a2f07ca。它后面跟着28个随机字节。这将在稍后使用。

会话ID:此处为空/NULL。如果我们在几秒钟前连接到Amazon.com,我们可能会恢复会话并避免完全握手。

密码套件:这是浏览器愿意支持的所有加密算法的列表。它的首选是“TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA”,紧随其后的是它愿意接受的33个其他选项。如果这些都说不通,也不用担心。我们稍后会发现,亚马逊并不是我们的首选。

SERVER_NAME扩展:这是一种告诉Amazon.com我们的浏览器正在尝试访问https://www.amazon.com/.的方式。这真的很方便,因为我们的TLS握手发生在任何HTTP流量之前很久。HTTP有一个“Host”报头,它允许节省成本的互联网托管公司将数百个网站堆积到一个IP地址上。SSL传统上要求每个站点使用不同的IP,但此扩展允许服务器使用浏览器正在查找的适当证书进行响应。如果没有其他问题,此扩展应该允许额外的一周左右的IPv4地址。

Amazon.com回复握手记录,这是一个巨大的两个数据包的大小(2551字节)。该记录的版本字节为0x0301,这意味着Amazon同意了我们使用TLS 1.0的请求。此记录有三个子消息,其中包含一些有趣的数据:

“Server Hello”消息(2):我们获得服务器的4个字节的时间Unix纪元时间表示及其稍后将使用的28个随机字节。

一个32字节的会话ID,以防我们希望在不进行大握手的情况下重新连接。

在我们提供的34个密码套件中,Amazon选择了“TLS_RSA_WITH_RC4_128_MD5”(0x0004)。这意味着它将使用“RSA”公钥算法来验证证书签名和交换密钥,使用RC4加密算法来加密数据,使用MD5散列函数来验证消息内容。我们稍后将深入讨论这些问题。我个人认为亚马逊选择这个密码套件是出于自私的原因。在榜单上,它是CPU使用最少的一个,这样Amazon就可以在他们的每台服务器上挤满更多的连接。一种不太可能的可能性是,他们想要特别向罗恩·里维斯特(Ron Rivest)致敬,他创造了所有这三种算法。

证书消息(11):此消息长达2464字节,是客户端可以用来验证Amazon证书的证书。它并不是什么花哨的东西。您可以在浏览器中查看其大部分内容:

“Server Hello Done”消息(14):这是一条零字节消息,它告诉客户端“Hello”进程已经完成,并指示服务器不会向客户端请求证书。

浏览器必须弄清楚自己是否应该信任亚马逊。在本例中,它使用的是证书。它查看了亚马逊的证书,发现当前时间介于2008年8月26日的“不早于”时间和2009年8月27日的“不早于”时间之间。它还检查以确保证书的公钥被授权用于交换密钥。

证书上附加了一个“签名”,它只是一个大端格式的非常长的数字:

任何人都可以把这些字节发给我们。我们为什么要相信这个签名呢?要回答这个问题,需要快速绕道进入数学王国:

人们有时会怀疑数学是否与编程有关。证书给出了一个非常实用的应用数学例子。亚马逊的证书告诉我们应该使用RSA算法来检查签名。RSA是由麻省理工学院教授Ron Rivest、Adi Shamir和Len Adleman在20世纪70年代创建的,他们发现了一种聪明的方法,将跨越2000年数学发展的想法结合在一起,得出了一个非常简单的算法:

你挑选两个巨大的质数“p”和“q”。将它们相乘得到“n=p*q”。接下来,您选择一个小的公共指数“e”,它是“加密指数”,而“e”的一个巧尽心思构建的倒数称为“d”,作为“解密指数”。然后将“n”和“e”公之于众,并尽可能将“d”保密,然后丢弃“p”和“q”(或者像“d”一样保密)。记住“e”和“d”是相反的,这一点非常重要。

现在,如果您有一些消息,您只需要将其字节解释为数字“M”。如果您想要“加密”一条消息以创建“密文”,您需要计算:

这意味着你将“M”乘以自身的“e”倍。“mod n”表示除以“n”时只取余数(例如“模”)。例如,上午11点+下午3小时≡2(下午12小时)。收件人知道“d”,这允许他们反转邮件以恢复原始邮件:

同样有趣的是,带“d”的人可以通过将消息“M”提升到“d”指数来“签署”文档:

这是因为“Siger”公开了“S”、“M”、“e”和“n”。任何人都可以通过简单的计算验证签名“S”:

像RSA这样的公钥密码算法通常被称为“非对称”算法,因为加密密钥(在我们的情况下是“e”)不等于(例如“对称”)解密密钥“d”。减少所有的“mod n”使得我们不可能使用我们习惯的简单技术,例如正常对数。Rsa的神奇之处在于您可以非常快速地计算/加密C≡Me(Modn),但是在不知道“d”的情况下很难计算/解密Cd≡M(Modn)。正如我们在前面看到的,“d”是通过将“n”分解回它的“p”和“q”而派生出来的,这是一个棘手的问题。

在现实世界中,RSA要记住的重要一点是,所有涉及的数字都必须很大,才能使用我们拥有的最好的算法使事情真正难以破解。多大?。Amazon.com的证书由“VeriSign Class 3安全服务器CA”“签名”。从证书中,我们可以看到这个VeriSign模数“n”是2048位长,它具有617位的基数-10表示法:

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”是216+1=65537。当然,他们对自己的“d”值保密,很可能是在一个由视网膜扫描仪和武装警卫保护的安全硬件设备上。在签名之前,VeriSign使用现实世界中的“握手”检查了Amazon.com在证书上声明的内容的有效性,其中包括查看他们的几个商业文档。一旦VeriSign对文档感到满意,他们就使用SHA-1散列算法来获得包含所有声明的证书的散列值。在Wireshark中,完整证书显示为“signedCertificate”部分:

这有点用词不当,因为它实际上意味着那些是签名者要签名的字节,而不是已经包含签名的字节。

实际的签名“S”在Wireshark中简称为“加密”。如果我们将“S”提高到VeriSign的公共“e”指数65537,然后取除以模数“n”后的余数,我们得到这个“解密”的签名十六进制值:

0001FFFFFFFFFFFFFFFFFFFFF00302130 0906052B0E03021A 05000414C19F8786 871775C60EFE0542 E4C2167C830539DB。

根据PKCS#1v1.5标准,第一个字节是“00”,它“确保[当]转换为整数时,加密块小于模数”。“01”的第二个字节表示这是一个私钥操作(例如,它是一个签名)。紧跟其后的是许多用于填充结果的“FF”字节,以确保结果足够大。填充以“00”字节结束。后面跟着“3021300906052B0E0302A0500414”,这是PKCS#1v2.1指定SHA-1散列算法的方法。最后20个字节是“signedCertificate”中字节的SHA-1散列摘要。

由于解密值的格式正确,并且最后一个字节与我们可以独立计算的散列值相同,因此我们可以假定知道“VeriSign Class 3 Secure Server CA”私钥的任何人都对其进行了“签名”。我们隐含地相信只有VeriSign知道私钥“d”。

我们可以重复该过程来验证“VeriSign Class 3 Secure Server CA”的证书是否由VeriSign的“Class 3 Public Primary Certification Authority”签署。

但我们为什么要相信这一点呢?信任链上没有更多的级别。

顶级“VeriSign 3类公共初级证书颁发机构”亲笔签名。自网络安全服务(NSS)库中的certdata.txt 1.4版开始,此证书已作为隐式信任的良好证书内置于Mozilla产品中。它于2000年9月6日由网景公司的罗伯特·雷耶亚登记入住,并发表了以下评论:

“让框架与NSS的其余部分一起编译。将‘live’certdata.txt包含在我们有权推送到开源的证书中(当我们从所有者那里获得许可时,将添加额外的证书)。“

由于证书的有效期为1996年1月28日至2028年8月1日,这一决定产生了相对较长的影响。

正如肯·汤普森(Ken Thompson)在他的“关于信任的思考”(Reflations On Trust Trust)中很好地解释的那样,你最终必须隐含地信任某人。这个问题是无法绕过的。在这种情况下,我们完全相信Robert Relyea做出了正确的选择。我们还希望Mozilla的内置证书策略对其他内置证书是合理的。

这里要记住的一件事是,所有这些证书和签名只是用来形成一个信任链。在公共互联网上,在你访问任何网站之前很久,火狐就已经完全信任VeriSign的根证书了。在公司中,您可以创建自己的根证书颁发机构(CA),可以将其安装在每个人的计算机上。

或者,您也可以避免向VeriSign这样的公司支付费用,并完全避免证书信任链。证书用于通过使用受信任的第三方(在本例中为VeriSign)建立信任。如果您拥有共享秘密“密钥”的安全方法,例如在某人耳边窃窃私语一个长密码,那么您可以使用预共享密钥(PSK)来建立信任。TLS有一些扩展可以实现这一点,比如TLS-PSK,还有我个人最喜欢的带有安全远程密码(SRP)扩展的TLS。不幸的是,这些扩展几乎没有得到广泛部署和支持,因此它们通常不实用。此外,这些替代方案给我们带来了负担,我们必须有一些其他安全的方式来传达秘密,这比我们试图通过TLS建立的秘密更麻烦(否则,我们为什么不使用它来处理所有事情呢?)。

我们需要执行的最后一项检查是验证证书上的主机名是否符合我们的预期。Nelson Bolyard在SSL_AuthCertificate函数中的注释解释了原因:

/*证书可以。这是SSL连接的客户端。*现在对照所需的主机名检查证书中的名称字段。*注意:这是我们对中间人(MITM)攻击的唯一防御!*/。

此检查有助于防止中间人攻击,因为我们隐含地相信证书信任链上的人不会做坏事,例如签署声称来自Amazon.com的证书,除非它实际上是Amazon.com。如果攻击者能够使用DNS缓存毒化等技术修改您的DNS服务器,您可能会被误认为是在受信任的站点(如Amazon.com),因为地址栏看起来很正常。最后一次检查隐含地信任证书颁发机构来阻止这些不好的事情发生。

我们已经核实了一些关于Amazon.com的声明,并知道它的公开加密指数“e”和模数“n”。任何监听流量的人都可以知道这一点(这一点很明显,因为我们使用的是Wireshark捕获)。现在,我们需要创建一个窃听者/攻击者无法破解的随机密钥。这并不像听起来那么容易。在1996年,研究人员发现Netscape Navigator 1.1只使用了三个源来作为伪随机数生成器(PRNG)的种子。来源是:一天中的时间、进程ID和父进程ID。正如研究人员所展示的那样,这些“随机”来源并不是那么随机,而且相对容易计算出来。

由于其他一切都来自这三个“随机”来源,因此在1996年代的机器上,可以在25秒内“破解”SSL“安全性”。如果您仍然不相信找到随机性是困难的,那就问问Debian OpenSSL维护者。如果你搞砸了,所有建在上面的安全措施都是可疑的。

在Windows上,用于加密目的的随机数是通过调用CryptGenRandom函数生成的,该函数对从超过125个来源采样的位进行散列。Firefox使用此函数以及从它自己的函数派生的一些位来为其伪随机数生成器设定种子。

生成的48字节“预主密码”随机值不会直接使用,但保密是非常重要的,因为很多东西都是从它派生出来的。毫不奇怪,Firefox使人们很难找到这个值。我必须编译调试版本并设置SSLDEBUGFILE和SSLTRACE环境变量才能看到它。

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)....。公元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。按照亚马逊“TLS_RSA_WITH_RC4_128_MD5”的愿望,我们将使用RSA来实现这一点。您可以使您的输入消息仅等于48字节的预主秘密,但是公钥加密标准(PKCS)#1,版本1.5RFC告诉我们,我们应该用随机数据填充这些字节,以使输入完全等于模数的大小(1024位/128字节)。这使得攻击者更难确定我们的预主密码。这也给了我们最后一次保护自己的机会,以防我们做了一些非常愚蠢的事情,比如重复使用同样的秘密。如果我们重用密钥,由于随机填充,窃听者很可能会看到放置在网络上的不同值。

同样,Firefox使您很难看到这些随机值。我必须在填充函数中插入调试语句才能看到正在发生的事情:

WrapperHandle=fopen(";Platextpadding.txt";,";a";);fprintf(wrapperHandle,";明文=";);for(i=0;i<;modusLen;i++){fprintf(wrapperHandle,";%02X";,block[i]);}fprintf(wrapperHandle,";\r\n&34;);fclose(WrapperHandle);

00 02 12 A3 EA B1 65 D6 81 6C 13 14 13 62 10 53 23 B3 96 85 FF 24FA 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 BB 7B 08 98 A7 49 DE E8 E9 B8 91 52 EC 81 4C C2 39 7B F6 BA 1B 0B1 95 50 29 BE AD 02 E6 AD 6E 11 3F 20 C4 66 C4 F0 57 7E E1 7B 3B。

Firefox采用此值并计算“C≡Me(Mod N)”,以获得我们在“客户端密钥交换”记录中看到的值:

最后,Firefox发出了最后一条未加密的消息,即“更改密码规范”记录:

这是火狐告诉亚马逊,它将开始使用约定的秘密来加密其下一条消息的方式。

如果我们做的一切都是正确的,那么两端(并且只有那些端)现在都知道48字节(256位)的预主密码。从Amazon的角度来看,这里有一个轻微的信任问题:预主密钥只有客户端生成的位,它们不考虑来自服务器的任何内容或我们前面说过的任何内容。我们会通过计算“主秘密”来解决这个问题。根据规范,这是通过计算完成的:

“PRE_MASTER_SECRET”是我们之前发送的秘密值。“主密钥”只是一个字符串,它的ASCII字节(例如“6d 61 73 74 65 72…”)。都被利用了。然后,我们将开始时看到的ClientHello和ServerHello(来自Amazon)消息中发送的随机值连接起来。

PRF是规范中也定义的“伪随机函数”,非常聪明。它使用MD5和SHA-1散列函数的密钥散列消息验证码(HMAC)版本将秘密、ASCII标签和我们提供的种子数据组合在一起。一半的输入被发送到每个散列函数。它很聪明,因为它很能抵抗攻击,即使面对MD5和SHA-1的弱点也是如此。这个过程可以自我反馈并不断迭代,以生成我们需要的任意数量的字节。

4C AF 20 30 8F 4C AA C5 66 4A 02 90 F2 AC 10 00 39 DB 1D E01F CB E0 E09D D7 E6 BE 62 A4 6C 18 06 AD 79 21 DB 82 1D 53 84 DB 35 A7 1F C1 01 19

现在两端都有了“主密钥”,规范向我们展示了如何使用PRF来创建一个“密钥块”来获取我们需要的所有会话密钥,我们将从这个“密钥块”中提取数据:

因为我们用的是STREAM。

.