Skip to content

Commit 04b4cf4

Browse files
committed
feat(p3): implement wasi:tls
Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
1 parent ca06d47 commit 04b4cf4

File tree

22 files changed

+1490
-11
lines changed

22 files changed

+1490
-11
lines changed

Cargo.lock

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

ci/vendor-wit.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ make_vendor "wasi-tls" "
6565
tls@v0.2.0-draft+505fc98
6666
"
6767

68+
make_vendor "wasi-tls/src/p3" "
69+
tls@v0.3.0-draft@wit-0.3.0-draft
70+
"
71+
6872
make_vendor "wasi-config" "config@v0.2.0-rc.1"
6973

7074
make_vendor "wasi-keyvalue" "keyvalue@219ea36"

crates/test-programs/artifacts/build.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,20 @@ impl Artifacts {
7474
// generates a `foreach_*` macro below.
7575
let kind = match test.name.as_str() {
7676
s if s.starts_with("p1_") => "p1",
77-
s if s.starts_with("p2_http_") => "p2_http",
78-
s if s.starts_with("p2_cli_") => "p2_cli",
7977
s if s.starts_with("p2_api_") => "p2_api",
78+
s if s.starts_with("p2_cli_") => "p2_cli",
79+
s if s.starts_with("p2_http_") => "p2_http",
80+
s if s.starts_with("p2_tls_") => "p2_tls",
8081
s if s.starts_with("p2_") => "p2",
8182
s if s.starts_with("nn_") => "nn",
8283
s if s.starts_with("piped_") => "piped",
8384
s if s.starts_with("dwarf_") => "dwarf",
8485
s if s.starts_with("config_") => "config",
8586
s if s.starts_with("keyvalue_") => "keyvalue",
86-
s if s.starts_with("tls_") => "tls",
8787
s if s.starts_with("async_") => "async",
88-
s if s.starts_with("p3_http_") => "p3_http",
8988
s if s.starts_with("p3_api_") => "p3_api",
89+
s if s.starts_with("p3_http_") => "p3_http",
90+
s if s.starts_with("p3_tls_") => "p3_tls",
9091
s if s.starts_with("p3_") => "p3",
9192
s if s.starts_with("fuzz_") => "fuzz",
9293
// If you're reading this because you hit this panic, either add

crates/test-programs/src/bin/tls_sample_application.rs renamed to crates/test-programs/src/bin/p2_tls_sample_application.rs

File renamed without changes.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use anyhow::{Context as _, Result, anyhow, bail};
2+
use core::future::{Future as _, poll_fn};
3+
use core::pin::pin;
4+
use core::str;
5+
use core::task::{Poll, ready};
6+
use futures::try_join;
7+
use test_programs::p3::wasi::sockets::ip_name_lookup::resolve_addresses;
8+
use test_programs::p3::wasi::sockets::types::{IpAddress, IpSocketAddress, TcpSocket};
9+
use test_programs::p3::wasi::tls;
10+
use test_programs::p3::wasi::tls::client::Hello;
11+
use test_programs::p3::wit_stream;
12+
use wit_bindgen::StreamResult;
13+
14+
struct Component;
15+
16+
test_programs::p3::export!(Component);
17+
18+
const PORT: u16 = 443;
19+
20+
async fn test_tls_sample_application(domain: &str, ip: IpAddress) -> Result<()> {
21+
let request = format!(
22+
"GET / HTTP/1.1\r\nHost: {domain}\r\nUser-Agent: wasmtime-wasi-rust\r\nConnection: close\r\n\r\n"
23+
);
24+
25+
let sock = TcpSocket::create(ip.family()).unwrap();
26+
sock.connect(IpSocketAddress::new(ip, PORT))
27+
.await
28+
.context("tcp connect failed")?;
29+
30+
let (sock_rx, sock_rx_fut) = sock.receive();
31+
let hello = Hello::new();
32+
hello
33+
.set_server_name(domain)
34+
.map_err(|()| anyhow!("failed to set SNI"))?;
35+
let (sock_tx, conn) = tls::client::connect(hello, sock_rx);
36+
let sock_tx_fut = sock.send(sock_tx);
37+
38+
let mut conn = pin!(conn.into_future());
39+
let mut sock_rx_fut = pin!(sock_rx_fut.into_future());
40+
let mut sock_tx_fut = pin!(sock_tx_fut);
41+
let conn = poll_fn(|cx| match conn.as_mut().poll(cx) {
42+
Poll::Ready(Ok(conn)) => Poll::Ready(Ok(conn)),
43+
Poll::Ready(Err(())) => Poll::Ready(Err(anyhow!("tls handshake failed"))),
44+
Poll::Pending => match sock_tx_fut.as_mut().poll(cx) {
45+
Poll::Ready(Ok(())) => Poll::Ready(Err(anyhow!("Tx stream closed unexpectedly"))),
46+
Poll::Ready(Err(err)) => {
47+
Poll::Ready(Err(anyhow!("Tx stream closed with error: {err:?}")))
48+
}
49+
Poll::Pending => match ready!(sock_rx_fut.as_mut().poll(cx)) {
50+
Ok(_) => Poll::Ready(Err(anyhow!("Rx stream closed unexpectedly"))),
51+
Err(err) => Poll::Ready(Err(anyhow!("Rx stream closed with error: {err:?}"))),
52+
},
53+
},
54+
})
55+
.await?;
56+
57+
let (mut req_tx, req_rx) = wit_stream::new();
58+
let (mut res_rx, result_fut) = tls::client::Handshake::finish(conn, req_rx);
59+
60+
let res = Vec::with_capacity(8192);
61+
try_join!(
62+
async {
63+
let buf = req_tx.write_all(request.into()).await;
64+
assert_eq!(buf, []);
65+
drop(req_tx);
66+
Ok(())
67+
},
68+
async {
69+
let (result, buf) = res_rx.read(res).await;
70+
match result {
71+
StreamResult::Complete(..) => {
72+
drop(res_rx);
73+
let res = String::from_utf8(buf)?;
74+
if res.contains("HTTP/1.1 200 OK") {
75+
Ok(())
76+
} else {
77+
bail!("server did not respond with 200 OK: {res}")
78+
}
79+
}
80+
StreamResult::Dropped => bail!("read dropped"),
81+
StreamResult::Cancelled => bail!("read cancelled"),
82+
}
83+
},
84+
async { result_fut.await.map_err(|()| anyhow!("TLS session failed")) },
85+
async { sock_rx_fut.await.context("TCP receipt failed") },
86+
async { sock_tx_fut.await.context("TCP transmit failed") },
87+
)?;
88+
Ok(())
89+
}
90+
91+
/// This test sets up a TCP connection using one domain, and then attempts to
92+
/// perform a TLS handshake using another unrelated domain. This should result
93+
/// in a handshake error.
94+
async fn test_tls_invalid_certificate(_domain: &str, ip: IpAddress) -> Result<()> {
95+
const BAD_DOMAIN: &'static str = "wrongdomain.localhost";
96+
97+
let sock = TcpSocket::create(ip.family()).unwrap();
98+
sock.connect(IpSocketAddress::new(ip, PORT))
99+
.await
100+
.context("tcp connect failed")?;
101+
102+
let (sock_rx, sock_rx_fut) = sock.receive();
103+
let hello = Hello::new();
104+
hello
105+
.set_server_name(BAD_DOMAIN)
106+
.map_err(|()| anyhow!("failed to set SNI"))?;
107+
let (sock_tx, conn) = tls::client::connect(hello, sock_rx);
108+
let sock_tx_fut = sock.send(sock_tx);
109+
110+
try_join!(
111+
async {
112+
match conn.await {
113+
Err(()) => Ok(()),
114+
Ok(_) => panic!("expecting server name mismatch"),
115+
}
116+
},
117+
async { sock_rx_fut.await.context("TCP receipt failed") },
118+
async { sock_tx_fut.await.context("TCP transmit failed") },
119+
)?;
120+
Ok(())
121+
}
122+
123+
async fn try_live_endpoints<'a, Fut>(test: impl Fn(&'a str, IpAddress) -> Fut)
124+
where
125+
Fut: Future<Output = Result<()>> + 'a,
126+
{
127+
// since this is testing remote endpoints to ensure system cert store works
128+
// the test uses a couple different endpoints to reduce the number of flakes
129+
const DOMAINS: &'static [&'static str] = &[
130+
"example.com",
131+
"api.github.com",
132+
"docs.wasmtime.dev",
133+
"bytecodealliance.org",
134+
"www.rust-lang.org",
135+
];
136+
137+
for &domain in DOMAINS {
138+
let result = (|| async {
139+
let ip = resolve_addresses(domain.into())
140+
.await?
141+
.first()
142+
.map(|a| a.to_owned())
143+
.ok_or_else(|| anyhow!("DNS lookup failed."))?;
144+
test(&domain, ip).await
145+
})();
146+
147+
match result.await {
148+
Ok(()) => return,
149+
Err(e) => {
150+
eprintln!("test for {domain} failed: {e:#}");
151+
}
152+
}
153+
}
154+
155+
panic!("all tests failed");
156+
}
157+
158+
impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
159+
async fn run() -> Result<(), ()> {
160+
println!("sample app");
161+
try_live_endpoints(test_tls_sample_application).await;
162+
println!("invalid cert");
163+
try_live_endpoints(test_tls_invalid_certificate).await;
164+
Ok(())
165+
}
166+
}
167+
168+
fn main() {}

crates/test-programs/src/p3/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ wit_bindgen::generate!({
88
world testp3 {
99
include wasi:cli/imports@0.3.0-rc-2025-09-16;
1010
include wasi:http/imports@0.3.0-rc-2025-09-16;
11+
include wasi:tls/imports@0.3.0-draft;
1112
1213
export wasi:cli/run@0.3.0-rc-2025-09-16;
1314
}
1415
",
15-
path: "../wasi-http/src/p3/wit",
16+
path: [
17+
"../wasi-http/src/p3/wit",
18+
"../wasi-tls/src/p3/wit",
19+
],
1620
world: "wasmtime:test/testp3",
1721
default_bindings_module: "test_programs::p3",
1822
pub_export_macro: true,

crates/wasi-tls-nativetls/tests/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ macro_rules! assert_test_exists {
6262
};
6363
}
6464

65-
test_programs_artifacts::foreach_tls!(assert_test_exists);
65+
test_programs_artifacts::foreach_p2_tls!(assert_test_exists);
6666

6767
#[tokio::test(flavor = "multi_thread")]
68-
async fn tls_sample_application() -> Result<()> {
69-
run_test(test_programs_artifacts::TLS_SAMPLE_APPLICATION_COMPONENT).await
68+
async fn p2_tls_sample_application() -> Result<()> {
69+
run_test(test_programs_artifacts::P2_TLS_SAMPLE_APPLICATION_COMPONENT).await
7070
}

crates/wasi-tls/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ description = "Wasmtime implementation of the wasi-tls API"
1111
[lints]
1212
workspace = true
1313

14+
[features]
15+
default = []
16+
p3 = ["wasmtime-wasi/p3", "wasmtime/component-model-async"]
17+
1418
[dependencies]
1519
anyhow = { workspace = true }
1620
bytes = { workspace = true }
@@ -20,6 +24,7 @@ tokio = { workspace = true, features = [
2024
"time",
2125
"io-util",
2226
] }
27+
tracing = { workspace = true }
2328
wasmtime = { workspace = true, features = ["runtime", "component-model"] }
2429
wasmtime-wasi = { workspace = true }
2530

crates/wasi-tls/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ use wasmtime::component::{HasData, ResourceTable};
7676
pub mod bindings;
7777
mod host;
7878
mod io;
79+
#[cfg(feature = "p3")]
80+
pub mod p3;
7981
mod rustls;
8082

8183
pub use bindings::types::LinkOptions;

crates/wasi-tls/src/p3/bindings.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! Raw bindings to the `wasi:tls` package.
2+
3+
#[expect(missing_docs, reason = "generated code")]
4+
mod generated {
5+
wasmtime::component::bindgen!({
6+
path: "src/p3/wit",
7+
world: "wasi:tls/imports",
8+
imports: {
9+
"wasi:tls/client.[static]handshake.finish": trappable | tracing | store,
10+
"wasi:tls/client.connect": trappable | tracing | store,
11+
"wasi:tls/server.[static]handshake.finish": trappable | tracing | store,
12+
default: trappable | tracing
13+
},
14+
with: {
15+
"wasi:tls/client.handshake": crate::p3::ClientHandshake,
16+
"wasi:tls/client.hello": crate::p3::ClientHello,
17+
"wasi:tls/server.handshake": crate::p3::ServerHandshake,
18+
"wasi:tls/types.certificate": crate::p3::Certificate,
19+
},
20+
});
21+
}
22+
23+
pub use self::generated::wasi::*;

0 commit comments

Comments
 (0)