【闲聊杂谈】HTTPS原理详解

HTTPS和HTTP的区别

HTTP虽然使用极为广泛, 但是却存在不小的安全缺陷, 主要是其数据的明文传送和消息完整性检测的缺乏, 而这两点恰好是网络支付, 网络交易等新兴应用中安全方面最需要关注的。

关于 HTTP的明文数据传输, 攻击者最常用的攻击手法就是网络嗅探, 试图从传输过程当中分析出敏感的数据, 例如管理员对 Web 程序后台的登录过程等等, 从而获取网站管理权限, 进而渗透到整个服务器的权限。即使无法获取到后台登录信息, 攻击者也可以从网络中获取普通用户的隐秘信息, 包括手机号码, 身份证号码, 信用卡号等重要资料, 导致严重的安全事故。进行网络嗅探攻击非常简单, 对攻击者的要求很低。使用网络发布的任意一款抓包工具, 一个新手就有可能获取到大型网站的用户信息。

HTTPS可以简单理解成安全的HTTP通道,基于原先的HTTP协议实现,S指的就是在会话层加上SSL证书(也有说是TLS证书,TLS→Transport Layer Security是 SSL→Secure Socket Layer的后续版本)。OSI的七层协议其实只是国际标准理论上的协议,它也就是个标准。而普遍实现的是采用五层协议,就是把七层中的应用层、表示层、会话层合为一层应用层,那么如此一来,这个SSL就是在五层协议中的应用层。而还有一种是TCP/IP四层协议,是美国官方很早期实现的模型,主要是将物理层和数据链路层合为一层网络接口层。

而SSL和TLS又有什么不同呢,早期SSL是网景Netscape公司于1990年推出的私有协议,当时这项协议仅仅存在于网景自身研发的浏览器中。网景浏览器在90年代相当火爆,市场占有率高达90%,后来跟微软打架打得热火朝天,微软基于操作系统的优势,网景市场占有率一步步被蚕食,最后只能被美国在线收购,最后于2003年7月15日解散。不过SSL这项协议是被保留了下来,单单一个SSL协议,广泛用于当今的互联网中,网景公司便功不可没,毕竟能将公司内部私有协议做成RFC标准的公司,技术水平绝对不会差。

网景战败之后,SSL这些协议便没人再去维护,随着版本的发展,后来交给了国际标准协议的一个工作组。到了1999年,SSL协议在互联网中应用特别广泛,已经成为事实上的互联网标准。于是,在1999年IETF组织将SSL3.0协议规范进行了标准化,便成了TLS协议(Transport Layer Secure)。不过由于SSL3.0和TLS之间存在加密算法上的差异,因此不能互相操作,算是两个不同的协议。

那么HTTPS和HTTP之间最最主要的区别就是:安全

HTTPS通过加密传输、身份认证、保证完整性、不可否认/抵赖机制来保证数据传输的安全性。

加密传输:整个数据的传输都是经过加密,就算传输中被第三方拦截,也无法识别传输的内容。加密的实现原理通过非对称加密+对称加密共同实现,也就是所谓的混合加密模式;

身份认证:服务器端和客户端都能够确认对方的身份,彼此能够证明数据的发送和接受都是可信的。如果没有身份认证机制,黑客可以通过模拟服务端来接受客户端发送来的数据,也可以通过模拟客户端来盗取服务器上的数据。好比两个特务接头,需要使用双方都认可的信物来相互确认身份,这个所谓的信物可以是双方都认可的第三方权威机构颁发的证书,通过证书保证双方没有被身份冒充;

保证完整性:虽然传输的内容是加密的,但是不代表不可以去修改传输内容,这样即使不知道传输的数据是什么,但是接受的一方收到的也是错误的数据。发送端给要发送的数据生成一个摘要,当接收端拿到数据之后,将摘要和原文按照特定的算法进行比对。比对的结果一致则说明数据没有问题,若摘要或原文任意一方有错误比对不成功,说明数据在传输过程中肯定有被篡改。接收端会将错误的数据丢弃,并告知发送端重新发送;

不可否认/抵赖机制:客户端和服务端的每一次交互在经过身份认证之后,生成每一次操作的记录。一旦操作发生了错误,则可以根据记录追本溯源,由于经过身份认证,每一方的每一步操作都会被记录下来不可抵赖;

除了最重要的安全区别之外,HTTPS和HTTP之间也会有一些其它的区别:

由于HTTPS协议中为了安全至上,融入了很多的加解密操作。加解密本身也是一种CPU密集型的操作,是一种非常耗费CPU资源的操作,所以纯粹论速度的话,肯定较HTTP要慢一些,整体的灵活度也会低一些。 当然,还有一个很现实的问题,HTTPS协议是需要收费的。在身份认证上需要拿到第三方机构的证书,当然这个第三方机构有很多,统称为CA机构。

