12. 用Rust手把手编写一个wmproxy(代理,内网穿透等), TLS的双向认证信息及token验证

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的认证

rust 复制代码
/// 获取服务端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的认证

rust 复制代码
/// 获取客户端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接收此消息

rust 复制代码
/// 进行身份的认证
#[derive(Debug)]
pub struct ProtToken {
    username: String,
    password: String,
}

下面是编码解码,密码要求不超过255个字符,即长度为1字节编码

rust 复制代码
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则表示他需要密码验证,

rust 复制代码
let mut verify_succ = option.username.is_none() && option.password.is_none();

如果verify_succ不为true,那么我们接下来的第一条消息必须为ProtToken,否则客户端不合法,关闭 收到该消息则进行验证

rust 复制代码
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;
}

认证通过后消息处理和之前的一样,验证流程完成

相关推荐
安的列斯凯奇6 小时前
SpringBoot篇 单元测试 理论篇
spring boot·后端·单元测试
架构文摘JGWZ6 小时前
FastJson很快,有什么用?
后端·学习
BinaryBardC6 小时前
Swift语言的网络编程
开发语言·后端·golang
邓熙榆6 小时前
Haskell语言的正则表达式
开发语言·后端·golang
专职9 小时前
spring boot中实现手动分页
java·spring boot·后端
Ciderw9 小时前
Go中的三种锁
开发语言·c++·后端·golang·互斥锁·
m0_7482463510 小时前
SpringBoot返回文件让前端下载的几种方式
前端·spring boot·后端
m0_7482304410 小时前
创建一个Spring Boot项目
java·spring boot·后端
卿着飞翔10 小时前
Java面试题2025-Mysql
java·spring boot·后端
C++小厨神10 小时前
C#语言的学习路线
开发语言·后端·golang