<commit_before>---
layout: post
title: 漫谈网络通讯加密（2）流程设计
tags: ['crypto']
published: true
---

<!--more-->

# 加密通讯的基础

## [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce)（不重数）

在进入正文前，先介绍一个关键的东西：nonce，以及几个粗糙的例子，以说明nonce的作用。

nonce本质就是一段随机码，因为不一定是数字，可能是字符，所以不能叫随机数。因此取了个名字叫nonce。

nonce的存在是为了对付**彩虹表(raintable)**、**重放攻击(replay attack)**，或者说为了制造**请求唯一性(request unique)**。

nonce有server nonce、client nonce之分：

- 需要client nonce的原因：阻止邪恶的客户端做重放攻击。
- 需要server nonce的原因：阻止中间人攻击。


nonce的生成策略有讲究：

- 可以是时间相关变量
- 可以是一个通过强随机算法生成的足够长的bits


### 例子：登录与登录密码

#### 方案1

直接经过无加密的tcp或者http，发password到服务器进行登录。

问题：登录包一被截取，密码就泄露了，攻击者想怎么搞就怎么搞了。


#### 方案2

对password做一个经典的hash：

secret = Hash(password);

客户端只发secret给服务器，服务器执行（这里假设存了密码原文）：

secrect_server = Hash(password_server)；

得到secret_server即可做校验。

问题：攻击者还是截包，就获得了secret，因为每次登录都是发一样的secret，所使攻击者首先可以做**重放攻击**，直接copy用户的登录包，发给服务器，就可以登录了；其次，因为Hash函数无非md5、SHA256这些，完全可以用raintable暴力查表，获得password。

#### 方案3

改进：客户端每次登录前，服务器会生成一个nonce并发给客户端，客户端计算：

secret = Hash(nonce_server, password);

客户端只发secret给服务器，服务器执行：

secrect_server = Hash(nonce_server, password_server)；

然后比较secret和secret_server是否一致即可。

优点：每次登录hash出来的secret都不一样，用户密码比较安全了，要破解密码，得根据secret、nonce_server、Hash，逆向出password，可行，但耗时。

问题：服务端发送的nonce可以被攻击者篡改成固定的nonce，从而导致nonce特性还是被消去了，攻击者就还是可以弄raintable，暴力破解密码。不过因为篡改的nonce和服务器的nonce对不上，客户端发的错误的登录信息，是过不了服务器这一关的。总之，攻击者并不能完美而透明地实现中间人攻击，但可以破解密码。



#### 基于方案3的方案4

既然方案3中，客户端接收了攻击者篡改出来的固定nonce，并发送了符合攻击者raintable预期的secret，导致密码容易被破解，那么客户端是否可以做一些保护措施，使得发到网络中的secret是随机的呢？

方法是有的，就是客户端也生成nonce， 称之为nonce_client，secret的计算变成：

secret = nonce_client || Hash(nonce_client, nonce_server, password)

（Note：||表示concat操作）

服务器收到secret后拆出nonce_client和hash值，本地也执行一遍这条公式，就能做登录校验。

这样处理后，攻击者即使篡改了nonce_server，攻击者收到的客户端secret依然是随机的。并不能直接查raintable获得password，而必须根据客户端的nonce_client，即时算出一份新的raintable表（计算量巨大），才能破解密码。

问题：在登录过程中，本质上还是发送了password，就依然有被猜出密码的可能。如果这个密码是用户的常用密码，例如生日，那后果还是很可怕的。

进一步的思路：既然发送加密的密码不妥，那就是用DH密钥交换了，就不会有密钥在网络中传输的问题。当然对于用户输入密码登录的场景，DH应该是用不了了。


# 简单版本的加密通讯

首先以一个较为简单的例子来说明：某客户端想和某服务器建立tcp加密通讯。

首先需要一个工具：公钥私钥对（非对称加密技术）。