HTTPS的工作流程

在说https的工作流程之前,先简单回顾一下TCP的工作流程。TPC的流程无外乎就是所谓的三次握手和四次分手。在三次请求的过程中,客户端先发送一个没有实际意义的数据报文给服务端。服务端拿到报文之后,也给客户端返回一个报文作为响应。客户端拿到服务端的数据之后,再次给服务端回复一个确认的报文,然后双方确认信息一致,建立连接。

TCP连接建立完成之后,接下来才是真正的开始HTTPS连接的建立,也就是TLS协议的握手建立。这个握手的过程比TCP握手要复杂很多,有的说4次,有的说7次,有的说12次,这个具体是看怎么样去划分。大致的流程为:

1、客户端发起请求;
2、服务器返回公钥证书;
3、客户端验证证书;
4、客户端生成对称秘钥,用公钥加密后发给服务端;
5、服务端使用私钥解密,得到对称秘钥;
6、客户端和服务端双方使用对称秘钥来加解密;

其实整个握手的过程就干了一件事,就是将客户端生成的对称秘钥安全地交付给服务端。图中已经是将复杂的操作简化再简化后总结的大概流程,实际上落地的操作流程远远比描述得要复杂很多。

我们都知道,HTTPS的加密模式采用的是混合加密方式,即用对称加密,也用非对称加密。刚刚说到的非对称加密的时候就是秘钥的交互,而对称加密就是使用在后期数据的传输中。

混合加密完成之后,客户端和服务端双方都持有一个相同的对称秘钥。前面说了HTTPS的实现基于TLS协议,而目前TLS主流的有1.2版本和1.3版本。

基于 TLS 1.2 的握手流程

第 1 次(ClientHello):

客户端向服务端发起请求,传递的参数包含:TLS版本号、一个随机数Random C、密码套件CipherSuites。所谓密码套件指的就是,客户端和服务端要进行数据的加密,生成所谓的秘钥。生成秘钥就需要经过一系列的算法来生成。于是客户端在第一次向服务端发送请求的时候就会告诉服务端我这里支持哪些算法,服务端就会使用客户端所支持的算来进行加密计算。说白了,这一次的握手请求其实就是将客户端自身的基础信息一股脑全部告诉服务端,好让服务端来因材施教;

第 2 次(ServerHello):

当服务端接受到客户端的请求之后,也会给客户端回复一个消息。服务端回复的内容就比较多了,包含:TLS版本号、一个随机数Random S、密码套件CipherSuites(1.2版本使用的主流算法是ECDHE)、服务器证书Certificate(用于证明服务端的身份,告诉客户端我值得信赖且权威)、ServerKeyExchange(包含加密算法所需要的参数)。这样便完成了两端随机数的交换,还确定了具体使用哪个加密算法;

第 3 次(ClientKeyExchange):

当客户端接受到服务端返回的一大堆信息之后,对接受到的信息进行检验。根据服务端给定的参数生成对称秘钥,然后将这个对称秘钥加密之后再发送给服务端;

第 4 次(ChangeCipherSpec):

服务端收到秘钥之后,告诉客户端秘钥接受成。

经过这4步之后,客户端和服务端后续数据的传输都使用对称秘钥进行加解密。其实以上的4次握手是整合了很多细碎步骤之后总结的大概流程,如果将这些流程细化下来,差不多可以拆为10来个步骤。譬如:第 2 步的ServerHello,其实在这一步服务端是分为很多个小步骤向客户端发送数据,A其中包含ACK确认是一步、TLS版本和随机数是一步、服务端证书是一步、密码套件是一步.....等等。这些细分的小步骤并没有明确的前后顺序之分,而且发送的顺序和到达的顺序也会因为网络波动存在颠倒。

第 3 步ClientKeyExchange会稍微复杂一些,实际在生成对称秘钥的时候,客户端首先先验证服务端发送来的证书和签名,验证完之后客户端会生成一个新的随机数,这个新的随机数就是客户端参数,然后将这个客户端参数发送给服务端。然后客户端和服务端双方都使用双方生成的随机数作为参数,经过ECDHE算法生成PreMaster。这个PreMaster是双方自己算出来的,中间没有经过任何的数据传输,所以不会被任何拦截非法获取。然后再使用PreMaster加上两个随机数生成最终的主密钥,也就是后续真正拿来加密传输数据的对称秘钥。此时双方都已经生成了主密钥,客户端会执行Change Cipher Spec,也就是告诉服务端改变密码规范。之前咱俩都是使用非对称加密,现在都有对称秘钥了,接下来咱俩都该使用对称加密了。

最后服务端收到之后再给客户端一个确认,这就是整个 TLS 1.2 的握手流程,主要的难点就是第 3 步主密钥的生成,真正传递的其实是核心参数,而不是主密钥本身。

基于 TLS 1.3 的握手流程

1.3 版本的诞生主要是为了解决 1.2 版本存在的一些问题,低版本存在哪些问题呢,基本上就是围绕安全性、性能、兼容性这 3 个维度铺设开来。1.2 版本好像是 08 年推出的,如今的互联网行业技术日新月异,低版本算法的自然也逐渐跟不上新时代的要求。所以在 1.3 版本中砍掉了很多落后的算法, 也优化了很多主流的算法。

而最主要的是针对 1.2 的性能问题进行大幅度改善。在1.2版本中,每一次的握手双方都会进行多次的数据传输,无疑会造成效率低下。每一次的一个数据传输往返叫一个往返时延(RTT),两次往返就需要两倍的 RTT 时间,多次往返就需要多次的 RTT 时间。想要优化这个问题,就得减少数据发送的次数。1.2 版本先是生成一个随机数发送给服务端,服务端拿到这个随机数之后再把服务端的一堆数据一股脑给到客户端,然后客户端在拿着服务端返回的数据进行第 2 次生成随机数。其实第 2 次随机数的生成真的有必要非依赖服务端的数据不可吗?反正结果都是生成随机数,有没有服务端的数据都是生成“随机”数,那直接在一开始的时候将两个随机数都生成好不是更省事?

于是在 1.3 版本中,第 1 次握手的时候客户端将自己所有的数据全部发送给服务端,这样服务端直击可以进行后面秘钥的生成。服务端也是一次性把自己该干的事全部干完,然后一股脑全给客户端,客户端拿到数据之后,也是自己进行一系列后续操作。然后在第 3 次握手的时候顺带发送点数据给服务端,告诉服务端该改变加密模式了。

还有一些有意思的点比如双方都有一个 key_share 字段,key 意思是秘钥,share 意思是共享:可以共享的秘钥,那么显然值得就是公钥。注意看,在第 1 步 ClientHello 的时候,客户端就已经将自身生成的一对秘钥中的公钥发送给了服务端。服务端在拿到公钥之后用公钥加密完数据,再将对应的数据返回给客户端。1.2 版本是服务端给客户端公钥,1.3 版本直接变成了客户端给服务端秘钥。那问题来了,客户端直接把所有的数据加密好传给服务端,可是双方好像都还没有确定好用什么算法来加解密,客户端凭什么觉得服务端就一定能成功解密它发送的数据呢?其实这就是在赌,毕竟目前主流的加密算法并不多,双方默认对方都是支持主流的算法,所以就不用在浪费步骤在啰嗦使用什么算法上,这样也大大提高了双方数据交换的效率。

HTTPS实现原理

机密性

所谓机密性指的就是非对称加密和对称加密都使用上,也就是常说的混合加密。两种加密方式也是分阶段使用:在秘钥交换的阶段使用的是非对称加密,在后续数据传输阶段使用的是对称加密。整体的流程,多次的握手,就是通信双方协商出一个用于对称加密的秘钥。

前面提到的 ECDHE 算法是 ECC 算法的一个子算法,而 RSA 算法其实是有一些安全隐患存在。1.2 版本所谓的安全隐患其实指的就是这个 RSA 算法的隐患。由于 RSA 算法刚刚推出的时候收到热烈追捧,所以 RSA 算法的知名度很高,但是在 1.3 版本中已经被强制淘汰了。RSA 的原理就是使用两个超大质数的乘积,作为生成秘钥的参数。如果黑客拿到大量使用 RSA 算法加密后的数据,理论上是可以反向推出原文的。但实际上是不可能的事情,因为HTTP协议是一种无状态的协议,所以每一次客户端和服务端的连接都是将所有的步骤从来一遍,也就是说每次的秘钥可以理解为是一次性的,就算反推出来也没有意义。所以所谓的隐患也仅仅只是隐患,实际落地几乎不可行。但是 ECC 算法无论是安全性还是效率上都比 RSA 要高效的多,所以有了更好的选择,RSA 也算是光荣完成使命。

关于对称加密和非对称加密,在这里写个简单的小程序说明一下。我们知道,对称加密就是加解密都使用相同的秘钥,那么异或操作绝对是当仁不让的典型代表:

public class SymmetricEncryption {

    private static final byte SYMMETRIC_KEY = 10;

    /**
     * 加解密同方法,原理很简单:
     * 一个数对另外一个数进行两次 ^ 运算,得到其本身
     */
    public static void encryptOrDecrypt(String from, String to) throws IOException {
        FileInputStream inputStream = new FileInputStream(new File(from));
        byte[] datas = inputStream.readAllBytes();

        for (int i = 0; i < datas.length; i++) {
            datas[i] = (byte) (datas[i] ^ SYMMETRIC_KEY);
        }

        FileOutputStream outputStream = new FileOutputStream(new File(to));
        outputStream.write(datas);

        outputStream.close();
        inputStream.close();
    }

