浅谈https基本原理

作者 大宝 日期 2018-07-13
浅谈https基本原理

为什么使用 https?

因为 http 协议的数据在传输过程中可以被截获和篡改,也就是中间人攻击(Man-in-the-MiddleAttack,简称 MITM),所以说 http 协议是不安全的。https 的出现就是为了给 http 协议提供一条加密通道,对传输中的数据进行加密,从而保证数据的安全性。

什么是中间人攻击(MITM)?

当主机A和主机B通信时,都由主机C来为其“转发”,而A、B之间并没有真正意思上的直接通信,而是以C作为中介来完成,但是A、B却不会意识到,而以为它们之间是在直接通信。这样攻击主机在中间成为了一个转发器,C不仅可以窃听A、B的通信,还可以对信息进行篡改再传给对方。

如何进行数据加密?

加密方式有两种:对称加密非对称加密

对称加密

加密和解密的密钥是同一个。常用的对称加密算法有AES和DES。

对于数据A(明文),如果对它进行AES加密,密钥是key,假如加密后的内容是B(密文):

B = AES_encrypt(A, key)

然后再对密文B进行AES解密,密钥还是key,则会得到明文A:

A = AES_decrypt(B, key)

非对称加密

非对称加密有两个密钥:公钥和私钥。常用的非对称加密算法是RSA。

首先通过RSA算法生成公钥 public_key 和私钥 private_key。对明文A进行RSA加密,密钥是public_key,得到密文B:

B = RSA_encrypt(A, public_key)

然后再对密文B进行RSA解密,密钥是 private_key,则可以得到明文A:

A = RSA_decrypt(B, private_key)

通常情况下,RSA是用公钥加密、私钥解密。但其实RSA用私钥加密、公钥解密也是可以的,只不过这种方式要求待加密内容的长度不能太长。

https 干了什么?

https 其实就是在主机A和主机B建立http连接的时候,增加了A、B间协商密钥的过程,然后用协商出来的密钥对之后传输的数据进行加密。

什么叫协商密钥?

协商密钥又叫交换密钥。这个过程就是让通信双方约定一个加密算法和密钥,用于通信双方后续传输数据的加密和解密。

比如小明和小红相互传纸条,但是中间又必须要经过小强,而他们不想让小强看到字条的内容。所以小明和小红就约定:咱们都用AES加密,密钥是key。这样小强就只能看到字条的密文,而小明和小红拿到字条后,使用密钥key进行AES解密,就可以看到字条的明文。

怎样协商密钥?

上面的例子其实有个问题:小明和小红究竟是通过什么方法来约定加密算法和密钥的?

拿客户端和服务端来举例,协商密钥可以采取如下方式:

1,编写客户端软件的时候,和服务端约定了加密算法和密钥,然后把这个约定写死在客户端。除非以后升级软件修改约定,否则这个约定不会改变。但是这样不能保证加密规则不被泄漏,因为有如下可能性:

  • 客户端软件的源码被破解,加密规则随之被破解。
  • 通过暴力破解进行匹配,对网络数据包不断尝试各种加密规则,最终获取正确的加密规则。

2,不将加密规则死在客户端,而是在每次建立连接的时候,由服务端随机生成一个加密规则,然后传给客户端。这显然不行,因为这个加密规则是直接明文传输的,很容易被中间人获取。

3,使用非对称加密算法,如RSA算法。首先在服务端使用RSA算法生成公钥 S_public_key 和私钥 S_private_key,然后在编写客户端软件的时候,将公钥 S_public_key 写死在客户端,而私钥 S_private_key 保存在服务端。在客户端和服务端建立连接的时候,由客户端随机生成加密规则 rule,然后使用公钥 S_public_key 进行加密得到 encrypted_rule:

encrypted_rule = RSA_encrypt(rule, S_public_key)

然后客户端把 encrypted_rule 传给服务端,服务端拿到加密数据 encrypted_rule 后,用私钥 S_private_key 解密即可得到加密规则 rule:

rule = RSA_decrypt(encrypted_rule, S_private_key)

以上其实就是一个“公钥加密、私钥解密”的过程。总结如下:

  • 服务端: 生成公钥 S_public_key 和私钥 S_private_key
  • 客户端: 将公钥 S_public_key 保存在客户端
  • 客户端: 生成加密规则 rule
  • 客户端: 对 rule 用 S_public_key 加密得到 encrypted_rule
  • 客户端: 将 encrypted_rule 传给服务端
  • 服务端: 收到 encrypted_rule 后用 S_private_key 解密得到加密规则 rule