1. 服务器在本地或数据库中存放了用于和任意客户端建立通讯的公钥私钥对。
2. 客户端想和服务器通讯时，服务器发送公钥给客户端。客户端确信用这个公钥加密的数据只有这个服务器能解密。（暂时不考虑中间人攻击问题）
3. 客户端计算一个key（其实是pre-master key和master key，下文会介绍），这个key可能真的是本地算出来的，也可能用DH算法和服务器交换出来的。如果是本地算出来的，那就必须用刚刚的非对称加密通讯信道，发给服务器。总之，key是客户端服务器都得持有。
4. 客户端服务器各自生成nonce，互发给对方。此时双方都持有key、cnonce、snonce。
5. 客户端服务器各自本地计算Hash(key，cnonce，snonce)获得真正用于通讯的对称密钥encrypt key。
6. 另外的，客户端服务器还可以计算Hash2(key，cnonce，snonce)，获得用于别的用途的key，例如integrity protection key，验证消息用的key。

仔细看，第三步传递key之后的步骤其实不是必须的，因为key是用安全的非对称加密信道协商了，没有泄露key的问题。

然而，如果每次建立通讯都要走1-3步骤的话，开销会比较大，所以key其实是要复用的。例如客户端可以缓存在本机，服务器就缓存到数据库，设个过期时间。

之后如果客户端想再次发起通讯，那么就可以跳过1-3步，直接执行4-6步，算出用于本次通讯的加密密钥。


# 完整版本的加密通讯

## 一，公钥证书

首先，上一篇文章说的CA那套公钥证书的东西，是必要的。因为在互联网中通讯，只有依靠公钥证书这个机制，才能确认正在握手的对端是不是你所预期的对端。简要地回顾下：

1. 服务器向客户端出示由某CA签发的公钥证书，即CA用**CA私钥**加密了服务器公钥，导出来的东西，称之为公钥证书
2. 在握手前，用户的浏览器已经安装了该CA的证书，该证书里面有CA的公钥
3. 客户端用本地CA公钥，**解密**收到的公钥证书得到服务器公钥。解得出来，那就说明这个服务器公钥（证书）确实是这个CA曾经用私钥加密过的。
4. 当然，公钥证书是有过期问题的，例如服务器把私钥泄露了，那相应的公钥证书就得从CA里删掉，这就涉及到了**证书撤销列表CRL**。CRL就是一个数据库，这个数据库显然得支持分布式，否则全世界的用户都涌到中心化的服务器去验证证书是否已被撤销，那是做不到的。而分布式数据库，显然就有个更新同步延迟问题，中心数据库更新了CRL，什么时候本地能更新CRL缓存，就决定了安全度有多高。


第4步骤先不说，前3步骤才是最关键的。这些流程，完全是为了**确认正在握手的服务器是不是自己所信任的CA签名了的服务器**。信任的根基是CA，客户端只相信CA，CA说什么就是什么。

值得思考的问题：

- 客户端会不会装了不可靠的CA证书。如果是，那后面的所有流程都是白搭了。
- 通讯双端没有CA基础设施怎么办？例如手机游戏，没有什么浏览器CA，只有tcp udp可用。答案是直接把服务器公钥硬编码到客户端安装包里，只要用户确保是从正规途径下载的安装包，那么就可以保证客户端里的公钥是真实服务器的公钥。例如从app store下载游戏。


## 二，都是为了加密性能

第一步中，客户端和服务器已经建立了可靠的**非对称加密通讯**，客户端持有服务器的公钥，客户端用公钥加密自己数据，只有服务器才能解密，不就万事大吉了吗？

然而很可惜的是，非对称加密算法，性能一般都差强人意，远不如**对称加密算法**。所有用户要发送大量隐私数据给服务器，只用非对称加密的话，先不说客户端耗能，服务器的CPU首先就炸了。

用指令openssl speed xxx，可以认识到各种算法的性能：