    public static void main(String[] args) throws IOException {
        String source = "image/dzq.jpeg";
        String encryptPath = "image/symm-dzq.jpeg";
        String decryptPath = "image/dec-dzq.jpeg";

        // 加密
        encryptOrDecrypt(source, encryptPath);

        // 解密
        encryptOrDecrypt(encryptPath, decryptPath);
    }


}

一个数对另外一个数进行两次异或运算,得到其本身。这里使用一张图片进行加解密,原图片:

加密后的图片:

解密后的图片:

这就是使用了同一把秘钥进行对称加密,解密后的图片和加密前的图片完全一致。 下面再来看看使用非对称加密的效果。其实非对称加密就是加解密使用不同的秘钥,只要秘钥不同就可以。加密算法一般不追求保密性,而是追求完备性,也就是攻击者不知道秘钥就无法从算法找到突破口。优点是没有私钥无法解密。缺点是公钥是公开的,可能会被截获;公钥不包含服务器信息,无法验证服务器身份,存在中间人攻击风险;而且效率上会比对称加密低一些。

public class AsymmetricEncryption {

    private static final int PUBLIC_KEY = 1;
    private static final int PRIVATE_KEY = -1;

    public static void encryptOrDecrypt(int key, String from, String to) throws IOException {
        FileInputStream inputStream = new FileInputStream(new File(from));
        byte[] datas = inputStream.readAllBytes();

        for (int i = 0; i < datas.length; i++) {
            datas[i] = (byte) (datas[i] + key);
        }

        FileOutputStream outputStream = new FileOutputStream(new File(to));
        outputStream.write(datas);

        outputStream.close();
        inputStream.close();
    }

    public static void main(String[] args) throws IOException {
        String source = "image/dzq.jpeg";
        String encryptPath = "image/symm-dzq-asy.jpeg";
        String decryptPath = "image/dec-dzq-asy.jpeg";

        // 加密
        encryptOrDecrypt(PUBLIC_KEY, source, encryptPath);

        // 解密
        encryptOrDecrypt(PRIVATE_KEY, encryptPath, decryptPath);
    }


}

还是同样的一张图片进行加解密,原图片:

加密后的图片:

解密后的图片:

完整性

实现了机密性之后,加密数据即使被拦截也无所谓,反正拦截者看不懂数据内容。但是本着得不到就毁掉的思想,虽然看不懂内容,但是可以对数据打乱、去头掐尾、中间随意增加内容.....让接受者也看不懂处理后的数据,这种数据就损失了原先的完整性。当服务端接受到不完整数据之后,是无法将其还原回去,只能通过让客户端重新传一份完整的数据过来,通过重传机制保证数据的完整性。

重传机制不在今天讨论的范围之内,这里主要想讨论的是服务端如何去验证数据的完整性。就是客户端在发送数据的时候,同时会发送一个与原文等价的数字摘要。服务端可以通过原文计算出这个摘要,无论是原文还是摘要被修改后,通过原文计算出的摘要和传递过来的摘要必然无法一致,服务端就可以知道数据是否被中途修改过。

摘要的算法其实就是通过一种压缩算法(Hash函数)计算出的一个字符串,因为这种算法可以将任意长度的值算出固定长度的值,所以这种定长的字符串也叫数据指纹。这种单向的加密算法无法解密,所以不可通过摘要逆推出原文,只能双方都通过相同的算法计算出摘要比较是否一致。

身份认证与不可否认

身份认证其实就是客户端和服务端相互证明自己是个正经的终端。黑客很容易模拟出一个假网站,在不知不觉中就将重要的信息提交到了假网站上从而上当受骗。于是要求客户端和服务端双方都要使用数字签名来防止伪装。

如何去理解这个数字签名,这其实和生活中我们经常使用签名来证明本人的身份是一样的性质。当然,签名也经常会被仿冒,于是便将签名、颁发者、证明者、有效时间等一系列的信息全部封装到一个数字证书中。而这个证书并不是自身自生成,而是由权威的第三方可信机构来颁发。这个证书就好比是我们的身份证,而颁发证书的机构就相当于发行身份证的国家。

黑客伪造一个网站容易,但是伪造的网站想要取得可信的证书却很难。有证书说明这个客户端或者服务端在官方有进行备案,是绝对可信的存在。而不可否认其实就是当一方有了证书之后,所进行的每一步的操作都是会被记录下来不可抵赖。当某一方干些坏事之后,可以根据记录来追本溯源,找到操作的源头。

颁发证书的机构叫CA,这些CA的权威性完全取决你的信任程度:只要你认为它是可信的,那么它就是值得信任的。所以,如果对市面上所有的CA都不信任的时候,你可以选择自己给自己颁发证书。这种操作较为常见的是各大银行,生活中去银行办网银的时候,银行多半会给你一个U盾之类的东西,这个U盾就可以理解为银行自己的证书,进行一些重大交易的时候,必须要有这个U盾确保安全之后才可以进行交易。

