透過 Rust 探索系統的本原:安全篇
安全是我的老本行,隔一段時間不拉出來談一談就不舒服。我個人覺得:做應用不談安全都是在耍流氓。
按照 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;
use x25519_dalek::{EphemeralSecret, PublicKey};
let alice_secret = EphemeralSecret::new(OsRng);
let alice_public = PublicKey::from(&alice_secret);
let bob_secret = EphemeralSecret::new(OsRng);
let bob_public = PublicKey::from(&bob_secret);
let alice_shared_secret = alice_secret.diffie_hellman(&bob_public);
let bob_shared_secret = bob_secret.diffie_hellman(&alice_public);
assert_eq!(alice_shared_secret.as_bytes(), bob_shared_secret.as_bytes());
你也許會問:我又不去實現 TLS 或者類似的加密協議,而我自己的網絡傳輸都靠 TLS 保護着呢,DH 對我來說有什麼用呢?
我能想到的一個場景是文件加密。在本文開頭,我說:
進一步的,如果我們用戶的私有 repo 下的所有事件日誌都只有用戶自己才能訪問,其他人(包括服務端應用)都無法訪問,那麼我們可以用用戶的公鑰來加密 repo 的所有事件日誌。
這個方案的缺點是效率太低 — 如果需要加密的文件有幾個 G,非對稱加密顯然不好。但我們又沒法用對稱加密:畢竟我們不能跟每個人都預先共享一組密鑰(共享密鑰本身又存在安全風險)。這時,我們可以用 DH 算法生成一個只對這個文件有效的密鑰,加密文件,然後在文件頭提供必要的信息即可:
-
生成一個臨時公鑰對
-
用私鑰和用戶的公鑰算 DH key
-
用 DH key 作爲 AES[8] 或者 ChachaPoly[9] 的密鑰,加密文件
-
在文件頭添加臨時生成的公鑰
這樣,在解密端,用戶可以用自己的私鑰和文件中攜帶的公鑰算出 DH key,然後解密之。
如果大家對這個思路感興趣,可以參考我用 Noise protocol 做的類似的解決方案:tyrchen/conceal[10]。
除了 x25519-dalek 外,ristretto255-dh[11] 也值得一試,它是 zcash 實現的 Ristretto255[12] DH 算法。
TLS:不僅僅是 HTTP 的安全防線
作爲 HTTPS 的安全協議的唯一選擇,相信大家對 TLS(以及它的前身 SSL)有一定的瞭解 — 起碼知道:
-
服務端需要安裝經過合法 CA 簽署的證書(certificate)。如果你配過 nginx,你還會知道,證書和證書的私鑰一般都是 PEM [13] 格式存儲在文件系統的。一般來說,除了配置自己的證書外,還需要配置整個服務器證書鏈以便客戶端驗證。
-
客戶端在連接服務器時,會獲取服務器證書,然後通過操作系統受信的根證書來驗證服務器的證書以及簽署該證書的 CA,以及簽署該 CA 的上一級 CA 等形成的整個證書鏈可以由某個根證書驗證。
-
客戶端驗證了服務器的證書後,會跟服務器交互建立一個安全信道,保證之後的傳輸的安全。這個過程是 TLS 握手。
-
之後整個 HTTP 協議交互的過程,都被這個安全信道保護起來(說人話就是加密了),第三方無法嗅探到內部的交互,也無法破壞其完整性。
如果你經常調試(或者逆向工程)HTTPS,你大概還知道:
-
通過 Charles Proxy 這樣的工具,可以做 Man-In-The-Middle[14],來截獲加密的數據。使用 Charles Proxy 時,需要在操作系統級「信任」其根證書,這是證書驗證的流程所決定的。一旦某個根證書被系統信任,那麼它可以簽署任何域名的證書,這樣第三方就可以僞裝成目標服務器,terminate 客戶端到服務器的任何 TLS 流量,然後再僞裝成客戶端,向實際服務器發送數據。所以,不要輕易信任來路不明的根證書。
-
如果要避免 Charles Proxy 等工具做 Man-In-The-Middle,你可以使用 certificate pinning。
你大概率不知道:
-
TLS 支持 client certificate - 也就是說不光客戶端可以驗證服務器是否是我要連的服務器;服務器也可以驗證客戶端是否是我(的 CA)簽署的客戶端。
-
客戶端驗證服務器時,除了可以通過系統的根證書驗證,也可以預置一個 CA 證書來驗證服務器的證書是否由該 CA 簽署。
證書是個什麼鬼?
我們這裏所說的證書,是 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: FALSE
和 TLS 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"
[cert]
pem = """-----BEGIN CERTIFICATE-----
MIIBeTCCASugAwIBAgIBKjAFBgMrZXAwNzELMAkGA1UEBgwCVVMxFDASBgNVBAoM
C0RvbWFpbiBJbmMuMRIwEAYDVQQDDAlEb21haW4gQ0EwHhcNMjEwMzE0MTg0NTU2
WhcNMzEwMzEyMTg0NTU2WjA3MQswCQYDVQQGDAJVUzEUMBIGA1UECgwLRG9tYWlu
IEluYy4xEjAQBgNVBAMMCURvbWFpbiBDQTAqMAUGAytlcAMhAAzhorM9IPsXjBTx
ZxykGl5xZrsj3X2XqKjaAVutnf7po1wwWjAUBgNVHREEDTALgglsb2NhbGhvc3Qw
HQYDVR0OBBYEFD+NqChBZDOs5FMgefHJSIWiRTHXMBIGA1UdEwEB/wQIMAYBAf8C
ARAwDwYDVR0PAQH/BAUDAwcGADAFBgMrZXADQQA9sligQcYGaBqTxR1+JadSelMK
Wp35+yhVVuu4PTL18kWdU819w3cVlRe/GHt+jjlbk1i22TvfO5AaNmdxySkO
-----END CERTIFICATE-----"""
[identity]
key = """-----BEGIN PRIVATE KEY-----
MFMCAQEwBQYDK2VwBCIEIArjJtHm3xb4aX3fsGHpuB8dD3yzcLxWcPCqy7AGtTG5
oSMDIQD/38MZBnYuyitIGU3ltOGwwDwtB6KYag4rL1zsSGTzYg==
-----END PRIVATE KEY-----"""
[identity.cert]
pem = """-----BEGIN CERTIFICATE-----
MIIBZDCCARagAwIBAgIBKjAFBgMrZXAwNzELMAkGA1UEBgwCVVMxFDASBgNVBAoM
C0RvbWFpbiBJbmMuMRIwEAYDVQQDDAlEb21haW4gQ0EwHhcNMjEwMzE0MTg0NTU2
WhcNMjEwOTEwMTg0NTU2WjAyMQswCQYDVQQGDAJVUzEQMA4GA1UECgwHYW5kcm9p
ZDERMA8GA1UEAwwIYWJjZDEyMzQwKjAFBgMrZXADIQD/38MZBnYuyitIGU3ltOGw
wDwtB6KYag4rL1zsSGTzYqNMMEowFAYDVR0RBA0wC4IJbG9jYWxob3N0MBMGA1Ud
JQQMMAoGCCsGAQUFBwMCMAwGA1UdEwQFMAMBAQAwDwYDVR0PAQH/BAUDAwfgADAF
BgMrZXADQQCKhph1Z3g6E+EULUi5yIROSXmMxWjzi+L1OmqNh9ANJlrQwlcfwq0G
8JbfGVwq1sotEI83mv42mWkSSX98uysO
-----END CERTIFICATE-----"""
有了這個配置,客戶端可以生成 ClientTlsConfig
,然後生成 connector
,在建立好 TCP stream 後,直接調用 connector.connect(stream)
就可以將 TCP 連接升級爲 TLS 連接,之後可以在其之上進行應用層的協議:
let msg = b"Hello world\n";
let mut buf = [0; 12];
let config: ClientTlsConfig = toml::from_str(config_file).unwrap();
let connector = config.tls_connector(Uri::from_static("localhost")).unwrap();
let stream = TcpStream::connect(addr).await.unwrap();
let mut stream = connector.connect(stream).await.unwrap();
info!("client: TLS conn established");
stream.write_all(msg).await.unwrap();
info!("client: send data");
let (mut reader, _writer) = split(stream);
reader.read_exact(buf).await.unwrap();
info!("client: read echoed data");
服務端的使用也很簡單:配置相同的 CA cert,以及服務器的 server/key:
[identity]
key = """-----BEGIN PRIVATE KEY-----
MFMCAQEwBQYDK2VwBCIEII0kozd0PJsbNfNUS/oqI/Q/enDiLwmdw+JUnTLpR9xs
oSMDIQAtkhJiFdF9SYBIMcLikWPRIgca/Rz9ngIgd6HuG6HI3g==
-----END PRIVATE KEY-----"""
[identity.cert]
pem = """-----BEGIN CERTIFICATE-----
MIIBazCCAR2gAwIBAgIBKjAFBgMrZXAwNzELMAkGA1UEBgwCVVMxFDASBgNVBAoM
C0RvbWFpbiBJbmMuMRIwEAYDVQQDDAlEb21haW4gQ0EwHhcNMjEwMzE0MTg0NTU2
WhcNMjIwMzE0MTg0NTU2WjA5MQswCQYDVQQGDAJVUzEUMBIGA1UECgwLRG9tYWlu
IEluYy4xFDASBgNVBAMMC0dSUEMgU2VydmVyMCowBQYDK2VwAyEALZISYhXRfUmA
SDHC4pFj0SIHGv0c/Z4CIHeh7huhyN6jTDBKMBQGA1UdEQQNMAuCCWxvY2FsaG9z
dDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMEBTADAQEAMA8GA1UdDwEB/wQF
AwMH4AAwBQYDK2VwA0EAy7EOIZp73XtcqaSopqDGWU7Umi4DVvIgjmY6qbJZP0sj
ExGdaVq/7MOlZl1I+vY7G0NSZWZIUilX0CoOkrn0DA==
-----END CERTIFICATE-----"""
[client_ca_root]
pem = """-----BEGIN CERTIFICATE-----
MIIBeTCCASugAwIBAgIBKjAFBgMrZXAwNzELMAkGA1UEBgwCVVMxFDASBgNVBAoM
C0RvbWFpbiBJbmMuMRIwEAYDVQQDDAlEb21haW4gQ0EwHhcNMjEwMzE0MTg0NTU2
WhcNMzEwMzEyMTg0NTU2WjA3MQswCQYDVQQGDAJVUzEUMBIGA1UECgwLRG9tYWlu
IEluYy4xEjAQBgNVBAMMCURvbWFpbiBDQTAqMAUGAytlcAMhAAzhorM9IPsXjBTx
ZxykGl5xZrsj3X2XqKjaAVutnf7po1wwWjAUBgNVHREEDTALgglsb2NhbGhvc3Qw
HQYDVR0OBBYEFD+NqChBZDOs5FMgefHJSIWiRTHXMBIGA1UdEwEB/wQIMAYBAf8C
ARAwDwYDVR0PAQH/BAUDAwcGADAFBgMrZXADQQA9sligQcYGaBqTxR1+JadSelMK
Wp35+yhVVuu4PTL18kWdU819w3cVlRe/GHt+jjlbk1i22TvfO5AaNmdxySkO
-----END CERTIFICATE-----"""
服務端同樣可以通過配置生成 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();
let acceptor = config.tls_acceptor().unwrap();
let listener = TcpListener::bind(addr).await.unwrap();
tokio::spawn(async move {
loop {
let (stream, peer_addr) = listener.accept().await.unwrap();
let stream = acceptor.accept(stream).await.unwrap();
info!("server: Accepted client conn with TLS");
let fut = async move {
let (mut reader, mut writer) = split(stream);
let n = copy(&mut reader, &mut writer).await?;
writer.flush().await?;
debug!("Echo: {} - {}", peer_addr, n);
}
tokio::spawn(async move {
if let Err(err) = fut.await {
error!("{:?}", err);
}
});
}
});
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 的主要區別有兩個:
-
在身份驗證方面二者走上了不同的道路(TLS 1.3 使用證書,而 Noise Protocol 完全不使用)
-
通訊過程中使用的算法一個走協商(TLS)一個走預配置(Noise)
走協商還是走配置這跟協議的使用場景有關。TLS 關注的是大量不同版本的標準客戶端(比如 Firefox)和服務器之間的交互,兩端支持的算法族可能有不小的出入,協商出雙方都能接受的算法是最優的選擇,這樣可以支持儘可能廣的應用場景;而 Noise Protocol 關注的是定製的客戶端和服務器之間的交互,因而兩端可以通過預配置來確定使用的算法。比如 WireGuard 使用 Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s
,那麼客戶端和服務端都需要:
-
Curve 25519 做 ECDH
-
CharChaPoly 做加密
-
Blake2s 做哈希
-
兩端使用 pre-shared key 進一步保證安全性
因爲避免使用證書這樣天然具有中心化的東西,Noise Protocol 在 p2p 領域走出了新的天地。從最簡單的 NN(雙方都沒有固定公鑰)KK(雙方都知道對方的固定公鑰),到最複雜的 XX(雙方都有固定公鑰,通過網絡加密傳輸給對方),Noise Protocol 定義了 12 種協商模式,再輔以不同的哈希和加密算法,可以組合出上百種安全方案,總有一種適合你:
在 Rust 下,snow[26] 是 Noise Protocol 的非常成熟的實現,而 libp2p 則使用了 snow 來實現 libp2p 協議的安全層。
下面是使用 snow 在 TCP 協議之上建立加密信道的實例。我們可以看到,僅需額外的幾行代碼就可以將你的網絡應用打造得非常安全:
-
創建 snow Builder
-
在建立連接後發送和接收不超過 3 個 Noise protocol 協議報文
-
協議握手完成後,使用
noise.into_transport_mode()
將 snow 狀態機切換到傳輸模式 -
之後收到報文後調用
noise.read_message()
解密,發送報文前調用noise.write_message()
加密即可。
服務器:
let params: NoiseParams = "Noise_XX_25519_ChaChaPoly_BLAKE2s".parse().unwrap();
let builder: Builder<'_> = Builder::new(params.clone());
let static_key = builder.generate_keypair().unwrap().private;
let mut noise = builder
.local_private_key(&static_key)
.build_responder()
.unwrap();
// wait on client's arrival
println!("Listening on 0.0.0.0:9999");
let (mut stream, _) = TcpListener::bind("0.0.0.0:9999").unwrap().accept().unwrap();
// <- e
noise
.read_message(&recv(&mut stream).unwrap(), &mut buf)
.unwrap();
// -> e, ee, s, es
let len = noise.write_message(&[0u8; 0], &mut buf).unwrap();
send(&mut stream, &buf[..len]);
// <- s, se
noise
.read_message(&recv(&mut stream).unwrap(), &mut buf)
.unwrap();
// transition the state machine to transport mode sinc handshake is complete.
let mut noise = noise.into_transport_mode().unwrap();
while let Ok(msg) = recv(&mut stream) {
let len = noise.read_message(&msg, &mut buf).unwrap();
println!("client said: {}", String::from_utf8_lossy(&buf[..len]));
}
println!("connection closed");
客戶端:
let params: NoiseParams = "Noise_XX_25519_ChaChaPoly_BLAKE2s".parse().unwrap();
let builder: Builder<'_> = Builder::new(params.clone());
let static_key = builder.generate_keypair().unwrap().private;
let mut noise = builder
.local_private_key(&static_key)
.build_initiator()
.unwrap();
// connect to server
let mut stream = TcpStream::connect("127.0.0.1:9999").unwrap();
println!("connected!");
// -> e
let len = noise.write_message(&[], &mut buf).unwrap();
send(&mut stream, &buf[..len]);
// <- e, ee, s, es
noise
.read_message(&recv(&mut stream).unwrap(), &mut buf)
.unwrap();
// -> s, se
let len = noise.write_message(&[], &mut buf).unwrap();
send(&mut stream, &buf[..len]);
let mut noise = noise.into_transport_mode().unwrap();
println!("Session established...");
// send secure data
for _ in 0..10 {
let len = noise.write_message(b"HACK THE PLANET", &mut buf).unwrap();
send(&mut stream, &buf[..len]);
}
因爲 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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/HCHYr5sWnEG_qOGE3hfNnQ