Skip to content

Commit e8d5e3a

Browse files
jsturtevantbadeendalexcrichton
authored
Initial implementation of Wasi-tls (Transport Layer Security) (#10249)
* Initial implementation of wasi-tls This crate provides the Wasmtime host implementation for the [wasi-tls] API. The [wasi-tls] world allows WebAssembly modules to perform SSL/TLS operations, such as establishing secure connections to servers. TLS often relies on other wasi networking systems to provide the stream so it will be common to enable the [wasi:cli] world as well with the networking features enabled. The initial implemntation is using rustls. Signed-off-by: James Sturtevant <jsturtevant@gmail.com> * Remove configuration object for now Signed-off-by: James Sturtevant <jstur@microsoft.com> * Update cargo patch to use temp branch Signed-off-by: James Sturtevant <jstur@microsoft.com> * Rename tcp streams to wasistreams to be more generic Signed-off-by: James Sturtevant <jstur@microsoft.com> * gate the wasi-tls ctx behind a feature Signed-off-by: James Sturtevant <jstur@microsoft.com> * cleanup and clippy fixes Signed-off-by: James Sturtevant <jstur@microsoft.com> * Fix issue when another pollable cancels Signed-off-by: James Sturtevant <jstur@microsoft.com> * prtest:full Signed-off-by: James Sturtevant <jstur@microsoft.com> * Skip test on riscv64/s390x Signed-off-by: James Sturtevant <jstur@microsoft.com> * Drop debug info to support tests on pulley based platforms Signed-off-by: James Sturtevant <jstur@microsoft.com> * Update signature of `close-notify` * Use draft version Signed-off-by: James Sturtevant <jstur@microsoft.com> * Remove patches Signed-off-by: James Sturtevant <jstur@microsoft.com> * Ungate tls on riscv64 and s390x * Un-gate wais-http on riscv64/s390x as well * Add wasmtime-wasi-tls to publish list * Add wasmtime-wasi-tls to public API crate list * Revert some changes to Cargo.lock --------- Signed-off-by: James Sturtevant <jsturtevant@gmail.com> Signed-off-by: James Sturtevant <jstur@microsoft.com> Co-authored-by: badeend <github@davebakker.io> Co-authored-by: Alex Crichton <alex@alexcrichton.com>
1 parent 665c098 commit e8d5e3a

File tree

21 files changed

+1374
-60
lines changed

21 files changed

+1374
-60
lines changed

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ wasi-common = { workspace = true, default-features = true, features = ["exit", "
5454
wasmtime-wasi = { workspace = true, default-features = true, optional = true }
5555
wasmtime-wasi-nn = { workspace = true, optional = true }
5656
wasmtime-wasi-config = { workspace = true, optional = true }
57+
wasmtime-wasi-tls = { workspace = true, optional = true }
5758
wasmtime-wasi-keyvalue = { workspace = true, optional = true }
5859
wasmtime-wasi-threads = { workspace = true, optional = true }
5960
wasmtime-wasi-http = { workspace = true, optional = true }
@@ -245,6 +246,7 @@ wasmtime-component-macro = { path = "crates/component-macro", version = "=32.0.0
245246
wasmtime-asm-macros = { path = "crates/asm-macros", version = "=32.0.0" }
246247
wasmtime-versioned-export-macros = { path = "crates/versioned-export-macros", version = "=32.0.0" }
247248
wasmtime-slab = { path = "crates/slab", version = "=32.0.0" }
249+
wasmtime-wasi-tls = { path = "crates/wasi-tls", version = "32.0.0" }
248250
component-test-util = { path = "crates/misc/component-test-util" }
249251
component-fuzz-util = { path = "crates/misc/component-fuzz-util" }
250252
wiggle = { path = "crates/wiggle", version = "=32.0.0", default-features = false }
@@ -377,6 +379,9 @@ libtest-mimic = "0.7.0"
377379
semver = { version = "1.0.17", default-features = false }
378380
ittapi = "0.4.0"
379381
libm = "0.2.7"
382+
tokio-rustls = "0.25.0"
383+
rustls = "0.22.0"
384+
webpki-roots = "0.26.0"
380385

381386
# =============================================================================
382387
#
@@ -409,6 +414,7 @@ default = [
409414
"wasi-http",
410415
"wasi-config",
411416
"wasi-keyvalue",
417+
"wasi-tls",
412418

413419
# Most features of Wasmtime are enabled by default.
414420
"wat",
@@ -459,6 +465,7 @@ disable-logging = ["log/max_level_off", "tracing/max_level_off"]
459465
# These features are all included in the `default` set above and this is
460466
# the internal mapping for what they enable in Wasmtime itself.
461467
wasi-nn = ["dep:wasmtime-wasi-nn"]
468+
wasi-tls = ["dep:wasmtime-wasi-tls"]
462469
wasi-threads = ["dep:wasmtime-wasi-threads", "threads"]
463470
wasi-http = ["component-model", "dep:wasmtime-wasi-http", "dep:tokio", "dep:hyper"]
464471
wasi-config = ["dep:wasmtime-wasi-config"]
@@ -567,3 +574,4 @@ opt-level = 's'
567574
inherits = "release"
568575
codegen-units = 1
569576
lto = true
577+

crates/cli-flags/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,8 @@ wasmtime_option_group! {
414414
/// Grant access to the given TCP listen socket
415415
#[serde(default)]
416416
pub tcplisten: Vec<String>,
417+
/// Enable support for WASI TLS (Transport Layer Security) imports (experimental)
418+
pub tls: Option<bool>,
417419
/// Implement WASI Preview1 using new Preview2 implementation (true, default) or legacy
418420
/// implementation (false)
419421
pub preview2: Option<bool>,

crates/test-programs/artifacts/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ fn build_and_generate_tests() {
7878
s if s.starts_with("dwarf_") => "dwarf",
7979
s if s.starts_with("config_") => "config",
8080
s if s.starts_with("keyvalue_") => "keyvalue",
81+
s if s.starts_with("tls_") => "tls",
8182
// If you're reading this because you hit this panic, either add it
8283
// to a test suite above or add a new "suite". The purpose of the
8384
// categorization above is to have a static assertion that tests
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use core::str;
2+
3+
use test_programs::wasi::sockets::network::{IpSocketAddress, Network};
4+
use test_programs::wasi::sockets::tcp::{ShutdownType, TcpSocket};
5+
use test_programs::wasi::tls::types::ClientHandshake;
6+
7+
fn test_tls_sample_application() {
8+
const PORT: u16 = 443;
9+
const DOMAIN: &'static str = "example.com";
10+
11+
let request = format!("GET / HTTP/1.1\r\nHost: {DOMAIN}\r\n\r\n");
12+
13+
let net = Network::default();
14+
15+
let Some(ip) = net
16+
.permissive_blocking_resolve_addresses(DOMAIN)
17+
.unwrap()
18+
.first()
19+
.map(|a| a.to_owned())
20+
else {
21+
eprintln!("DNS lookup failed.");
22+
return;
23+
};
24+
25+
let socket = TcpSocket::new(ip.family()).unwrap();
26+
let (tcp_input, tcp_output) = socket
27+
.blocking_connect(&net, IpSocketAddress::new(ip, PORT))
28+
.unwrap();
29+
30+
let (client_connection, tls_input, tls_output) =
31+
ClientHandshake::new(DOMAIN, tcp_input, tcp_output)
32+
.blocking_finish()
33+
.unwrap();
34+
35+
tls_output.blocking_write_util(request.as_bytes()).unwrap();
36+
client_connection
37+
.blocking_close_output(&tls_output)
38+
.unwrap();
39+
socket.shutdown(ShutdownType::Send).unwrap();
40+
let response = tls_input.blocking_read_to_end().unwrap();
41+
let response = String::from_utf8(response).unwrap();
42+
43+
assert!(response.contains("HTTP/1.1 200 OK"));
44+
}
45+
46+
fn main() {
47+
test_tls_sample_application();
48+
}

crates/test-programs/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod http;
22
pub mod nn;
33
pub mod preview1;
44
pub mod sockets;
5+
pub mod tls;
56

67
wit_bindgen::generate!({
78
inline: "
@@ -12,15 +13,17 @@ wit_bindgen::generate!({
1213
include wasi:http/imports@0.2.3;
1314
include wasi:config/imports@0.2.0-draft;
1415
include wasi:keyvalue/imports@0.2.0-draft;
16+
include wasi:tls/imports@0.2.0-draft;
1517
}
1618
",
1719
path: [
1820
"../wasi-http/wit",
1921
"../wasi-config/wit",
2022
"../wasi-keyvalue/wit",
23+
"../wasi-tls/wit/world.wit",
2124
],
2225
world: "wasmtime:test/test",
23-
features: ["cli-exit-with-code"],
26+
features: ["cli-exit-with-code", "tls"],
2427
generate_all,
2528
});
2629

crates/test-programs/src/tls.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use crate::wasi::clocks::monotonic_clock;
2+
use crate::wasi::io::streams::StreamError;
3+
use crate::wasi::tls::types::{ClientConnection, ClientHandshake, InputStream, OutputStream};
4+
5+
const TIMEOUT_NS: u64 = 1_000_000_000;
6+
7+
impl ClientHandshake {
8+
pub fn blocking_finish(self) -> Result<(ClientConnection, InputStream, OutputStream), ()> {
9+
let future = ClientHandshake::finish(self);
10+
let timeout = monotonic_clock::subscribe_duration(TIMEOUT_NS * 200);
11+
let pollable = future.subscribe();
12+
13+
loop {
14+
match future.get() {
15+
None => pollable.block_until(&timeout).expect("timed out"),
16+
Some(Ok(r)) => return r,
17+
Some(Err(e)) => {
18+
eprintln!("{e:?}");
19+
unimplemented!()
20+
}
21+
}
22+
}
23+
}
24+
}
25+
26+
impl ClientConnection {
27+
pub fn blocking_close_output(
28+
&self,
29+
output: &OutputStream,
30+
) -> Result<(), crate::wasi::io::error::Error> {
31+
let timeout = monotonic_clock::subscribe_duration(TIMEOUT_NS);
32+
let pollable = output.subscribe();
33+
34+
self.close_output();
35+
36+
loop {
37+
match output.check_write() {
38+
Ok(0) => pollable.block_until(&timeout).expect("timed out"),
39+
Ok(_) => unreachable!("After calling close_output, the output stream should never accept new writes again."),
40+
Err(StreamError::Closed) => return Ok(()),
41+
Err(StreamError::LastOperationFailed(e)) => return Err(e),
42+
}
43+
}
44+
}
45+
}

crates/wasi-http/Cargo.toml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,9 @@ http-body-util = { workspace = true }
2828
tracing = { workspace = true }
2929
wasmtime-wasi = { workspace = true }
3030
wasmtime = { workspace = true, features = ['component-model'] }
31-
32-
# The `ring` crate, used to implement TLS, does not build on riscv64 or s390x
33-
[target.'cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))'.dependencies]
34-
tokio-rustls = { version = "0.25.0" }
35-
rustls = { version = "0.22.0" }
36-
webpki-roots = { version = "0.26.0" }
31+
tokio-rustls = { workspace = true }
32+
rustls = { workspace = true }
33+
webpki-roots = { workspace = true }
3734

3835
[dev-dependencies]
3936
test-programs-artifacts = { workspace = true }

crates/wasi-http/src/types.rs

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -373,58 +373,48 @@ pub async fn default_send_request_handler(
373373
})?;
374374

375375
let (mut sender, worker) = if use_tls {
376-
#[cfg(any(target_arch = "riscv64", target_arch = "s390x"))]
377-
{
378-
return Err(crate::bindings::http::types::ErrorCode::InternalError(
379-
Some("unsupported architecture for SSL".to_string()),
380-
));
381-
}
376+
use rustls::pki_types::ServerName;
382377

383-
#[cfg(not(any(target_arch = "riscv64", target_arch = "s390x")))]
384-
{
385-
use rustls::pki_types::ServerName;
386-
387-
// derived from https://github.com/rustls/rustls/blob/main/examples/src/bin/simpleclient.rs
388-
let root_cert_store = rustls::RootCertStore {
389-
roots: webpki_roots::TLS_SERVER_ROOTS.into(),
390-
};
391-
let config = rustls::ClientConfig::builder()
392-
.with_root_certificates(root_cert_store)
393-
.with_no_client_auth();
394-
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config));
395-
let mut parts = authority.split(":");
396-
let host = parts.next().unwrap_or(&authority);
397-
let domain = ServerName::try_from(host)
398-
.map_err(|e| {
399-
tracing::warn!("dns lookup error: {e:?}");
400-
dns_error("invalid dns name".to_string(), 0)
401-
})?
402-
.to_owned();
403-
let stream = connector.connect(domain, tcp_stream).await.map_err(|e| {
404-
tracing::warn!("tls protocol error: {e:?}");
405-
types::ErrorCode::TlsProtocolError
406-
})?;
407-
let stream = TokioIo::new(stream);
408-
409-
let (sender, conn) = timeout(
410-
connect_timeout,
411-
hyper::client::conn::http1::handshake(stream),
412-
)
413-
.await
414-
.map_err(|_| types::ErrorCode::ConnectionTimeout)?
415-
.map_err(hyper_request_error)?;
416-
417-
let worker = wasmtime_wasi::runtime::spawn(async move {
418-
match conn.await {
419-
Ok(()) => {}
420-
// TODO: shouldn't throw away this error and ideally should
421-
// surface somewhere.
422-
Err(e) => tracing::warn!("dropping error {e}"),
423-
}
424-
});
378+
// derived from https://github.com/rustls/rustls/blob/main/examples/src/bin/simpleclient.rs
379+
let root_cert_store = rustls::RootCertStore {
380+
roots: webpki_roots::TLS_SERVER_ROOTS.into(),
381+
};
382+
let config = rustls::ClientConfig::builder()
383+
.with_root_certificates(root_cert_store)
384+
.with_no_client_auth();
385+
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(config));
386+
let mut parts = authority.split(":");
387+
let host = parts.next().unwrap_or(&authority);
388+
let domain = ServerName::try_from(host)
389+
.map_err(|e| {
390+
tracing::warn!("dns lookup error: {e:?}");
391+
dns_error("invalid dns name".to_string(), 0)
392+
})?
393+
.to_owned();
394+
let stream = connector.connect(domain, tcp_stream).await.map_err(|e| {
395+
tracing::warn!("tls protocol error: {e:?}");
396+
types::ErrorCode::TlsProtocolError
397+
})?;
398+
let stream = TokioIo::new(stream);
425399

426-
(sender, worker)
427-
}
400+
let (sender, conn) = timeout(
401+
connect_timeout,
402+
hyper::client::conn::http1::handshake(stream),
403+
)
404+
.await
405+
.map_err(|_| types::ErrorCode::ConnectionTimeout)?
406+
.map_err(hyper_request_error)?;
407+
408+
let worker = wasmtime_wasi::runtime::spawn(async move {
409+
match conn.await {
410+
Ok(()) => {}
411+
// TODO: shouldn't throw away this error and ideally should
412+
// surface somewhere.
413+
Err(e) => tracing::warn!("dropping error {e}"),
414+
}
415+
});
416+
417+
(sender, worker)
428418
} else {
429419
let tcp_stream = TokioIo::new(tcp_stream);
430420
let (sender, conn) = timeout(

crates/wasi-http/tests/all/main.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,8 +528,6 @@ async fn do_wasi_http_echo(uri: &str, url_header: Option<&str>) -> Result<()> {
528528
}
529529

530530
#[test_log::test(tokio::test)]
531-
// test uses TLS but riscv/s390x don't support that yet
532-
#[cfg_attr(any(target_arch = "riscv64", target_arch = "s390x"), ignore)]
533531
async fn wasi_http_without_port() -> Result<()> {
534532
let req = hyper::Request::builder()
535533
.method(http::Method::GET)

0 commit comments

Comments
 (0)