证书的等级目前分为 3 个等级:

DV证书:域名验证型证书,证书审核方式为通过验证域名所有权即可签发证书。此类型证书适合个人和小微企业申请,价格较低,申请快捷,但是证书中无法显示企业信息,安全性较差。如果是部署在web网站上,浏览器中显示锁型标志。

OV证书:企业验证型证书,证书审核方式为通过验证域名所有权和申请企业的真实身份信息才能签发证书。目前OV类型证书是全球运用最广,兼容性最好的证书类型。此证书类型适合中型企业和互联网业务申请。如果是部署在web网站上,浏览器中显示锁型标志,并能通过点击查看到企业相关信息。支持ECC高安全强度加密算法,加密数据更加安全,加密性能更高。

EV证书:增强验证型证书,证书审核级别为所有类型最严格验证方式,在OV类型的验证基础上额外验证其他企业的相关信息,比如银行开户许可证书。EV类型证书多使用于银行、金融、证券、支付等高安全标准行业。如果是部署在web网站上,其在地址栏可以显示独特的EV绿色标识地址栏,最大程度的标识出网站的可信级别。支持ECC高安全强度加密算法,加密数据更加安全,加密性能更高。

其实在日常的工作中绝大部分时候是不需要过深地去研究 https 的,一般来说小微企业会选择直接购买大厂的服务器进行使用,譬如阿里云、腾讯云等的云服务器是支持提供 https 协议,直接花钱去一个信得过的CA机构买一个证书配置上去即可。至于具体的那些验证流程、生成过程等等作为开发人员来说其实无需过多关心,更不需要关心所谓的自建CA,大多数时候只需要会在Nginx中配置即可。

但是在我这里,怎么可能只满足简单的配置就完事了, 本着要挖就挖到底的精神,咱们手把手的将整个 https 的生成全过程一步一个脚印给它走完。

安装 OpenSSL 工具

在开始之前,需要将必要的准备工作完成,就是安装好必要的OpenSSL工具:
win - http://slproweb.com/products/Win32OpenSSL.html
Linux - https://www.openssl.org/source/

这个网站全是英文,看得懂看不懂不重要,往下翻找到下载的表格

这个表格上半部分是 3+ 的高版本,下半部分是 1+ 的低版本。其实这个低版本其实也是长期支持的版本,而且它的安全性、稳定性、兼容性都已经做的非常完善。相反高版本的兼容性可能就不如低版本,当然官方肯定说尽可能的去保证高版本的兼容性,反正你自己看着来。

安装完成之后打开安装的目录瞅一眼文件夹里面,包含生成证书的工具和各类加密算法。接下里会在win系统中生成服务端证书,然后再把相关文件上传到 Linux 服务器。一般来说会生成 3 个文件,分别是 key(私钥)、csr(证书签名请求文件,即待签名证书,也可以当成公钥来使用)、crt(证书)。

win系统下生成证书

来到 OpenSSL 安装目录中的 bin 目录中,打开 cmd 窗口,执行命令:
openssl genrsa -des3 -out g:/my_cert/server.key

要求输入生成私钥的秘钥,第二行是进行密码确认,会看到在指定的目录下生成了一个私钥:

这玩意儿打开就是一个被加密过的大长字符串:

有了私钥之后下一步就是要生成待签名的证书,还是在同样的 cmd 窗口中执行:
openssl req -new -key g:/my_cert/server.key -out g:/my_cert/pub.csr

这里要求输入的密码就是刚刚生成私钥的密码

后面输入的这些就是封装在证书中的一些信息,生成的时候要求提供:

- Country Name (2 letter code) [XX]:CN  # 请求签署人的信息,CN代表中国
- State or Province Name (full name) [Some-State]:Fujian  # 请求签署人的省份名字
- Locality Name (eg, city) []:Xiamen  # 请求签署人的城市名字
- Organization Name (eg, company) [Internet Widgits Pty Ltd]:Feenix  #请求签署人的公司名字
- Organizational Unit Name (eg, section) []:Laboratory  #请求签署人的部门名字
- Common Name (e.g. server FQDN or YOUR name) []:www.feenix.com  #这里一般填写请求人的服务器域名 
- Email Address []:[email protected]  # 请求人的邮件地址

至于最后的两个:
A challenge password []:  # 一个富有挑战性的密码
An optional company name []:  # 一个备选公司的名称

可写可不写吧,反正是自己玩,我是懒得写了,会看到在指定的目录下生成了待签名证书:

同样的,这玩意儿打开也是一个被加密过的大长字符串:

有了私钥和待签名证书之后,最后一步就是需要权威的CA机构给待签名证书进行签名。不过刚刚生成待签名证书填的那些乱七八糟的信息,估计也没哪个正经的CA机构能验证通过,所以求人不如求己,我们自己自建一个CA机构来给待签名证书盖章签字。

自建CA

接下来还是用同一套逻辑去生成 CA 机构,所谓 CA 机构的本质,其实也是一套证书,只不过这套证书具有公信力,可以用来给别人签名。

我们用的操作系统(Win、Linux、Unix、Android、iOS.....)都预置了很多信任的根证书,比如我的 windows 中就包含 VeriSign 的根证书,那么浏览器访问服务器,譬如支付宝 www.alipay.com 时,SSL 协议握手时服务器就会把它的服务器证书发给用户浏览器,而这本服务器证书又是比如VeriSign 颁发的,自然就验证通过了。

还是同样的 cmd 窗口,创建 CA 私钥 命令:
openssl genrsa -out g:/my_cert/myca.key 2048

使用 CA 私钥生成 CA 待签名证书 命令:
openssl.exe req -new -key g:/my_cert/myca.key -out g:/my_cert/myca.csr

和刚刚生成待签名的证书一样,该走的流程一步别想少再来一遍。有了 CA 私钥和 CA 待签名证书之之后,就可以生成 CA 根证书 命令:
openssl.exe x509 -req -in g:/my_cert/myca.csr -extensions v3_ca -signkey g:/my_cert/myca.key -out g:/my_cert/myca.crt

CA 机构的证书由于是给别人进行认证签字的,所以它有一个比较牛批的名字:根证书。这玩意生成之后,连 icon 都和别的证书不一样:

到这一步,就已经生成完了 CA 机构,就是磁盘上的三个文件:myca.key、myca.csr、myca.crt。所以 CA 机构并不是某个实体,只不过是一套加密包含了实体各种信息的数字证书。

现在终于可以使用 CA 机构对之前生成的待签名证书进行签名 命令:
openssl x509 -days 365 -req -in g:/my_cert/pub.csr -extensions v3_req -CAkey g:/my_cert/myca.key -CA g:/my_cert/myca.crt -CAcreateserial -out g:/my_cert/server.crt

-days 365                                有效期365天
-in g:/my_cert/pub.csr             待签名证书
-CAkey g:/my_cert/myca.key  使用CA机构私钥
-CA g:/my_cert/myca.crt         使用CA机构证书
-out g:/my_cert/server.crt        生成签名后的证书

CA 机构给待签名证书签名完之后,CA 机构也没啥用了。到此为止,可以说 https 证书的生成已经全部完成,下面只要将证书在Nginx上配置好即可。而在事迹生产环境中,也基本上只需要配置证书,根本不需要自己去生成证书。

HTTPS Nginx 配置与安装

没有配置证书前只能通过 http 进行访问,https 是无法成功访问到 nginx 欢迎页

将前面生成的 server.key 和 server.crt 两个文件扔到服务器上,然后修改 nginx.conf 配置文件:

在 nginx.conf 文件的末尾,将两个文件的路径写上即可。修改完配置文件之后,重新启动 nginx 服务,会发现需要输入生成私钥时输入的密码:

nginx 重启完之后,可以看到现在已经开始监听 443 端口

使用 https 访问 nginx 首页,会弹出安全警告:

有了证书为啥还会弹出这个安全警告呢?因为现在仅仅只是服务端配置了证书,但是客户端还没有相关证书,那对于客户端来讲,凭啥要认你服务端的野鸡证书呢。客户端目前还无法识别服务端的证书真假, 虽然服务端的证书确实是某一个CA机构签名认证的,但是这个CA机构是否具有权威性,有没有足够的公信力,在客户端这儿一概不算数。

但是,警告归警告,并不影响继续访问:

所以下一步,就需要让客户算也安装上对应的证书。 那问题来了,nginx 上配置了两个证书,客户端这里装哪个证书?还是说两个都装?之前有说过,双方在进行握手的时候,服务端会将自己的证书发给客户端进行校验,客户端拿着服务端发来的证书在本地的证书信任列表里瞅一圈,没发现对应的证书,那也就是说这个证书我是不信任的,于是就会出现不安全的提示。

执行 certmgr.msc 命令,查看本地系统上所有可信任 CA 机构(根证书)列表,没有我们刚刚自建的 CA 机构:

使用用谷歌浏览器进行 https 访问 nginx 首页,就会发现这里的不安全提示中有显示证书无效

本质上客户端对于证书的不信任,其实是对于证书签名的颁发者不信任。所以只要将 CA 机构在客户端安装上,就可以解决这个不信任的问题。 win 上证书的安装非常简单,直接双击运行即可:

安装完成之后,可以在刚才的根证书目录中看到自建 CA 机构的根证书