```<commit_msg>no message<commit_after>---
layout: post
title: 漫谈网络通讯加密（2）流程设计
tags: ['crypto']
published: true
---

<!--more-->

# 加密通讯的基础

## [nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce)（不重数）

在进入正文前，先介绍一个关键的东西：nonce，以及几个粗糙的例子，以说明nonce的作用。

nonce本质就是一段随机码，因为不一定是数字，可能是字符，所以不能叫随机数。因此取了个名字叫nonce。

nonce的存在是为了对付**彩虹表(raintable)**、**重放攻击(replay attack)**，或者说为了制造**请求唯一性(request unique)**。

nonce有server nonce、client nonce之分：

- 需要client nonce的原因：阻止邪恶的客户端做重放攻击。
- 需要server nonce的原因：阻止中间人攻击。


nonce的生成策略有讲究：

- 可以是时间相关变量
- 可以是一个通过强随机算法生成的足够长的bits


### 例子：登录与登录密码

#### 方案1

直接经过无加密的tcp或者http，发password到服务器进行登录。

问题：登录包一被截取，密码就泄露了，攻击者想怎么搞就怎么搞了。


#### 方案2

对password做一个经典的hash：

secret = Hash(password);

客户端只发secret给服务器，服务器执行（这里假设存了密码原文）：

secrect_server = Hash(password_server)；

得到secret_server即可做校验。

问题：攻击者还是截包，就获得了secret，因为每次登录都是发一样的secret，所使攻击者首先可以做**重放攻击**，直接copy用户的登录包，发给服务器，就可以登录了；其次，因为Hash函数无非md5、SHA256这些，完全可以用raintable暴力查表，获得password。

#### 方案3

改进：客户端每次登录前，服务器会生成一个nonce并发给客户端，客户端计算：

secret = Hash(nonce_server, password);

客户端只发secret给服务器，服务器执行：

secrect_server = Hash(nonce_server, password_server)；

然后比较secret和secret_server是否一致即可。

优点：每次登录hash出来的secret都不一样，用户密码比较安全了，要破解密码，得根据secret、nonce_server、Hash，逆向出password，可行，但耗时。

问题：服务端发送的nonce可以被攻击者篡改成固定的nonce，从而导致nonce特性还是被消去了，攻击者就还是可以弄raintable，暴力破解密码。
拿到密码后，就可以骗过服务器了，实现中间人攻击。

#### 基于方案3的方案4

既然方案3中，客户端接收了攻击者篡改出来的固定nonce，并发送了符合攻击者raintable预期的secret，导致密码容易被破解，那么客户端是否可以做一些保护措施，使得发到网络中的secret是随机的呢？

方法是有的，就是客户端也生成nonce， 称之为nonce_client，secret的计算变成：

secret = nonce_client || Hash(nonce_client, nonce_server, password)

（Note：||表示concat操作）

服务器收到secret后拆出nonce_client和hash值，本地也执行一遍这条公式，就能做登录校验。

这样处理后，攻击者即使篡改了nonce_server，攻击者收到的客户端secret依然是随机的。并不能直接查raintable获得password，而必须根据客户端的nonce_client，即时算出一份新的raintable表（计算量巨大），才能破解密码。

问题：在登录过程中，本质上还是发送了password，就依然有被猜出密码的可能。如果这个密码是用户的常用密码，例如生日，那后果还是很可怕的。

进一步的思路：既然发送加密的密码不妥，那就是用DH密钥交换了，就不会有密钥在网络中传输的问题。当然对于用户输入密码登录的场景，DH应该是用不了了。


# 简单版本的加密通讯

首先以一个较为简单的例子来说明：某客户端想和某服务器建立tcp加密通讯。

首先需要一个工具：公钥私钥对（非对称加密技术）。

1. 服务器在本地或数据库中存放了用于和任意客户端建立通讯的公钥私钥对。
2. 客户端想和服务器通讯时，服务器发送公钥给客户端。客户端确信用这个公钥加密的数据只有这个服务器能解密。（暂时不考虑中间人攻击问题）
3. 客户端计算一个key（其实是pre-master key和master key，下文会介绍），这个key可能真的是本地算出来的，也可能用DH算法和服务器交换出来的。如果是本地算出来的，那就必须用刚刚的非对称加密通讯信道，发给服务器。总之，key是客户端服务器都得持有。
4. 客户端服务器各自生成nonce，互发给对方。此时双方都持有key、cnonce、snonce。
5. 客户端服务器各自本地计算Hash(key，cnonce，snonce)获得真正用于通讯的对称密钥encrypt key。
6. 另外的，客户端服务器还可以计算Hash2(key，cnonce，snonce)，获得用于别的用途的key，例如integrity protection key，验证消息用的key。

仔细看，第三步传递key之后的步骤其实不是必须的，因为key是用安全的非对称加密信道协商了，没有泄露key的问题。

然而，如果每次建立通讯都要走1-3步骤的话，开销会比较大，所以key其实是要复用的。例如客户端可以缓存在本机，服务器就缓存到数据库，设个过期时间。

之后如果客户端想再次发起通讯，那么就可以跳过1-3步，直接执行4-6步，算出用于本次通讯的加密密钥。


# 完整版本的加密通讯

## 一，公钥证书

首先，上一篇文章说的CA那套公钥证书的东西，是必要的。因为在互联网中通讯，只有依靠公钥证书这个机制，才能确认正在握手的对端是不是你所预期的对端。简要地回顾下：

1. 服务器向客户端出示由某CA签发的公钥证书，即CA用**CA私钥**加密了服务器公钥，导出来的东西，称之为公钥证书
2. 在握手前，用户的浏览器已经安装了该CA的证书，该证书里面有CA的公钥
3. 客户端用本地CA公钥，**解密**收到的公钥证书得到服务器公钥。解得出来，那就说明这个服务器公钥（证书）确实是这个CA曾经用私钥加密过的。
4. 当然，公钥证书是有过期问题的，例如服务器把私钥泄露了，那相应的公钥证书就得从CA里删掉，这就涉及到了**证书撤销列表CRL**。CRL就是一个数据库，这个数据库显然得支持分布式，否则全世界的用户都涌到中心化的服务器去验证证书是否已被撤销，那是做不到的。而分布式数据库，显然就有个更新同步延迟问题，中心数据库更新了CRL，什么时候本地能更新CRL缓存，就决定了安全度有多高。


第4步骤先不说，前3步骤才是最关键的。这些流程，完全是为了**确认正在握手的服务器是不是自己所信任的CA签名了的服务器**。信任的根基是CA，客户端只相信CA，CA说什么就是什么。

值得思考的问题：

- 客户端会不会装了不可靠的CA证书。如果是，那后面的所有流程都是白搭了。
- 通讯双端没有CA基础设施怎么办？例如手机游戏，没有什么浏览器CA，只有tcp udp可用。答案是直接把服务器公钥硬编码到客户端安装包里，只要用户确保是从正规途径下载的安装包，那么就可以保证客户端里的公钥是真实服务器的公钥。例如从app store下载游戏。


## 二，都是为了加密性能

第一步中，客户端和服务器已经建立了可靠的**非对称加密通讯**，客户端持有服务器的公钥，客户端用公钥加密自己数据，只有服务器才能解密，不就万事大吉了吗？

然而很可惜的是，非对称加密算法，性能一般都差强人意，远不如**对称加密算法**。所以如果用户要发送大量隐私数据给服务器，且只用非对称加密的话，先不说客户端耗能，服务器的CPU首先就炸了。

用指令openssl speed xxx，可以认识到各种算法的性能：

```
{'fraction_non_alphanumeric': 0.10959091530989191, 'fraction_numerical': 0.008209057326583663, 'mean_word_length': 14.324947589098532, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6619171', 'n_tokens_mistral': 5892, 'n_tokens_neox': 7025, 'n_words': 226}