How webtor-rs Handles TLS Connections in the Browser: Wasm-Compatible Encryption

webtor-rs delegates TLS encryption to the browser's native networking stack via the subtle-tls crate, using conditional compilation to exclude native TLS implementations when targeting wasm32.

webtor-rs is a Rust implementation of Tor networking designed to compile to WebAssembly for browser environments. When running in the browser, the library cannot rely on native TLS libraries due to WebAssembly sandbox constraints, so it leverages the subtle-tls crate to perform TLS 1.2 and 1.3 handshakes through the Web Crypto API.

Why Native TLS Fails in WebAssembly

Standard Rust TLS implementations depend on system-level cryptography libraries and std::time::Instant, which are unavailable in browser contexts. The webtor-rs codebase explicitly excludes native TLS code from WebAssembly builds using conditional compilation attributes.

In webtor/src/tls.rs, the entire native TLS implementation is gated behind #[cfg(not(target_arch = "wasm32"))]. This ensures that when compiling for wasm32-unknown-unknown, the compiler skips this file entirely, preventing linker errors and runtime incompatibilities.

The Wasm-Specific TLS Architecture

Conditional Compilation Strategy

The codebase uses Rust's conditional compilation to maintain dual TLS implementations. For native targets, webtor/src/tls.rs provides platform-specific encryption. For WebAssembly targets, the library routes all TLS operations through webtor/src/http.rs, which implements browser-compatible encryption layers.

The WasmTlsStream Abstraction

At the heart of the browser TLS implementation lies the WasmTlsStream trait, defined in webtor/src/http.rs at lines 91-98:

trait WasmTlsStream {
    async fn tls_write(&mut self, buf: &[u8]) -> std::io::Result<usize>;
    async fn tls_flush(&mut self) -> std::io::Result<()>;
    async fn tls_read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
}

This trait abstracts the asynchronous read, write, and flush operations required by TLS streams, providing a uniform interface regardless of the underlying TLS version.

subtle-tls Integration

The same file provides concrete implementations of WasmTlsStream for the subtle-tls crate's stream types. For TLS 1.3 connections, lines 102-113 implement the trait for subtle_tls::TlsStream<S>:

// Implementation for subtle_tls::TlsStream<S> (TLS 1.3)
impl<S> WasmTlsStream for subtle_tls::TlsStream<S>
where
    S: AsyncRead + AsyncWrite + Unpin,
{
    async fn tls_write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.write(buf).await
    }
    
    async fn tls_flush(&mut self) -> std::io::Result<()> {
        self.flush().await
    }
    
    async fn tls_read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.read(buf).await
    }
}

For legacy TLS 1.2 support, lines 118-130 provide an identical implementation for subtle_tls::TlsStream12<S>. Both implementations forward directly to the underlying async methods of the subtle-tls stream, which handles the cryptographic operations using the browser's Web Crypto API.

Executing HTTPS Requests in the Browser

The Request Flow

When an HTTPS request originates from a WebAssembly build, the HttpClient::request method creates a TLS connection using subtle_tls::TlsConnector::connect. The resulting stream passes to execute_http_request_wasm, defined at lines 336-378 in webtor/src/http.rs:

async fn execute_http_request_wasm<S: WasmTlsStream>(
    stream: &mut S,
    request: &Request,
) -> Result<Response, Error> {
    // Write request headers and body
    let data = format_request(request);
    stream.tls_write(data.as_bytes()).await?;
    stream.tls_flush().await?;
    
    // Read response with size limits
    let mut buf = vec![0u8; 8192];
    let n = stream.tls_read(&mut buf).await?;
    parse_response(&buf[..n])
}

For TLS 1.2 connections, the code path diverges at line 244 to execute_http_request_wasm_tls12, maintaining separate handling for legacy protocol versions while using the same WasmTlsStream abstraction.

Integration with Browser Primitives

Under the hood, subtle-tls establishes the TLS session over browser-native transports. The crate performs the handshake using the Web Crypto API for cryptographic operations, while the actual network traffic flows through the browser's fetch or WebSocket implementations. This approach avoids the std::time::Instant limitation prevalent in WebAssembly environments.

Practical Implementation Examples

Creating an HTTPS request from a wasm-compiled webtor client requires no special syntax—the library handles TLS transparently:

let client = HttpClient::new(/* Tor config */).await?;
let response = client.get("https://example.com").await?;
println!("Status: {}", response.status);
println!("Body ({} bytes)", response.body.len());

The underlying process executes these steps:

  1. Resolve the host and open a TCP-like connection via browser WebSocket or fetch primitives
  2. Call subtle_tls::TlsConnector::connect to establish a TLS 1.3 (or 1.2) session using browser cryptography
  3. Wrap the resulting subtle_tls::TlsStream as a WasmTlsStream
  4. Use execute_http_request_wasm to transmit the HTTP request and read the response

For advanced use cases requiring manual stream control:

let connector = subtle_tls::TlsConnector::new().await?;
let mut tls_stream = connector
    .connect("example.com".parse().unwrap(), WebSocket::new("wss://example.com").await?)
    .await?;

tls_stream.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").await?;
tls_stream.flush().await?;
let mut buf = vec![0u8; 8192];
let n = tls_stream.read(&mut buf).await?;
println!("{}", std::str::from_utf8(&buf[..n])?);

This example mirrors the internal steps of execute_http_request_wasm while exposing the raw subtle_tls API for custom protocols.

Key Source Files and Their Roles

Understanding the browser TLS implementation requires familiarity with these specific files in the igor53627/webtor-rs repository:

  • webtor/src/http.rs – Defines the WasmTlsStream trait, implements it for subtle-tls types, and drives TLS-enabled HTTP requests in wasm environments
  • webtor/src/tls.rs – Contains the native-only TLS implementation, excluded from browser builds via conditional compilation
  • subtle-tls/src/lib.rs – Core crate providing TlsConnector, TlsStream, and Web-Crypto-based TLS for WebAssembly
  • subtle-tls/tests/tls12_browser_tests.rs – Test suite confirming subtle-tls functionality in browser environments
  • webtor/src/snowflake_ws.rs – Demonstrates additional subtle-tls usage for WebSocket-based Snowflake bridges

Summary

  • webtor-rs uses conditional compilation to exclude native TLS code in wasm32 targets, routing browser builds through webtor/src/http.rs instead of webtor/src/tls.rs
  • The WasmTlsStream trait abstracts TLS operations for browser environments, with implementations for both TLS 1.3 and TLS 1.2 via the subtle-tls crate
  • HTTPS requests execute through execute_http_request_wasm and execute_http_request_wasm_tls12, which handle request serialization and response parsing over encrypted streams
  • Cryptographic operations rely on the Web Crypto API rather than native system libraries, ensuring compatibility with WebAssembly sandbox constraints
  • The subtle-tls crate performs handshakes while browser networking primitives (fetch/WebSocket) handle the underlying transport layer

Frequently Asked Questions

What prevents webtor-rs from using rustls or native-tls in the browser?

WebAssembly running in browsers lacks access to system-level sockets and native threading primitives. Additionally, std::time::Instant is unavailable in wasm32-unknown-unknown targets, breaking most native TLS implementations. The webtor-rs codebase explicitly excludes these via #[cfg(not(target_arch = "wasm32"))] attributes in webtor/src/tls.rs.

How does subtle-tls differ from traditional Rust TLS libraries?

subtle-tls builds TLS 1.2 and 1.3 sessions on top of the browser's Web Crypto API rather than interfacing with operating system cryptographic libraries. It delegates network I/O to browser fetch and WebSocket APIs while maintaining the standard async read/write interface exposed by the WasmTlsStream trait in webtor/src/http.rs.

Can webtor-rs negotiate both TLS 1.2 and 1.3 in browser environments?

Yes. The codebase provides separate implementations of WasmTlsStream for both subtle_tls::TlsStream (TLS 1.3) at lines 102-113 and subtle_tls::TlsStream12 (TLS 1.2) at lines 118-130 of webtor/src/http.rs. The HttpClient automatically selects the appropriate version during the handshake process.

Where does the actual TLS handshake occur when using webtor-rs in a browser?

The handshake occurs within the subtle-tls crate's TlsConnector::connect method, which utilizes the Web Crypto API for cryptographic operations. However, the underlying TCP-like connection and network transport are handled by the browser's native fetch or WebSocket implementation, as seen in webtor/src/snowflake_ws.rs and the HTTP client code paths.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →