Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions ci/vendor-wit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ make_vendor "wasi-tls" "
[email protected]+505fc98
"

make_vendor "wasi-tls/src/p3" "
[email protected]@wit-0.3.0-draft
"

make_vendor "wasi-config" "[email protected]"

make_vendor "wasi-keyvalue" "keyvalue@219ea36"
Expand Down
9 changes: 5 additions & 4 deletions crates/test-programs/artifacts/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,20 @@ impl Artifacts {
// generates a `foreach_*` macro below.
let kind = match test.name.as_str() {
s if s.starts_with("p1_") => "p1",
s if s.starts_with("p2_http_") => "p2_http",
s if s.starts_with("p2_cli_") => "p2_cli",
s if s.starts_with("p2_api_") => "p2_api",
s if s.starts_with("p2_cli_") => "p2_cli",
s if s.starts_with("p2_http_") => "p2_http",
s if s.starts_with("p2_tls_") => "p2_tls",
s if s.starts_with("p2_") => "p2",
s if s.starts_with("nn_") => "nn",
s if s.starts_with("piped_") => "piped",
s if s.starts_with("dwarf_") => "dwarf",
s if s.starts_with("config_") => "config",
s if s.starts_with("keyvalue_") => "keyvalue",
s if s.starts_with("tls_") => "tls",
s if s.starts_with("async_") => "async",
s if s.starts_with("p3_http_") => "p3_http",
s if s.starts_with("p3_api_") => "p3_api",
s if s.starts_with("p3_http_") => "p3_http",
s if s.starts_with("p3_tls_") => "p3_tls",
s if s.starts_with("p3_") => "p3",
s if s.starts_with("fuzz_") => "fuzz",
// If you're reading this because you hit this panic, either add
Expand Down
168 changes: 168 additions & 0 deletions crates/test-programs/src/bin/p3_tls_sample_application.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use anyhow::{Context as _, Result, anyhow, bail};
use core::future::{Future as _, poll_fn};
use core::pin::pin;
use core::str;
use core::task::{Poll, ready};
use futures::try_join;
use test_programs::p3::wasi::sockets::ip_name_lookup::resolve_addresses;
use test_programs::p3::wasi::sockets::types::{IpAddress, IpSocketAddress, TcpSocket};
use test_programs::p3::wasi::tls;
use test_programs::p3::wasi::tls::client::Hello;
use test_programs::p3::wit_stream;
use wit_bindgen::StreamResult;

struct Component;

test_programs::p3::export!(Component);

const PORT: u16 = 443;

async fn test_tls_sample_application(domain: &str, ip: IpAddress) -> Result<()> {
let request = format!(
"GET / HTTP/1.1\r\nHost: {domain}\r\nUser-Agent: wasmtime-wasi-rust\r\nConnection: close\r\n\r\n"
);

let sock = TcpSocket::create(ip.family()).unwrap();
sock.connect(IpSocketAddress::new(ip, PORT))
.await
.context("tcp connect failed")?;

let (sock_rx, sock_rx_fut) = sock.receive();
let hello = Hello::new();
hello
.set_server_name(domain)
.map_err(|()| anyhow!("failed to set SNI"))?;
let (sock_tx, conn) = tls::client::connect(hello, sock_rx);
let sock_tx_fut = sock.send(sock_tx);

let mut conn = pin!(conn.into_future());
let mut sock_rx_fut = pin!(sock_rx_fut.into_future());
let mut sock_tx_fut = pin!(sock_tx_fut);
let conn = poll_fn(|cx| match conn.as_mut().poll(cx) {
Poll::Ready(Ok(conn)) => Poll::Ready(Ok(conn)),
Poll::Ready(Err(())) => Poll::Ready(Err(anyhow!("tls handshake failed"))),
Poll::Pending => match sock_tx_fut.as_mut().poll(cx) {
Poll::Ready(Ok(())) => Poll::Ready(Err(anyhow!("Tx stream closed unexpectedly"))),
Poll::Ready(Err(err)) => {
Poll::Ready(Err(anyhow!("Tx stream closed with error: {err:?}")))
}
Poll::Pending => match ready!(sock_rx_fut.as_mut().poll(cx)) {
Ok(_) => Poll::Ready(Err(anyhow!("Rx stream closed unexpectedly"))),
Err(err) => Poll::Ready(Err(anyhow!("Rx stream closed with error: {err:?}"))),
},
},
})
.await?;

let (mut req_tx, req_rx) = wit_stream::new();
let (mut res_rx, result_fut) = tls::client::Handshake::finish(conn, req_rx);

let res = Vec::with_capacity(8192);
try_join!(
async {
let buf = req_tx.write_all(request.into()).await;
assert_eq!(buf, []);
drop(req_tx);
Ok(())
},
async {
let (result, buf) = res_rx.read(res).await;
match result {
StreamResult::Complete(..) => {
drop(res_rx);
let res = String::from_utf8(buf)?;
if res.contains("HTTP/1.1 200 OK") {
Ok(())
} else {
bail!("server did not respond with 200 OK: {res}")
}
}
StreamResult::Dropped => bail!("read dropped"),
StreamResult::Cancelled => bail!("read cancelled"),
}
},
async { result_fut.await.map_err(|()| anyhow!("TLS session failed")) },
async { sock_rx_fut.await.context("TCP receipt failed") },
async { sock_tx_fut.await.context("TCP transmit failed") },
)?;
Ok(())
}

/// This test sets up a TCP connection using one domain, and then attempts to
/// perform a TLS handshake using another unrelated domain. This should result
/// in a handshake error.
async fn test_tls_invalid_certificate(_domain: &str, ip: IpAddress) -> Result<()> {
const BAD_DOMAIN: &'static str = "wrongdomain.localhost";

let sock = TcpSocket::create(ip.family()).unwrap();
sock.connect(IpSocketAddress::new(ip, PORT))
.await
.context("tcp connect failed")?;

let (sock_rx, sock_rx_fut) = sock.receive();
let hello = Hello::new();
hello
.set_server_name(BAD_DOMAIN)
.map_err(|()| anyhow!("failed to set SNI"))?;
let (sock_tx, conn) = tls::client::connect(hello, sock_rx);
let sock_tx_fut = sock.send(sock_tx);

try_join!(
async {
match conn.await {
Err(()) => Ok(()),
Ok(_) => panic!("expecting server name mismatch"),
}
},
async { sock_rx_fut.await.context("TCP receipt failed") },
async { sock_tx_fut.await.context("TCP transmit failed") },
)?;
Ok(())
}

async fn try_live_endpoints<'a, Fut>(test: impl Fn(&'a str, IpAddress) -> Fut)
where
Fut: Future<Output = Result<()>> + 'a,
{
// since this is testing remote endpoints to ensure system cert store works
// the test uses a couple different endpoints to reduce the number of flakes
const DOMAINS: &'static [&'static str] = &[
"example.com",
"api.github.com",
"docs.wasmtime.dev",
"bytecodealliance.org",
"www.rust-lang.org",
];