到此为止,https 的双端证书安装就已经全部完成。但是即便是客户端安装了根证书之后,使用 https 访问的时候依然还是会有警告,原因在于:虽然自建 CA 的根证书已经安装在操作系统中,但是浏览器本身还是觉得这件事有点可疑。因为各家大厂出品的浏览器,也是和操作系统一样,自身也是预先安装了很多可值得信任的根证书,即便自建 CA 机构的根证书骗了服务端,也骗了客户端,但是浏览器可骗不了,除非能深度更改浏览器自带的根证书列表,但这个难度可想而知。

HTTPS 性能优化

为什么需要优化

其实很多所谓的性能优化,在大部分时候是根本不需要我们去优化什么东西,反而有些人为了突出自己的能力,强行去优化结果只能是适得其反。虽然前面提及过,https 的效率相较于 http 较差,但经过这么长时间的迭代升级,到现在 https 的性能和 http 已经相差无几。

如果一定要对优化做点事情的话,可以考虑从这几个角度思考:协议优化、证书优化、会话复,以及其它方面的一些点。还有,比较前瞻性的 http2,可兼容 http1 及 https。

前面有提及过,http 加了 s 之后,安全性得到了保障,但基于 TLS 协议下每次握手都比较浪费时间,客户端拿到服务端的证书之后,要花费时间去验证证书的可靠性,是否值得信任啊,有没有过期啊等等一系列问题;而且每次双方数据发送前要耗费时间加密,数据拿到手之后也要耗费时间去解密;这些都是非常重要的隐形的消耗。

随便打开一个网站,浏览器进入开发者模式,观察消耗时间的瀑布流:

Queueing:指的是当前浏览器可以并发处理的请求只有 6 个,请求多了之后,就需要排队进行等待,可以看到当前这个请求在队列中等待了 1.41ms。

等待完成之后才算是真正进行处理的时间,Stalled:指的是处浏览器需要准备一些资源所耗费的时间,这个准备工作花费了0.75ms;DNS Lookup:查找 DNS 域名解析花费了 22 微秒,极短极短的时间;Initial connection:初始连接,就是TCP的三次握手 + 下面的 SSL 所一共花费的时间,高达 37.73ms,这个时间可不短了;SSL:前面介绍的 TLS 1.2 或者 1.3 版本的 https 工作流程的几次握手,耗费了19.07ms。

连接建立完成之后,才会进行真正的请求和响应。Request sent:请求的发送只占了0.23ms;Waiting for server response:等待服务端的响应正常来说永远占的是最大头,消耗了248.5ms;Content download:最后是将内容下载到本地耗费了27.18ms,也还算快。

通过这张图可以很清晰的看出,客户端和服务端之间真正的有效的通信,只有那 0.23ms 的请求,和 27.18ms 的下载,其余时间全都是各种延迟,不是准备这个,就是加载那个,真正用来干活的时间寥寥无,像极了各位平时上班时输出时间和摸鱼时间。

优化策略

既然大部分的时候都在等待各种延迟,那么优化的思路就很单一,想方设法降低这些延迟即可。

从协议优化的角度来说,从根本上升级协议,直接将 1.2 换成 1.3 是最优解,但是如果无法升级协议的话,那么可以选择将秘钥交换时的椭圆算法换成 x25519 算法。这个算法的高效在于支持False Start,也试试抢跑机制,可以提前向对方发送消息,1.2 版本的这个算法几乎可以达到 1.3 版本算法的效率。

从证书优化的角度来说,如果 A 证书给 B 证书进行签名验证,那么 A 证书就是 B 证书的父证书,这种层级关系叫 证书链。一般来说,服务端会将证书逐一发送给客户端,客户端就逐一接受,逐一验证。而且客户端对于证书的验证所做的事情远比表面上说起来的要复杂得多。客户端会去访问证书的官网,又得发起 http 请求过去。要知道,现在可是处于基础的握手阶段啊,好家伙直接来了个完整的请求!这效率不慢谁慢。

为了加快相关的运算,推荐服务器证书加密使用 ECDSA 算法取代 RSA 算法,相比较起来,ECDSA 算法占用资源更少,运算速度更快,保密性还更好。并且证书在生成的时候也会生成有效期,根证书也是如此。假设根证书一旦到期失效,接收到下级的证书一层一层向上验证的时候,到了最顶层结果发现根证书失效,客户端就会去访问根证书(CA 机构)的网站,远程来验证当前的证书是否合法。这么一折腾下来,花费的时间几乎是不可接受的。

所以,推荐将证书吊销列表换成在线状态 OCSP 协议:可以直接访问当前证书的 CA 机构,让 CA 机构回复此证书的可靠性。但是,这件事并不是客户端来做,而是服务端在发送之前提前已经做好了,这样服务端可以将证书和证书的状态一起发送给客户端,客户端只管接受即可。

