qiwihui / pocket_readings

每一段时间的 pocket 文章阅读情况的记录
http://pr.qiwihui.com
45 stars 1 forks source link

透过 Rust 探索系统的本原:安全篇 #1063

Closed qiwihui closed 3 years ago

qiwihui commented 3 years ago

微信号 programmer_life 功能介绍 十年漫漫程序人生,打过各种杂,也做过让我骄傲的软件;管理过数十人的团队,还带领一班兄弟姐妹创过业,目前在硅谷一家创业公司担任 VP。关注程序人生,了解程序猿,学做程序猿,做好程序

Tags: rust

via Pocket https://ift.tt/3cD0h8v original site

April 01, 2021 at 11:58AM

github-actions[bot] commented 3 years ago

透过 Rust 探索系统的本原:安全篇 by 程序人生

2021-03-22 安全是我的老本行,隔一段时间不拉出来谈一谈就不舒服。我个人觉得:做应用不谈安全都是在耍流氓。

按照 CISSP[1] 的定义,安全有八大领域:

本文只关注 Communication and Network Security 中 TCP/IP 范畴下的 Session Layer Security,也就是 TCP/UDP 层之上的安全方案:

目前业界首选的方案是 TLS[2]。在所有流行的应用层解决方案中,都离不开 TLS。

在 p2p 领域,TLS 并不那么受待见,大家似乎更喜欢和 TLS 提供了同等安全水平,但更加去中心化(不需要 CA[3])的 noise protocol[4]。我去年写过一篇关于 Noise protocol 的文章:Noise 框架:构建安全协议的蓝图

本文围绕 TLS 和 Noise protocol,以及它们在 Rust 下的使用场景,谈谈我们如何做安全的系统和应用。

安全的本质

很多人谈到安全,首先想到的是加密解密。加解密只解决了安全范畴的机密性(confidentialilty)的问题,但它没有触及另外两个重要的范畴:完整性(integrity)和可用性(availability)。我们简单讲一下这三个概念:

为了保证可用性,我们会提供服务的高可用性(防止 DoS 以及服务故障),做数据冗余和灾备处理(防止数据丢失),监控,故障转移等等。

为了保证完整性,我们会使用哈希算法,数字签名来保证数据的一致性。

为了保证机密性,我们会使用对称和非对称加密来保证在传输途中,以及在数据载体上的机密性。机密性往往需要完整性作为先决条件;而完整性不一定需要机密性作为先决条件。

下图阐述了安全领域涉及机密性和完整性的主要算法:

注意,这里的一些算法是泛称,而非具体某种算法。比如:sha3 算法族下面除了 keccak 以外,还有 blake2,blake3 等其他算法;而 ECC 算法下面,属于签名算法的有 Ed25519,Ed448 等,属于非对称加密的有 x25519,x448 等。

如果你看了我前几周的文章,大概对我介绍的《胖客户端,廋服务器》有些印象。文章中我提到了服务端把用户端的事件写入到事件日志中,客户端可以 clone / pull 这些事件日志,在本地生成相应的状态。那么问题来了,客户端怎么知道 clone 下来的事件日志是未经第三方篡改的事件日志呢?很简单,我们只需对日志文件做个 hash,然后用服务器的私钥对这个 hash 做一个签名,把签名附带在文件头上。这样客户端收到文件后,用服务器的公钥验证这个签名即可。这样,只要服务器的私钥不泄露,这个签名就可以验证文件的完整性。在比特币的世界里,每个区块的完整性都由打包该区块的矿工的签名来保证,而整个链的完整性则由哈希链和中本聪共识(最长链)保证。

进一步的,如果我们用户的私有 repo 下的所有事件日志都只有用户自己才能访问,其他人(包括服务端应用)都无法访问,那么我们可以用用户的公钥来加密 repo 的所有事件日志。

DH 算法:互联网安全的基石

当我们需要保证存储在媒介上的信息的安全性时,一切都简单而直观;但当我们需要保证在网络传输中的实时信息的安全性时,我们就面临着巨大的挑战。

这其中第一个挑战就是:每个连接使用什么密钥来加密数据?

我们知道,在网络传输中,非对称加密的效率不高,不适合大量数据的加密,而对称加密则需要双方共享密钥,才能正常通讯。因此,我们需要一种手段,在不安全的网络信道中,只传输部分信息,通过这部分信息 + 本地的私有信息,协商出来双方一致的共享密钥。第三方即便获得明文传输的信息,也无法推导出密钥。如果这样的手段行得通,那么,我们就可以在网络通讯的握手过程,生成每个 session 独立的共享密钥(session key),后续的通讯都通过这个(这对)密钥来加密完成。这个协商的过程就是 DH 算法(Diffie-Hellman Key Exchange Algorithm)[5](对算法细节感兴趣的可以去看 wikipedia):

