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 theSubtleCryptoobject fromglobalThis.crypto.subtleEcdhKeyPair::generate()(lines 65-88) usessubtle.generateKeyto create P-256 ECDH key pairsderive_shared_secretcallssubtle.deriveBitsto compute the ECDH shared secret- AES-GCM operations use
subtle.encryptandsubtle.decryptfor 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:
-
Transport Selection:
HttpClient::requestdetects HTTPS and enters the#[cfg(target_arch = "wasm32")]branch inwebtor/src/http.rs, creating aTlsConnectorwithTlsVersion::Tls13. -
Key Generation: The
TlsStream::connectmethod generates an ECDH key pair by callingEcdhKeyPair::generate(), which invokessubtle.generateKeyin the browser. -
Shared Secret Derivation: Upon receiving the server's key share, the client calls
derive_shared_secret, which usessubtle.deriveBitsto compute the ECDH premaster secret. -
Key Schedule: The handshake code derives TLS 1.3 traffic keys using HKDF-SHA256 implemented in Rust, feeding in the SubtleCrypto-derived shared secret.
-
Record Encryption: The
RecordLayerencrypts application data using AES-GCM viasubtle.encrypt, and decrypts server responses viasubtle.decrypt. -
Certificate Validation: Server certificates are parsed with
x509-parserand signatures verified usingsubtle.verifybefore 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.rsto select SubtleTLS for WASM targets while retainingrustlsfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →