12. 用Rust手把手編寫一個wmproxy(代理,內網穿透等), TLS的雙向認證資訊及token驗證
專案 ++wmproxy++
gite: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
什麼是TLS雙向認證
TLS雙向認證是指客戶端和伺服器端都需要驗證對方的身份,也稱mTLS
。
在建立Https連線的過程中,握手的流程比單向認證多了幾步。
- 單向認證的過程,客戶端從伺服器端下載伺服器端公鑰證書進行驗證,然後建立安全通訊通道。
- 雙向通訊流程,客戶端除了需要從伺服器端下載伺服器的公鑰證書進行驗證外,還需要把客戶端的公鑰證書上傳到伺服器端給伺服器端進行驗證,等雙方都認證透過了,才開始建立安全通訊通道進行資料傳輸。
TLS是安全套接層(SSL)的繼任者,叫傳輸層安全(transport layer security)。說直白點,就是在明文的上層和TCP層之間加上一層加密,這樣就保證上層資訊傳輸的安全,然後解密完後又以原樣的資料回傳給應用層,做到與應用層無關,所以http加個s就成了https,ws加個s就成了wss,ftp加個s就成了ftps,都是從普通tcp傳輸轉換成tls傳輸實現安全加密,應用相當廣泛。
單向與雙向的差別
SSL單向驗證
單向通訊的示意圖如下
雙向通訊的示意圖如下,差別
備註:客戶端將之前所有收到的和傳送的訊息組合起來,並用hash演算法得到一個hash值,然後用客戶端金鑰庫的私鑰對這個hash進行簽名,這個簽名就是CertificateVerify message;
程式碼實現
將原來的rustls
中的TlsAcceptor和TlsConnector進行相應的改造,變成可支援雙向認證的加密結構。
獲取TlsAcceptor的認證
/// 獲取服務端https的證書資訊
pub async fn get_tls_accept(&mut self) -> ProxyResult<TlsAcceptor> {
if !self.tc {
return Err(ProxyError::ProtNoSupport);
}
let certs = Self::load_certs(&self.cert)?;
let key = Self::load_keys(&self.key)?;
let config = rustls::ServerConfig::builder().with_safe_defaults();
// 開始雙向認證,需要客戶端提供證書資訊
let config = if self.two_way_tls {
let mut client_auth_roots = rustls::RootCertStore::empty();
for root in &certs {
client_auth_roots.add(&root).unwrap();
}
let client_auth = rustls::server::AllowAnyAuthenticatedClient::new(client_auth_roots);
config
.with_client_cert_verifier(client_auth.boxed())
.with_single_cert(certs, key)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?
} else {
config
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?
};
let acceptor = TlsAcceptor::from(Arc::new(config));
Ok(acceptor)
}
獲取TlsAcceptor的認證
/// 獲取客戶端https的Config配置
pub async fn get_tls_request(&mut self) -> ProxyResult<Arc<rustls::ClientConfig>> {
if !self.ts {
return Err(ProxyError::ProtNoSupport);
}
let certs = Self::load_certs(&self.cert)?;
let mut root_cert_store = rustls::RootCertStore::empty();
// 信任通用的簽名商
root_cert_store.add_trust_anchors(
webpki_roots::TLS_SERVER_ROOTS
.iter()
.map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}),
);
for cert in &certs {
let _ = root_cert_store.add(cert);
}
let config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_cert_store);
if self.two_way_tls {
let key = Self::load_keys(&self.key)?;
Ok(Arc::new(config.with_client_auth_cert(certs, key).map_err(
|err| io::Error::new(io::ErrorKind::InvalidInput, err),
)?))
} else {
Ok(Arc::new(config.with_no_client_auth()))
}
}
這裡預設信任的通用的CA簽發證書平臺,像系統證書,瀏覽器信任的證書,只有第一步把基礎的被信任才有資格做簽發證書平臺。
至此雙向TLS的能力已經達成,感謝前人的經典程式碼才能如此輕鬆。
token驗證
首先先定義協議的Token結構,只有sock_map為0接收此訊息
/// 進行身份的認證
#[derive(Debug)]
pub struct ProtToken {
username: String,
password: String,
}
下面是編碼解碼,密碼要求不超過255個字元,即長度為1位元組編碼
pub fn parse<T: Buf>(_header: ProtFrameHeader, mut buf: T) -> ProxyResult<ProtToken> {
let username = read_short_string(&mut buf)?;
let password = read_short_string(&mut buf)?;
Ok(Self { username, password })
}
pub fn encode<B: Buf + BufMut>(self, buf: &mut B) -> ProxyResult<usize> {
let mut head = ProtFrameHeader::new(ProtKind::Token, ProtFlag::zero(), 0);
head.length = self.username.as_bytes().len() as u32 + 1 + self.password.as_bytes().len() as u32 + 1;
let mut size = 0;
size += head.encode(buf)?;
size += write_short_string(buf, &self.username)?;
size += write_short_string(buf, &self.password)?;
Ok(size)
}
服務端處理
如果服務端啟動的時候配置了username
及 password
則表示他需要密碼驗證,
let mut verify_succ = option.username.is_none() && option.password.is_none();
如果verify_succ
不為true,那麼我們接下來的第一條訊息必須為ProtToken
,否則客戶端不合法,關閉
收到該訊息則進行驗證
match &p {
ProtFrame::Token(p) => {
if !verify_succ
&& p.is_check_succ(&option.username, &option.password)
{
verify_succ = true;
continue;
}
}
_ => {}
}
if !verify_succ {
ProtFrame::new_close_reason(0, "not verify so close".to_string())
.encode(&mut write_buf)?;
is_ready_shutdown = true;
break;
}
認證透過後訊息處理和之前的一樣,驗證流程完成