How webtor-rs Uses Browser SubtleCrypto APIs for TLS: A Deep Dive into SubtleTLS

webtor-rs delegates all cryptographic operations to the browser's native SubtleCrypto API through SubtleTLS, enabling TLS 1.3 in WebAssembly without native dependencies.

webtor-rs is a full Tor client implementation that runs both natively and in the browser via WebAssembly. When targeting wasm32-unknown-unknown, the project replaces the standard rustls dependency with SubtleTLS, a custom TLS implementation that uses browser SubtleCrypto APIs for TLS to perform every cryptographic operation. This design allows the Tor client to establish secure HTTPS connections in browsers without requiring native libraries like ring or BoringSSL.

Compile-Time Transport Selection

The integration begins in webtor/src/http.rs, where conditional compilation selects the appropriate TLS backend. For native targets, the code uses rustls, but the #[cfg(target_arch = "wasm32")] block instantiates a TlsConnector from the subtle-tls crate.

In webtor/src/http.rs lines 199-210, the WASM-specific branch configures the connector:

#[cfg(target_arch = "wasm32")]
{
    let connector = subtle_tls::TlsConnector::with_config(
        subtle_tls::TlsConfig {
            skip_verification: false,
            alpn_protocols: vec!["http/1.1".into()],
            version: subtle_tls::TlsVersion::Tls13,
        }
    );
    // Handshake and stream creation follow...
}

This compile-time switch ensures that the same high-level HTTP client API works across platforms while using browser-native cryptography in WASM builds.

SubtleTLS Architecture and SubtleCrypto Integration

SubtleTLS is structured as a layered TLS implementation where the protocol state machine runs in Rust, but every cryptographic primitive is delegated to the browser's window.crypto.subtle object.

Public API and Configuration

The main entry points are defined in subtle-tls/src/lib.rs lines 52-71. The TlsConnector struct accepts a TlsConfig that specifies the TLS version, ALPN protocols, and verification settings. When TlsConnector::connect is called (lines 62-70), it creates a TlsStream that will drive the handshake.

Cryptographic Primitive Delegation

The bridge to the browser's SubtleCrypto API lives in subtle-tls/src/crypto.rs lines 21-90. This module provides Rust wrappers around JavaScript cryptographic operations:

  • get_subtle_crypto() obtains the SubtleCrypto object from globalThis.crypto.subtle
  • EcdhKeyPair::generate() (lines 65-88) uses subtle.generateKey to create P-256 ECDH key pairs
  • derive_shared_secret calls subtle.deriveBits to compute the ECDH shared secret
  • AES-GCM operations use subtle.encrypt and subtle.decrypt for record layer protection

Handshake State Machine

The TLS 1.3 handshake logic resides in subtle-tls/src/stream.rs lines 52-78. This code constructs the ClientHello, processes the ServerHello, and handles EncryptedExtensions and Certificate messages. Throughout this process, it calls into the crypto module for ECDH key generation and shared secret derivation.

Certificate Verification

X.509 certificate parsing and signature verification occur in subtle-tls/src/cert.rs lines 4-15. The code uses the x509-parser crate to parse certificates, then verifies signatures by calling subtle.verify through the crypto wrapper, ensuring browser-compatible certificate validation without native crypto libraries.

The TLS Handshake Workflow with SubtleCrypto

When a WASM-compiled webtor client makes an HTTPS request, the following sequence occurs:

  1. Transport Selection: HttpClient::request detects HTTPS and enters the #[cfg(target_arch = "wasm32")] branch in webtor/src/http.rs, creating a TlsConnector with TlsVersion::Tls13.

  2. Key Generation: The TlsStream::connect method generates an ECDH key pair by calling EcdhKeyPair::generate(), which invokes subtle.generateKey in the browser.

  3. Shared Secret Derivation: Upon receiving the server's key share, the client calls derive_shared_secret, which uses subtle.deriveBits to compute the ECDH premaster secret.

  4. Key Schedule: The handshake code derives TLS 1.3 traffic keys using HKDF-SHA256 implemented in Rust, feeding in the SubtleCrypto-derived shared secret.

  5. Record Encryption: The RecordLayer encrypts application data using AES-GCM via subtle.encrypt, and decrypts server responses via subtle.decrypt.

  6. Certificate Validation: Server certificates are parsed with x509-parser and signatures verified using subtle.verify before the handshake completes.

This workflow allows the Tor client to establish secure connections using only browser-native cryptographic primitives, eliminating the need for ring or other native libraries in WASM builds.

Practical Code Examples

Example 1: HTTPS Request via webtor Client

The following example demonstrates making an HTTPS request from a WASM-compiled webtor client. The SubtleCrypto integration happens automatically when targeting wasm32-unknown-unknown.

// In a WASM environment (e.g., Vite demo)
use webtor::client::TorClient;
use webtor::http::{HttpClient, HttpRequest};

