12. 用Rust手把手編寫一個wmproxy(代理,內網穿透等), TLS的雙向認證資訊及token驗證

問蒙服務框架發表於2023-10-14

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單向驗證

單向通訊的示意圖如下

sequenceDiagram Client->>Server: Client Hello Client->>Server: 包含SSL/TLS版本,對稱加密演演算法列表,隨機數A Server-->>Client: Server Hello,服務端先進行選擇 Server-->>Client: 雙方都支援的SSL/TLS協議版本,對稱加密演演算法 Server-->>Client: 公鑰證書,服務端生成的隨機數B Server-->>Client: Change Cipher Spec,收到這訊息後開始密文傳輸 Client-)Client: 驗證證書,是否過期,是否被吊銷,是否可信,域名是否一致 Client->>Server: Change Cipher Spec Client->>Server: 應用資料(客戶端加密) Server-->>Client: 應用資料(服務端加密)

雙向通訊的示意圖如下,差別

sequenceDiagram Client->>Server: Client Hello Server-->>Client: Server Hello rect rgba(0, 0, 255, 0.5) Server-->>Client: 額外要求客戶端提供客戶端證書 end Client-)Client: 驗證證書 rect rgba(0, 0, 255, 0.5) Client-->>Server: 客戶端證書 Client-->>Server: 客戶端證書驗證資訊(CertificateVerify message) Server->Server: 驗證客戶端證書是否有效 Server->Server: 驗證客戶端證書驗證訊息的簽名是否有效 end Server-->>Client: 握手結束 Client->>Server: 握手結束

備註:客戶端將之前所有收到的和傳送的訊息組合起來,並用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)
}

服務端處理

如果服務端啟動的時候配置了usernamepassword則表示他需要密碼驗證,

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;
}

認證透過後訊息處理和之前的一樣,驗證流程完成

相關文章