DH 算法是 TLS 以及 Noise protocol 的基石。如果没有它,我们就不会有目前这样一个繁荣且安全的互联网世界。

在 Rust 下,如果你需要直接使用 DH 算法,可以使用 dalek 出品的 x25519-dalek[6]。它是使用 curve25519 [7] 的 ECDH(Elliptic Curve Diffie-Hellman) 的实现。代码如下:

use rand_core::OsRng;

你也许会问:我又不去实现 TLS 或者类似的加密协议,而我自己的网络传输都靠 TLS 保护着呢,DH 对我来说有什么用呢?

我能想到的一个场景是文件加密。在本文开头,我说:

进一步的,如果我们用户的私有 repo 下的所有事件日志都只有用户自己才能访问,其他人(包括服务端应用)都无法访问,那么我们可以用用户的公钥来加密 repo 的所有事件日志。

这个方案的缺点是效率太低 — 如果需要加密的文件有几个 G,非对称加密显然不好。但我们又没法用对称加密:毕竟我们不能跟每个人都预先共享一组密钥(共享密钥本身又存在安全风险)。这时,我们可以用 DH 算法生成一个只对这个文件有效的密钥,加密文件,然后在文件头提供必要的信息即可:

  1. 生成一个临时公钥对

  2. 用私钥和用户的公钥算 DH key

  3. 用 DH key 作为 AES[8] 或者 ChachaPoly[9] 的密钥,加密文件

  4. 在文件头添加临时生成的公钥

这样,在解密端,用户可以用自己的私钥和文件中携带的公钥算出 DH key,然后解密之。

如果大家对这个思路感兴趣,可以参考我用 Noise protocol 做的类似的解决方案:tyrchen/conceal[10]。

除了 x25519-dalek 外,ristretto255-dh[11] 也值得一试,它是 zcash 实现的 Ristretto255[12] DH 算法。

TLS:不仅仅是 HTTP 的安全防线

作为 HTTPS 的安全协议的唯一选择,相信大家对 TLS(以及它的前身 SSL)有一定的了解 — 起码知道:

如果你经常调试(或者逆向工程)HTTPS,你大概还知道:

你大概率不知道:

证书是个什么鬼?

我们这里所说的证书,是 PKI 体系下的 X.509 证书[16]。X.509 证书用于证明一个公钥的身份。我说我是 domain.com 的合法服务器,何以见得?我生成一对私钥和公钥,通过其签署一个 CSR(Certificate Signing Request [17]),里面通过 CN(Common Name)声索我对 *.domain.com 的占有。一般一个 CSR 包含这些信息:

随后我把 CSR 提交给一个由某个根证书签署的 CA,由其签名并发回给我。这样,任何应用通过 HTTPS 连接 domain.com 时就可以正常通讯。

在 letsencrypt[18] 成为主流之前,证书是个几乎相当于特许经营的好生意。像 godaddy 这样的家伙,一个证书可以卖上百美金一年,简直如同抢钱。证书作为一门生意,极大地破坏了互联网的安全性,很多小的玩家不想支付每年的证书费用,干脆就避免使用 HTTPS。letsencrypt 的出现,几乎摧毁了各大吃相难看的 CA 的生意。Letsencrypt 自动化了证书的申请流程,只要你能把某个域名指向你的服务器,让 letsencrypt 验证到你请求的域名就是你拥有的域名,可以立即签署一个有效期是 3 个月的免费证书。至于证书的有效期为啥不能更长,这个,根本不是技术原因,我猜是来自做垂死挣扎的既得利益者们的压力。

能不能自己做 CA?

CA 机构是 internet 的不可或缺,却相对脆弱的一环。Letsencrypt 只是解决了证书收费的问题,不过没有解决 CA 机构本身的脆弱性 — 任何一个中心化的,可以签署证书,被数亿设备信任的机构都是有安全风险的,因为黑客随时盯着这些机构的漏洞。一旦一个CA 被攻陷,黑客们可以伪造出成千上万的域名的服务器证书。

有没有可能一个应用的客户端和服务器使用自己的 CA,绕过任何 CA 机构?