从会话复用的角度来说,都知道 http 是一种无状态的协议,每一次的请求都是新的握手动作,都会建立新的会话。对于 https 协议来说,也就是每次都需要进行新的发送证书、接受证书、解密、验证....等等一些的事情。那如果第一次建立好的会话,生成一个 SessionID,服务端把这个SessionID 一并发送给客户端。后面每次请求的时候,客户端都带上这个SessionID,服务端就可以不用建立新的会话,去复用原先的会话,可以省去中间那些解密啊、校验啊那些步骤,自然效率上可以大大的提高。

但是这其中存在一个问题就是,对于客户端来说,连接的的服务端毕竟不多,存 SessionID 问题不大;但是服务端会同时连接无数个客户端,如果每个客户端的 SessionID 都存在本地,这个数据量也不小。于是,便诞生了 SessionTicket 的概念:服务器端将生成的 SessionID 用公钥加密之后发给客户端,这个加密后的字符串就是 ticket。客户端下次请求过来带上这个ticket,服务端拿到之后只需解密这个 ticket 就能得到SessionID,不需要存储在本地,节约了宝贵了服务器资源。

除了 SessionID 机制之外,还有另一种 PSK 机制,也就是 Pre-shared Key(预共享秘钥)。它可以实现 0-RTT,也就是没有时间延迟,那就是直接发数据呗!是的没错,既然要进行会话的复用,也就是说之前已经建立过连接,那还握什么手啊,直接给对方发数据得了,非常的简单粗暴。注意:这里说的握手,是 TCP 握手之后的 TLS 协议握手机制。无论是哪种会话复用,每次连接的 TCP 握手都是必不可少的阶段。当然这种机制的缺点也很明显,数据被截获之后,攻击者可以无限的向服务端发送同样的数据,因为每次过来的数据都是合法的,大量密集的请求就会导致服务器崩溃。而解决的方法也很简单,带上时间戳即可。

除了上述的软件优化之外,硬件层次也可以进行优化升级。实际工作中几乎不会遇到这种场景,这里就不多赘述。大体上的要点在上图中有列出,感兴趣看两眼吧。

HTTP2 特性

https 就是 http 在会话层加了 TLS 协议来保证数据的安全性,而 http2 只是针对 http 进行升级,和 https 没有任何关系。不过既然是基于 http 升级而来,自然也兼容 http 和 https。在开放互联网上http 2 将只用于 https 协议,而 http 协议将继续使用 http1,目的是在开放互联网上增加使用加密技术,以提供强有力的保护去遏制主动攻击。

 为什么在 http2 中要进行头部压缩呢?随便打开看一个请求的请求头内容还是挺多的:

对请求头进行压缩之后,某种程度上来说也算是节约了带宽。其实大部分的时候,真正用于请求的数据真没多少,倒是携带一大堆乱七八糟的数据。请求体能占多少数据,对吧,天天写代码还能不知道么,典型的头重脚轻。对于这些数据,专门发明了一种压缩算法来进行压缩,就是 HPACK(哈弗曼编码)。

哈夫曼编码可用来解决可变长度数据编码问题,就是有的数据编码长,有的数据编码短,其实就是将前面无实际意义补〇去掉。然后给所有的字符创建一个庞大的二叉树来记录,二叉树的叶子结点就是每一个字符,每一个叶子结点距离根节点的路径肯定是不相同的,而这个路径代表的就是所谓的编码。当然,服务端和客户端都得有这套编码,这样压完了传给对方才能保证双方看到的内容是一致的。

另外一个升级的点,就是将老版本传输的字符改为传输二进制流。字符这个东西确实方便了人眼的识别,但是计算机可不认识字符,计算机只认识二进制码。所以在 http2 中直接使用二进制流进行数据的传输,那自然就不需要分什么 header 啊、body 啊之类的,所有的数据全部整合到一个 headers 当中打成一个完整的包。再将这个包切分成 N 多个小段,每个小段就是一帧(frame),充分运用化整为零的思想。这样带来的好处就是,这些帧数据是可以同时走不同的信道并行发送出去。并且同一个信道可以提供给不同的客户端使用,这样可以充分的利用信道资源,尽可能的压榨性能,尽量让信道没有空闲的闲置时间。

并且每次客户端向服务端发送一次请求之后,服务端不仅响应请求的数据,还会将下一次甚至下几次客户端有可能请不求的数据主动推送给客户端,减少了客户端主动请求的次数,这就是 Cache Push 机制。

那么 http2 的一些基本概念就先介绍到这里,毕竟目前 http2 还不是市场上的主流力量,甚至 TLS1.3 都没有普及开来,这些技术的完全普及还有相当长一段的路要走。

猜你喜欢

转载自blog.csdn.net/FeenixOne/article/details/129261291
今日推荐