问题:

为什么这种方式可以保证加密规则的安全?

RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥,即公钥,而两个大素数组合成私钥。

RSA算法中,用公钥加密的数据只能用私钥来解密。而私钥是保存在服务端的,不会在网络上随意传输,可以认为是非常私密和安全的。而通过公钥计算出私钥又几乎是不可能完成的工作。所以私钥的私密性和不可破解性,决定了RSA算法的安全性。

还有非常重要的一点是:这个加密规则是动态变化的。每个客户端生成的加密规则都是不同的,而且每次建立连接时生成的加密规则也是不同的。

既然RSA既可以公钥加密私钥解密,又可以私钥加密公钥解密,为什么不直接使用公钥和私钥对之后传输的数据进行加解密?

有两个原因:首先,私钥加密要求待加密的数据长度不能太长,所以如果传输的数据太大,私钥加密是不可行的。其次,RSA的加密效率要比对称加密的效率低,所以选择用RSA进行对称加密密钥的协商,然后再用协商出来的密钥对后续传输的数据进行对称加解密,这样就能提高数据传输的效率。

为什么需要由客户端生成加密规则,而不是服务端?

如果在服务端生成加密规则,只能通过“服务端私钥加密,客户端公钥解密”的方式来协商加密规则。加密规则包含加密算法和密钥,而为了增加破译难度,这个密钥可能会比较长。而私钥加密要求待加密数据的长度不能太长,所以,如果加密规则的长度过长,可能导致无法进行私钥加密的情况。

为什么需要将私钥保存在服务端,公钥保存在客户端,而不是反过来?

私钥一般有两个用处:解密和签名。解密指的是,对用公钥加密的数据进行解密。签名指的是,对一段较短的数据进行加密(因为私钥不能对太长的数据加密)。而在密钥协商过程中,服务端生成的私钥,其作用就是解密。
私钥必须要保证绝对的私密性和安全性,一旦生成,就要保存在本地,不能随意外传。私钥如果泄漏,RSA加密将没有任何安全性可言。所以,如果将私钥保存在客户端,私钥的私密性将无法保证。

为什么需要将公钥写死在客户端,而不是通过网络传给客户端?

如果通过网络传给客户端,则存在中间人攻击的风险。

假如在客户端C和服务端S之间有一个中间人M,则C和S之间的数据传递会经过M。服务端S生成了一对公钥 S_public_key 和私钥 S_private_key ,然后把 S_public_key 传给客户端C。M拦截到了 S_public_key,但没有把它传给C,而是自己生成了一对公钥 M_public_key 和私钥 M_private_key,然后把 M_public_key 传给了C。

然后客户端C随机生成了一个加密规则 rule,用 M_public_key 加密得到 encrypted_rule,并传给服务端S。中间人M拦截到了 encrypted_rule,用M_private_key 进行解密,从而得到rule,这样就导致了加密规则的泄漏。这时候,M甚至可以自定义加密规则 M_rule,用 S_public_key 加密得到 M_encrypted_rule,并传给服务端S。然后S用 S_private_key 对 M_encrypted_rule 进行解密,得到 M_rule。

这种情况下,客户端C和服务端S都是感知不到中间人M存在的。简而言之,如果将公钥通过网络传给客户端,则会因为中间人攻击而导致加密规则的泄漏甚至篡改。

以上过程总结如下:

  • 服务端: 生成公钥 S_public_key 和私钥 S_private_key
  • 服务端: 本想把 S_public_key 传给客户端,却被中间人拦截
  • 中间人: 拦截了 S_public_key
  • 中间人: 生成自己的公钥 M_public_key 和私钥 M_private_key
  • 中间人: 将公钥 M_public_key 传给客户端
  • 客户端: 生成加密规则 rule,并用 M_public_key 加密生成 encrypted_rule
  • 客户端: 本想把 encrypted_rule 传给服务端,却被中间人拦截
  • 中间人: 拦截了 encrypted_rule,并用自己的私钥 M_private_key 解密得到 rule
  • 中间人: 生成自己的加密规则 M_rule(M_rule 可能等于 rule),并用 S_public_key 加密生成 M_encrypted_rule
  • 中间人: 将 M_encrypted_rule 传给服务端
  • 服务端: 对 M_encrypted_rule 用自己的私钥 S_private_key 解密得到 rule