当然可以。你可以生成自己的 CA cert(自签名),然后用 CA key 签名 Server cert。你的客户端在启动 TLS 连接时,信任你自己的 CA cert 即可。你甚至还可以通过你的 CA 给每个客户端签名,让服务器也同时验证客户端是你信任的客户端。如下图所示:

一个新的客户端在注册新用户/登录时,服务器会从 CA 获取证书,连同用户登录获得的 token 一同返回给客户端。之后,客户端访问任何服务端的 API(除 auth 之外),都需要提供 client cert 供服务器验证,这样,额外增加安全性,并且,可以杜绝 Charles Proxy 这样的中间人。

当然这样的手段只适合客户端代码由你自己控制(比如 iOS/android/OSX/Linux/Windows app)。你无法让你的服务器证书通过浏览器的安全验证(因为证书不在系统根证书的信任列表中),因而,任何使用浏览器访问你的服务器的用户将无法使用你的服务。

如果你对这样的方案感兴趣,可以看看我的 crate: tyrchen/cellar[19]。它借鉴比特币分层钱包[20]的设计,可以从一个 passphrase 衍生出确定的分层密码/密钥/证书。生成的证书可以被应用在 TLS 应用中,比如:tyrchen/mobc-tonic[21](我做的一个 grpc client connection pool)。

下面是我通过 celllar 生成的 CA 证书(注意 CA: TRUE):

以及该 CA 签署的服务器证书(注意 CA: FALSETLS Web Server Authentication):

以及该 CA 签署的客户端证书(注意 CA: FALSE 以及 TLS Web Client Authentication):

将 TLS 应用在 HTTP 之外

TLS 可以保护我们的 HTTP 应用,其中包括 REST/GraphQL/Websocket API,以及 gRPC API。虽然目前 HTTP 是几乎绝大多数互联网应用使用的协议,但还有大量的其它基于 TCP 层的协议。如果你要保证这些协议的安全性,使用 TLS 是一个简单直接的选择。

然而,理解和使用好 OpenSSL 库不是一件容易的事情。十多年前,我曾经用 C 语言和老旧的 OpenSSL (0.9.x)打过交道,那体验相当不好。Python / Erlang 有不错的 OpenSSL 的封装,在应用中使用 TLS 比较舒服自然。如果你熟悉的语言没有很好的库去包装 OpenSSL,那么,在应用中使用 TLS 就不那么容易。

在 Rust 里,除了 OpenSSL 的封装,我们还有 Rustls[22]。它是一个经过了 security auditing[23] 的 TLS 安全裤,性能比 OpenSSL 更好,且理论上更加安全(没有遗留的历史问题,没有 TLS1.1 及以下的不安全代码,没有遗留的不安全的加密算法,比如 RC4,3DES)。

Rustls 虽然比 OpenSSL 容易使用,但成功建立起 TLS 连接,还需要更多对 TLS 细节的理解。为此,我做了一个 crate:tokio-tls-helper[24],可以让你通过简单的配置,创建 TLS connector (client) 和 acceptor (server)。

比如客户端使用自定义的 CA cert 以及通过自定义 CA 签署的 client cert:

domain = "localhost"

有了这个配置,客户端可以生成 ClientTlsConfig,然后生成 connector,在建立好 TCP stream 后,直接调用 connector.connect(stream) 就可以将 TCP 连接升级为 TLS 连接,之后可以在其之上进行应用层的协议:

let msg = b"Hello world\n";

服务端的使用也很简单:配置相同的 CA cert,以及服务器的 server/key:

[identity]

服务端同样可以通过配置生成 ServerTlsConfig,然后生成 acceptor,之后正常使用 TCP listener accept 一个 TCP stream 后,就可以通过 acceptor.accept(stream) 把 TCP 连接升级为 TLS。这个过程配合客户端的 connector.connect(stream),共同完成前面所说的 DH 过程,协商出来 session key,然后开始加密/解密应用层的数据:

let config: ServerTlsConfig = toml::from_str(config_file).unwrap();

Noise Protocol:狂野西部的守护者

如果你没看过我之前的文章,大概率 Noise Protocol 对你来说是个陌生的名字。如果你搭过各种各样的梯子,你也许使用过 Wireguard[25],那么恭喜你,你已经在不知不觉中使用 Noise Protocol 了 — 因为 Wireguard 在安全层使用了 Noise Protocol。我曾经写过一篇文章:Wireguard:简约之美,介绍了 Wireguard 这个非常牛逼的 VPN 工具。