for &domain in DOMAINS {
let result = (|| async {
let ip = resolve_addresses(domain.into())
.await?
.first()
.map(|a| a.to_owned())
.ok_or_else(|| anyhow!("DNS lookup failed."))?;
test(&domain, ip).await
})();

match result.await {
Ok(()) => return,
Err(e) => {
eprintln!("test for {domain} failed: {e:#}");
}
}
}

panic!("all tests failed");
}

impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
async fn run() -> Result<(), ()> {
println!("sample app");
try_live_endpoints(test_tls_sample_application).await;
println!("invalid cert");
try_live_endpoints(test_tls_invalid_certificate).await;
Ok(())
}
}

fn main() {}
6 changes: 5 additions & 1 deletion crates/test-programs/src/p3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ wit_bindgen::generate!({
world testp3 {
include wasi:cli/[email protected];
include wasi:http/[email protected];
include wasi:tls/[email protected];

export wasi:cli/[email protected];
}
",
path: "../wasi-http/src/p3/wit",
path: [
"../wasi-http/src/p3/wit",
"../wasi-tls/src/p3/wit",
],
world: "wasmtime:test/testp3",
default_bindings_module: "test_programs::p3",
pub_export_macro: true,
Expand Down
6 changes: 3 additions & 3 deletions crates/wasi-tls-nativetls/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ macro_rules! assert_test_exists {
};
}

test_programs_artifacts::foreach_tls!(assert_test_exists);
test_programs_artifacts::foreach_p2_tls!(assert_test_exists);

#[tokio::test(flavor = "multi_thread")]
async fn tls_sample_application() -> Result<()> {
run_test(test_programs_artifacts::TLS_SAMPLE_APPLICATION_COMPONENT).await
async fn p2_tls_sample_application() -> Result<()> {
run_test(test_programs_artifacts::P2_TLS_SAMPLE_APPLICATION_COMPONENT).await
}
5 changes: 5 additions & 0 deletions crates/wasi-tls/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ description = "Wasmtime implementation of the wasi-tls API"
[lints]
workspace = true

[features]
default = []
p3 = ["wasmtime-wasi/p3", "wasmtime/component-model-async"]

[dependencies]
anyhow = { workspace = true }
bytes = { workspace = true }
Expand All @@ -20,6 +24,7 @@ tokio = { workspace = true, features = [
"time",
"io-util",
] }
tracing = { workspace = true }
wasmtime = { workspace = true, features = ["runtime", "component-model"] }
wasmtime-wasi = { workspace = true }

Expand Down
2 changes: 2 additions & 0 deletions crates/wasi-tls/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ use wasmtime::component::{HasData, ResourceTable};
pub mod bindings;
mod host;
mod io;
#[cfg(feature = "p3")]
pub mod p3;
mod rustls;

pub use bindings::types::LinkOptions;
Expand Down
23 changes: 23 additions & 0 deletions crates/wasi-tls/src/p3/bindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! Raw bindings to the `wasi:tls` package.

#[expect(missing_docs, reason = "generated code")]
mod generated {
wasmtime::component::bindgen!({
path: "src/p3/wit",
world: "wasi:tls/imports",
imports: {
"wasi:tls/client.[static]handshake.finish": trappable | tracing | store,
"wasi:tls/client.connect": trappable | tracing | store,
"wasi:tls/server.[static]handshake.finish": trappable | tracing | store,
default: trappable | tracing
},
with: {
"wasi:tls/client.handshake": crate::p3::ClientHandshake,
"wasi:tls/client.hello": crate::p3::ClientHello,
"wasi:tls/server.handshake": crate::p3::ServerHandshake,
"wasi:tls/types.certificate": crate::p3::Certificate,
},
});
}

pub use self::generated::wasi::*;
Loading
Loading