但是,将公钥写死在客户端还有一个问题:如果由于某种原因(比如私钥不小心泄漏了)需要换一对公钥和私钥,对于服务端来说很简单,但是对于客户端,软件必须修改后升级才能得到最新的公钥。这样就显得很不灵活。所以,要想灵活,客户端就需要通过网络请求从服务端获取最新的公钥,但这样做怎样防止中间人攻击呢?

怎样正确地通过网络传递公钥?

如果要通过网络请求把公钥传给客户端,需要引入一个有公信力的第三方机构A。这个机构A自己生成一对公钥 A_public_key 和私钥 A_private_key,然后将公钥公之于众,将私钥保存起来,除了自己不让任何人知道。

服务端S自己生成了一对公钥 S_public_key 和私钥 S_private_key,为了将公钥安全的传给客户端C,S将自己生成的公钥 S_public_key 交给机构A,让A生成一个证书。

这个证书主要包含:

  • S生成的公钥 S_public_key
  • S的组织名称和域名信息等
  • 证书签名 signature
  • 签名所使用的信息摘要算法(如 MD5 或 SHA256 等)
  • 证书颁发机构A的名称
  • 证书有效期等

证书签名 signature 的计算过程是这样的:首先选择一个信息摘要算法,如 SHA256,计算出证书相关信息 S_related_info(包括服务端S的公钥和域名等信息)的摘要信息 summary,然后用机构A的私钥 A_private_key 对 summary 进行加密,从而得到签名 signature:

summary = SHA256(S_related_info)

signature = RSA_encrypt(summary, A_private_key)

这个时候机构A的私钥 A_private_key 所起的就是“签名”的作用,而不是“解密”的作用。之所以先计算 S_related_info 的摘要信息,再用 A_private_key 进行加密(签名),是因为私钥加密要求待加密内容不能太长。

然后,服务端S就可以把这个带有签名和公钥 S_public_key 的证书放心地传给客户端C了。客户端C拿到这个证书后会对证书上的域名和签名等信息进行验证,域名的验证很简单,签名验证的过程大致如下:

首先,C根据证书上提供的信息摘要算法,如 SHA256,对证书上的相关信息 S_related_info1 计算出其摘要信息 summary1:

summary1 = SHA256(S_related_info1)

然后,C拿着机构A的公钥 A_public_key(前面说过,这个公钥对所有人都是公开的)对证书上的签名 signature1 进行解密,得到 summary2(私钥加密,公钥解密):

summary2 = RSA_decrypt(signature1, A_public_key)

然后比较 summary1 和 summary2,如果相等,则表示该证书就是服务端S的证书,没有被中间人M篡改或替换过,验证通过。

BUT WHY?

其实这正是证书上的签名起了作用,它可以防止证书上的公钥等信息被篡改。如果中间人M修改了证书上的公钥等信息,但是证书的签名是伪造不了的,因为证书签名必须要用机构A的私钥,而M不可能拿到A的私钥。

假如M生成了一对公钥 M_public_key_for_sign 和私钥 M_private_key_for_sign,然后用 M_private_key_for_sign 伪造了签名(签名过程同机构A),客户端C由于没有 M_public_key_for_sign,所以在签名验证过程中计算出的 summary2 是不可能和 summary1 相等的。

或者,假如M也在机构A生成了一个证书,而这个证书上的域名不可能是服务端S的域名,所以不能通过客户端的域名验证。如果M强行把证书上的域名换成服务端S的域名,那么这个证书上的签名验证就不可能通过。

所以,问题的关键是:中间人M由于没有机构A的私钥而无法伪造签名,而我们知道,机构A是很有公信力的,他不可能把自己的私钥泄漏给任何人,这个机构A我们一般叫作“数字证书签发机构”,即CA。

Charles为什么可以拦截https?

以上就是 https 避免中间人攻击的原理。但是有的同学要问:我用 Charles 可以拦截 https 啊,Charles 的工作方式不就是中间人攻击吗?

是的,Charles 拦截网络请求其实就是中间人攻击,但是它是怎么对 https 进行中间人攻击的呢?

其实在用 Charles 拦截 https 请求之前,需要你在客户端安装一个 Charles 生成的证书,这个证书主要包含了刚才提到的公钥 M_public_key_for_sign。这样,你在建立 http 连接的时候,Charles 就可以用私钥 M_private_key_for_sign 伪造签名,而客户端由于安装了 Charles 的证书,就会选择用 M_public_key_for_sign 去验证签名,这样签名就可以验证通过。BINGO!

最后也要提醒大家,一般情况下,千万不要在自己的手机、电脑和浏览器上安装一些不受信任的证书,以免自己的用户名密码等敏感信息被泄漏。