因为之前的关于 Wireguard 和 Noise protocol 的文章对 Noise Protocol 本身已经有足够丰富的介绍,这里我就不再赘述 Noise Protocol 的细节。

如果说 TLS 是出身高贵的正规军,那么 Noise Protocol 就是出身草根的土八路。但二者其实互相借鉴,互相学习。TLS 1.3 和 Noise Protocol 的主要区别有两个:

  1. 在身份验证方面二者走上了不同的道路(TLS 1.3 使用证书,而 Noise Protocol 完全不使用)

  2. 通讯过程中使用的算法一个走协商(TLS)一个走预配置(Noise)

走协商还是走配置这跟协议的使用场景有关。TLS 关注的是大量不同版本的标准客户端(比如 Firefox)和服务器之间的交互,两端支持的算法族可能有不小的出入,协商出双方都能接受的算法是最优的选择,这样可以支持尽可能广的应用场景;而 Noise Protocol 关注的是定制的客户端和服务器之间的交互,因而两端可以通过预配置来确定使用的算法。比如 WireGuard 使用 Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s,那么客户端和服务端都需要:

因为避免使用证书这样天然具有中心化的东西,Noise Protocol 在 p2p 领域走出了新的天地。从最简单的 NN(双方都没有固定公钥)KK(双方都知道对方的固定公钥),到最复杂的 XX(双方都有固定公钥,通过网络加密传输给对方),Noise Protocol 定义了 12 种协商模式,再辅以不同的哈希和加密算法,可以组合出上百种安全方案,总有一种适合你:

在 Rust 下,snow[26] 是 Noise Protocol 的非常成熟的实现,而 libp2p 则使用了 snow 来实现 libp2p 协议的安全层。

下面是使用 snow 在 TCP 协议之上建立加密信道的实例。我们可以看到,仅需额外的几行代码就可以将你的网络应用打造得非常安全:

服务器:

let params: NoiseParams = "Noise_XX_25519_ChaChaPoly_BLAKE2s".parse().unwrap();

客户端:

let params: NoiseParams = "Noise_XX_25519_ChaChaPoly_BLAKE2s".parse().unwrap();

因为 snow 的所有操作都直接操作内存的 buffer,所有的 IO 都是由你创建的 TCP stream 完成,所以 snow 可以很好地在同步或者异步模式下工作。

贤者时刻

连接千万条,安全第一条。网络不加密,亲人两行泪。- 鲁迅:不是我说的

参考文献

[1] CISSP: https://www.isc2.org/Certifications/CISSP

[2] TLS: https://en.wikipedia.org/wiki/Transport\_Layer\_Security

[3] CA: https://en.wikipedia.org/wiki/Certificate\_authority

[4] Noise Protocol: https://noiseprotocol.org/

[5] DH 算法:https://en.wikipedia.org/wiki/Diffie–Hellman\_key\_exchange

[6] x25519-dalek: https://github.com/dalek-cryptography/x25519-dalek

[7] curve25519: https://en.wikipedia.org/wiki/Curve25519

[8] AES: https://en.wikipedia.org/wiki/Advanced\_Encryption\_Standard

[9] ChachaPoly: https://tools.ietf.org/html/rfc7539

[10] tyrchen/conceal: https://github.com/tyrchen/conceal

[11] ristretto255-dh: https://github.com/ZcashFoundation/ristretto255-dh

[12] Ristretto: https://ristretto.group/

[13] PEM: https://en.wikipedia.org/wiki/Privacy-Enhanced\_Mail

[14] Charles proxy: https://www.charlesproxy.com/

[15] Man in the middle: https://en.wikipedia.org/wiki/Man-in-the-middle\_attack

[16] X.509 certificate: https://en.wikipedia.org/wiki/X.509

[17] Certificate Signing Request: https://en.wikipedia.org/wiki/Certificate\_signing\_request

[18] Let's encrypt: https://letsencrypt.org/

[19] Cellar: https://github.com/tyrchen/cellar

[20] BIP 32: HD wallet: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki

[21] mobc-tonic:https://github.com/tyrchen/mobc-tonic

[22] Rustls: https://github.com/ctz/rustls

[23] Rustls audit report: https://github.com/ctz/rustls/blob/master/audit/TLS-01-report.pdf

[24] Tokio TLS helper: https://github.com/tyrchen/tokio-tls-helper

[25] WireGuard: https://www.wireguard.com/

[26]snow: https://github.com/mcginty/snow

[27] libp2p: https://github.com/libp2p/rust-libp2p