上篇网络协议的文章介绍了HTTP协议,由于HTTP是文本格式,又是明文传输,所以是非安全的,任何人都可以截获HTTP的请求并看到你传输的信息,万一信息里有用户名密码或银行卡密码之类的信息,那问题就大了。基于此,HTTPS横空出世。
HTTPS是啥?
HTTPS 可以理解为 HTTP+SSL。SSL全称(Secure Sockets Layer)安全套接层,它是网景公司在1994年推出的,后来IETF(互联网工程任务组)将SSL进行标准化,产生了TLS(Transport Layer Security 传输层安全性协议),TLS是 SSL 的直接后继者,所有版本的 SSL 目前均已弃用。但是,使用术语 SSL 来描述 TLS 连接的情况很常见。在大多数情况下,术语 SSL 和 SSL/TLS 都是指 TLS 协议和 TLS 证书。
HTTP不用多说,上篇文章讲的很详细了;TLS就是用来实现加密传输的。在 TCP/IP 协议中TLS和HTTP同属应用层,在OSI 协议中TLS属于会话层,在应用层(HTTP)之下,传输层(TCP)之上。[参考文章],所以顺序是先生成HTTP协议内容,然后交给TLS保证加密传输,TLS加密后再交给TCP进行数据传输。
SSL/TLS协议的特点:
- 身份验证。TLS通过数字证书对通信的双方进行身份验证。数字证书通常由受信任的证书颁发机构(CA)签发,确保通信的对端是合法的实体。
- 加密通信。沟通双方会通过非对称加密的沟通方式协商出一个秘钥,后续用这个秘钥进行对称加密,TLS会将HTTP报文中的任何东西都加密,包括所有报头和荷载。一个攻击者所能知道的只有在两者之间有一连接这一事实。
- 完整性保护。TLS使用消息摘要算法确保传输数据的完整性,防止在传输过程中被篡改。
获取CA证书的过程
一个web网站向外提供服务一般是通过域名的方式来提供的,在国内,一个域名网站要想在互联网上被访问的话需要提前向工信部进行备案,备案成功后,你的域名网站就是合法的了。
备案后就可以使用http协议了,但是如果要想使用https协议的话,需要再去申请CA认证,获得CA证书(也称数字证书)。备案与CA认证可同时进行。
获取CA证书分以下几步:
1.首先生成一对公私钥,当做自己https网站的公私钥,私钥自己保留,将公钥连同公司或个人的信息发送给CA认证机构,申请证书。
2.CA认证机构收到申请后,会进行一系列的调查来保证申请者的合法性,审核完成后,就要给申请者签发证书了。证书内容中可以分为两部分,证书详情信息与签名。
证书详情信息包含了CA认证机构的信息,申请者的信息,证书有效期,网站公钥等。
将证书详情信息用hash算法得到一个摘要值,CA认证机构再用自己的私钥将这个摘要值进行加密后得到一个值,这个值就是证书的签名。这个签名在后面客户端进行证书验证时会用到。
下图是用chrome浏览器查看百度证书的页面
3.申请者拿到证书后就放到自己的服务器上,客户端访问时就可以获取到证书了。
验证证书的过程
客户端发起https请求后,被访问的服务端需要返回它的证书给客户端进行身份验证,来保证传输的安全可靠,防止被非法网站欺诈。
客户端拿到对端的证书后,拿到颁发这个证书的CA机构的公钥,用这个公钥解密证书中的签名,得到证书详情信息的原始摘要值,然后客户端重新用相同的hash算法(证书中有说明是哪种hash算法)将证书详情做个hash运算得到此时证书的当前摘要值,如果原始摘要值==当前摘要值,则证书验证通过。
这里有个问题,客户端怎么知道签发这个证书的机构是个合法机构呢?
CA机构的合法性也是层层认证的,全球几大CA机构被称为ROOT CA,它会用它的私钥给其他机构签发证书,使其下游的CA结构获得身份认证,然后下游机构又可以以同样的方式向下延展,成为一个链式的认证链路。我们的计算机系统里内置一些根证书,当我们拿到一个自称CA机构签发的证书时,可以顺着它的签发链路往上找,直到找到一个授信的证书。系统默认信任任何从ROOT CA签发链路下来的证书都是合法的。如果没有找到合法的证书那么这个网站就可能有问题。
为什么要进行证书验证呢?
上面说了证书验证就是为了证明我们连接到的这个网站是它本人并且是合法的,那什么情况下我们可能链接到的不是本人呢?这就涉及到了中间人攻击。
中间人攻击是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。
HTTPS 使用了 SSL 加密协议,是一种非常安全的机制,目前并没有方法直接对这个协议进行攻击,一般都是在建立 SSL 连接时,拦截客户端的请求,利用中间人获取到 CA证书、非对称加密的公钥、对称加密的密钥;有了这些条件,就可以对请求和响应进行拦截和篡改。
中间人攻击前提:本地请求被劫持(DNS劫持等),客户端没有证书验证的逻辑。下图是一个大致的攻击流程。
可以看到如果没有没有证书验证的话,仅仅使用https的加密策略仍然是不可靠的。而当客户端加上了证书验证后,中间人的非法证书就会被检测到,提示客户端要建立的连接已经不安全了。
当然,即使链接不安全,你仍然能选择信任这个非法证书,强制建立这个连接。这种情况在浏览器上比较常见。另外我们抓包https请求时也可以在代理软件上生成证书,并让自己的浏览器信任这个证书,这样我们就可以抓到https请求的明文了。
HTTPS如何进行加密通信
首先先来了解下HTTPS用到的两种加密策略:对称加密与非对称加密。
对称加密
如果客户端和服务端使用相同的密钥进行消息加密,那么这个加密算法就是对称加密。沟通双方在线下约定一个秘钥,使用相同的算法与密钥进行加密解密。常见的对称加密算法有
- DES(Data Encryption Standard):已经被AES所取代,使用56位密钥长度。
- 3DES(Triple Data Encryption Standard):是DES的加强版,使用三次DES算法进行加密,提供更高的安全性。
- AES(Advanced Encryption Standard):目前最常用的对称加密算法之一,提供128、192和256位密钥长度。
- Blowfish:使用可变长度的密钥(32位到448位),适用于多种应用。
- RC4(Rivest Cipher 4):流密码算法,被广泛用于SSL/TLS协议。然而,由于存在安全性问题,已不再推荐使用。
- IDEA(International Data Encryption Algorithm):一个对称分组密码算法,由于专利限制,目前使用较少。
其中AES是目前最常用的。用java来编写加密解密代码的话可以这样写
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class AESUtil {
private static final String ALGORITHM = "AES";
// 加密方法
public static String encrypt(String content, String key) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
// 解密方法
public static String decrypt(String content, String key) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(content));
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
// 生成密钥方法
public static String generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
keyGenerator.init(128);
SecretKey secretKey = keyGenerator.generateKey();
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
}
public static void main(String[] args) {
try {
String key = generateKey();
String content = "用户名:zs,密码:123";
String encryptedContent = encrypt(content, key);
String decryptedContent = decrypt(encryptedContent, key);
System.out.println("原始内容: " + content);
System.out.println("加密后的内容: " + encryptedContent);
System.out.println("解密后的内容: " + decryptedContent);
} catch (Exception e) {
e.printStackTrace();
}
}
}
原始内容: 用户名:zs,密码:123
加密后的内容: oj77HhYm8pzWqZSFlvDc+L8uMOSSLzZEt+BmSI9cbdg=
解密后的内容: 用户名:zs,密码:123
非对称加密
非对称加密算法会有一对公私钥,私钥放在服务端,不对外公开;公钥对外公开,任何人都可以获得(比如放在CA证书中)。使用公钥加密的内容只有私钥可以解密,使用私钥加密的内容只有公钥可以解密。
常见的非对称加密算法有以下几种:
- RSA(Rivest-Shamir-Adleman): RSA是最早广泛应用的非对称加密算法之一,基于大数因子分解的困难性。
- DSA(Digital Signature Algorithm): DSA主要用于数字签名,确保数据的完整性和认证发送者。
- ECDSA(Elliptic Curve Digital Signature Algorithm): ECDSA是DSA的椭圆曲线版本,提供了相同的安全性,但使用更短的密钥。
- Diffie-Hellman(DH): Diffie-Hellman算法用于密钥交换,而不是直接用于加密。它允许两个通信方在不直接传输密钥的情况下协商共享密钥。
- ECDH(Elliptic Curve Diffie-Hellman): 类似于Diffie-Hellman,但使用椭圆曲线加密,提供相同的密钥交换功能。
- ElGamal: ElGamal算法用于加密和数字签名,基于离散对数问题。
一般情况下 HTTPS 的握手阶段一般采用RSA或DH算法。在现代的HTTPS实现中,为了提高安全性,越来越多地采用了DH密钥交换或其椭圆曲线版本(ECDH)来进行密钥协商。这样可以避免直接使用RSA密钥交换可能存在的一些问题。因此,通常会优先选择使用DH或ECDH算法。
下面是一个DH算法的JAVA代码示例
import cn.hutool.crypto.digest.MD5;
import javax.crypto.KeyAgreement;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class DHExample {
public static void main(String[] args) throws Exception {
// 创建发送方的密钥对
KeyPair senderKeyPair = generateKeyPair();
PublicKey senderPublicKey = senderKeyPair.getPublic();
PrivateKey senderPrivateKey = senderKeyPair.getPrivate();
// 将发送方的公钥传递给接收方
byte[] senderPublicKeyBytes = senderPublicKey.getEncoded();
// 创建接收方的密钥对
KeyPair receiverKeyPair = generateKeyPair();
PublicKey receiverPublicKey = receiverKeyPair.getPublic();
PrivateKey receiverPrivateKey = receiverKeyPair.getPrivate();
// 将接收方的公钥传递给发送方
byte[] receiverPublicKeyBytes = receiverPublicKey.getEncoded();
// 发送方使用接收方的公钥生成共享密钥
byte[] senderSharedKey = generateSharedKey(receiverPublicKeyBytes, senderPrivateKey);
// 接收方使用发送方的公钥生成共享密钥
byte[] receiverSharedKey = generateSharedKey(senderPublicKeyBytes, receiverPrivateKey);
String senderSharedKeyStr = Base64.getEncoder().encodeToString(senderSharedKey);
String receiverSharedKeyStr = Base64.getEncoder().encodeToString(receiverSharedKey);
System.out.println("发送方生成的对称密钥:" + senderSharedKeyStr);
System.out.println("接收方生成的对称密钥:" + receiverSharedKeyStr);
// 由于AES的密钥字节长度仅支持以下几种 [16, 24, 32],所以这里缩减对称密钥的长度, 这里用到了hutool包里的MD5工具类
senderSharedKeyStr = MD5.create().digestHex(senderSharedKeyStr);
receiverSharedKeyStr = MD5.create().digestHex(receiverSharedKeyStr);
System.out.println("精简后的发送方对称密钥:" + senderSharedKeyStr);
System.out.println("精简后的接收方对称密钥:" + receiverSharedKeyStr);
System.out.println("开始演示加解密");
String sendMsg = "你好,这是我的用户名密码";
// 发送方加密消息
String decryptMsg = AESUtil.encrypt(sendMsg, senderSharedKeyStr);
// 接受方解密消息
String decrypt = AESUtil.decrypt(decryptMsg, receiverSharedKeyStr);
System.out.println("原始消息:" + sendMsg);
System.out.println("加密后的消息:" + decryptMsg);
System.out.println("解密后的消息:" + decrypt);
}
private static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DH");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
}
private static byte[] generateSharedKey(byte[] otherPartyPublicKeyBytes, PrivateKey privateKey) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("DH");
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(otherPartyPublicKeyBytes);
PublicKey otherPartyPublicKey = keyFactory.generatePublic(x509KeySpec);
KeyAgreement keyAgreement = KeyAgreement.getInstance("DH");
keyAgreement.init(privateKey);
keyAgreement.doPhase(otherPartyPublicKey, true);
return keyAgreement.generateSecret();
}
}
发送方生成的对称密钥:s2XXYOQmN0Cl4HgAzFGFX74mQt66stSzvcI29ZtXswYtJJwjDFlVvTwtySQJEHHwZmearpfpJoiUNcm/6Wu+rrTrMQ+wHQwPwAu5sFXPWBHFG2kk0DNqwSkx4J46r4enh9V/Pwaq6dzVmo9qnHTpdDqJA5f+uVmZD7QioIOexppJf+Dpc+itlQLN6Xl70BP8Ar+TI3rlQoC+ZbJVPnhjDaeWMe1i7sNdiFN8EMcEmw4KkUaChppIcOQeZ5xQL6x5HR5Be6e7huG+u824hQ5uA4UbhKI05PwrSc9tzyRJX95oDN2+Jra5DOOwd7Wd0HrgJlgf+HZDt5+bEiuZJRYuiA==
接收方生成的对称密钥:s2XXYOQmN0Cl4HgAzFGFX74mQt66stSzvcI29ZtXswYtJJwjDFlVvTwtySQJEHHwZmearpfpJoiUNcm/6Wu+rrTrMQ+wHQwPwAu5sFXPWBHFG2kk0DNqwSkx4J46r4enh9V/Pwaq6dzVmo9qnHTpdDqJA5f+uVmZD7QioIOexppJf+Dpc+itlQLN6Xl70BP8Ar+TI3rlQoC+ZbJVPnhjDaeWMe1i7sNdiFN8EMcEmw4KkUaChppIcOQeZ5xQL6x5HR5Be6e7huG+u824hQ5uA4UbhKI05PwrSc9tzyRJX95oDN2+Jra5DOOwd7Wd0HrgJlgf+HZDt5+bEiuZJRYuiA==
精简后的发送方对称密钥:2f9bd45e9ada84fe7440d387ca0354ef
精简后的接收方对称密钥:2f9bd45e9ada84fe7440d387ca0354ef
开始演示加解密
原始消息:你好,这是我的用户名密码
加密后的消息:UBxBf7OR8sc5osK1JAAuS/10Dri09FY1ECa0f76IO5ivQjOZgWAo0p6Oh10otQFZ
解密后的消息:你好,这是我的用户名密码
HTTPS的密钥协商过程(TLS握手过程)
https秘钥协商的过程简单来说就是两步
1.利用非对称加密方式得到一个随机数
- 客户端生成随机数后公钥加密传输,告诉服务端(RSA算法)
- 双方通过各自的公私钥算出这个随机数(DH算法)
2.利用商定好的加密算法根据这个随机数算出对称加密的密钥,然后开始对称加密对话
流程如下图
当我们使用浏览器访问一个网站的时候,由于是使用 HTTPS 协议,客户端会发送 Client Hello 消息到服务器,以明文传输 TLS 版本信息、加密套件候选列表、压缩算法候选列表等信息。另外,还会有一个随机数,在协商对称密钥的时候使用。
然后,服务端返回 Server Hello 消息, 告诉客户端,服务器选择使用这个TLS协议版本、这个加密套件、这个压缩算法等,还有一个随机数,用于后续的密钥协商,这次的消息也是明文。
紧接着,服务端会给你一个服务器端的证书,然后说:“Server Hello Done,我这里就这些信息了。”
客户端自然不相信这个证书,于是从自己信任的 CA 仓库中,拿 CA 的证书里面的公钥去解密服务端给的证书。如果能够成功,则说明外卖网站是可信的。这个过程中,你可能会不断往上追溯 CA、CA 的 CA、CA 的 CA 的 CA,反正直到一个授信的 CA,就可以了。
证书验证完毕之后,觉得这个服务端可信,于是客户端计算产生随机数字 pre-master,发送 Client Key Exchange,用证书中的公钥加密,再发送给服务器,服务器可以通过私钥解密出来。
到目前为止,无论是客户端还是服务器,都有了三个随机数,分别是:自己的、对端的,以及刚生成的 pre-master 随机数,通过这三个随机数,可以在客户端和服务器产生相同的对称密钥。三个随机数中前两个是明文传输的,只有pre-master是公钥加密传输的,所以,预防黑客攻击的主要手段就是这个加密的随机数。
有了对称密钥,客户端就可以说:“Change Cipher Spec,咱们以后都采用协商的通信密钥和加密算法进行加密通信了。”然后发送一个 Encrypted Handshake Message,将已经商定好的参数等,采用协商密钥进行加密,发送给服务器用于数据与握手验证,试一试对称加密传输。
同样,服务器也可以发送 Change Cipher Spec,说:“没问题,咱们以后都采用协商的通信密钥和加密算法进行加密通信了”,并且也发送 Encrypted Handshake Message 的消息试试。当双方握手结束之后,就可以通过对称密钥进行加密传输了。
除了加密策略外其他的步骤就和 HTTP 协议一样了,毕竟 HTTPS 就是 HTTP + TSL 。
上面的图和过程是采用RSA进行非对称加密的过程,看过上面DH加密算法的同学应该能看到,DH算法不需要pre-master,仅仅双方传递一下公钥就能得到对称加密的密钥。所以不同的非对称加密算法在TLS握手阶段的对话过程会略有不同,但是大体方向不变,都是非对称加密协商秘钥,然后使用秘钥进行接下来的对称加密对话。
HTTPS协议如何保证传输数据的完整性呢
TLS握手完成后,双方开始采用对称加密算法加密传输,每次传递的消息格式都为【有效数据+消息摘要】,接收方收到消息后采用双方提前约定好的摘要算法对有效数据进行摘要计算,如果算出的值和收到的消息摘要值相同,则证明消息是正确无损的;反之则消息有可能是在传输过程中丢失了数据或者被篡改了数据。
好了,到此为止,HTTPS的内容差不多介绍完了,感兴趣的同学可以使用抓包工具如wireshark自己抓一下包看看实际交互过程,用实践验证理论。