#[wasm_bindgen(start)]
pub async fn start() -> Result<(), JsValue> {
    // Build a Tor client (default configuration)
    let tor = TorClient::new().await?;
    let http = HttpClient::new(&tor)?;

    // Perform an HTTPS GET – SubtleCrypto is used automatically
    let resp = http.get("https://example.com/").await?;
    web_sys::console::log_1(&format!("Status: {}", resp.status).into());

    Ok(())
}

The http.get call resolves to the WASM TLS branch in webtor/src/http.rs lines 199-210, which instantiates the SubtleTLS connector.

Example 2: Direct SubtleTLS Usage with Custom Transport

For applications requiring direct TLS over a custom stream (such as a WebSocket), SubtleTLS can be used independently of the high-level HTTP client.

use subtle_tls::{TlsConnector, TlsConfig, TlsVersion};
use futures::io::{AsyncReadExt, AsyncWriteExt};

// `stream` could be a WebSocket or any async transport that works in WASM
async fn tls_over_stream<S>(mut stream: S, host: &str) -> Result<(), Box<dyn std::error::Error>>
where
    S: futures::io::AsyncRead + futures::io::AsyncWrite + Unpin,
{
    // Configure the connector (TLS 1.3, ALPN set to HTTP/1.1)
    let config = TlsConfig {
        skip_verification: false,
        alpn_protocols: vec!["http/1.1".into()],
        version: TlsVersion::Tls13,
    };
    let connector = TlsConnector::with_config(config);

    // Perform the handshake – all crypto goes through SubtleCrypto
    let mut tls = connector.connect(stream, host).await?;

    // Send a simple HTTP request over the encrypted channel
    tls.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
        .await?;
    tls.flush().await?;

    // Read the response
    let mut buf = vec![0u8; 4096];
    let n = tls.read(&mut buf).await?;
    web_sys::console::log_1(&format!("Received {} bytes", n).into());

    Ok(())
}

The TlsConnector::connect method creates a TlsStream that internally delegates to SubtleCrypto, as implemented in subtle-tls/src/lib.rs lines 62-70.

Example 3: Low-Level ECDH Key Generation

The following example shows the direct use of the cryptographic wrapper to generate an ECDH key pair using the browser's SubtleCrypto API.

use subtle_tls::crypto::EcdhKeyPair;

async fn demo_ecdh() -> Result<(), Box<dyn std::error::Error>> {
    // This function runs entirely inside the browser's SubtleCrypto layer
    let keypair = EcdhKeyPair::generate().await?;
    web_sys::console::log_1(
        &format!("Public key (hex): {}", hex::encode(&keypair.public_key_bytes)).into(),
    );
    Ok(())
}

The generation logic is implemented in subtle-tls/src/crypto.rs lines 65-88, which wraps subtle.generateKey and handles the JavaScript interop.

Summary

  • webtor-rs uses conditional compilation in webtor/src/http.rs to select SubtleTLS for WASM targets while retaining rustls for native builds.
  • SubtleTLS implements a full TLS 1.3 client stack in Rust, but delegates every cryptographic primitive to the browser's SubtleCrypto API.
  • Key operations including ECDH key generation, shared secret derivation, AES-GCM encryption, and X.509 signature verification are performed via window.crypto.subtle.
  • This architecture allows the Tor client to run in any modern browser without native dependencies, leveraging FIPS-compliant browser cryptography for secure connections.

Frequently Asked Questions

How does webtor-rs handle TLS when compiled to WebAssembly?

When targeting wasm32-unknown-unknown, webtor-rs replaces the native rustls crate with SubtleTLS. In webtor/src/http.rs lines 199-210, the code detects the WASM architecture and instantiates a TlsConnector from the subtle-tls crate, which uses the browser's SubtleCrypto API for all cryptographic operations.

What specific SubtleCrypto operations does SubtleTLS use?

SubtleTLS delegates the following operations to window.crypto.subtle: ECDH key pair generation via generateKey, shared secret derivation via deriveBits, AES-GCM encryption/decryption via encrypt and decrypt, and X.509 signature verification via verify. These are implemented in subtle-tls/src/crypto.rs lines 21-90.

Is SubtleTLS limited to TLS 1.3?

No, SubtleTLS supports both TLS 1.2 and TLS 1.3. The version is configurable via TlsConfig when creating the TlsConnector. However, the implementation in subtle-tls/src/stream.rs primarily focuses on the TLS 1.3 handshake state machine for modern browser compatibility.

Can SubtleTLS be used independently of the Tor client?

Yes, SubtleTLS is a standalone crate within the webtor-rs repository. You can use TlsConnector and TlsStream directly with any async transport that implements AsyncRead and AsyncWrite, as demonstrated in the direct usage example above. This makes it suitable for any Rust project targeting WebAssembly that requires TLS connections.

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 →