Skip to content

Commit 694e553

Browse files
badeendrvolosatovs
andauthored
feat(p3): implement wasi:tls (#12834)
* Split off p2-specific bits into submodule * Vendor the 0.3.0-draft WIT files * Host traits scaffolding * Rename p2 test * Work around bug in `tokio-native-tls` * Create error type * Implement p3 Co-authored-by: Roman Volosatovs <rvolosatovs@riseup.net> * Fix test on Windows' SChannel Same reason as described in #11064 * Satisfy clippy * Fix typo --------- Co-authored-by: Roman Volosatovs <rvolosatovs@riseup.net>
1 parent f33f15e commit 694e553

36 files changed

+1610
-232
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,8 @@ jobs:
429429
- name: wasmtime-wasi-tls
430430
checks: |
431431
-p wasmtime-wasi-tls --no-default-features
432+
-p wasmtime-wasi-tls --no-default-features --features p2
433+
-p wasmtime-wasi-tls --no-default-features --features p3
432434
-p wasmtime-wasi-tls --no-default-features --features rustls
433435
-p wasmtime-wasi-tls --no-default-features --features nativetls
434436
-p wasmtime-wasi-tls --no-default-features --features openssl

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.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ component-model-async = [
528528
"component-model",
529529
"wasmtime-wasi?/p3",
530530
"wasmtime-wasi-http?/p3",
531+
"wasmtime-wasi-tls?/p3",
531532
"dep:futures",
532533
]
533534
rr = ["wasmtime/rr", "component-model", "wasmtime-cli-flags/rr", "run"]

ci/vendor-wit.sh

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,29 @@ set -ex
1010
cache_dir=$(mktemp -d)
1111
trap "rm -rf $cache_dir" EXIT
1212

13-
# Helper to download the `WebAssembly/$repo` dir at the `$tag` (or rev)
14-
# specified. The `wit/*.wit` files are placed in `$path`.
13+
# Helper to download content from `WebAssembly/$repo` at the `$tag` (or rev)
14+
# specified. By default `wit/*` is copied into `$path`, or a different
15+
# subdirectory can be specified with the optional fourth argument.
1516
get_github() {
1617
local repo=$1
1718
local tag=$2
1819
local path=$3
20+
local prefix=${4:-wit}
1921

2022
rm -rf "$path"
2123
mkdir -p "$path"
2224

23-
cached_extracted_dir="$cache_dir/$repo-$tag"
25+
cached_extracted_dir="$cache_dir/$prefix/$repo-$tag"
2426

2527
if [[ ! -d $cached_extracted_dir ]]; then
2628
mkdir -p $cached_extracted_dir
2729
curl --retry 5 --retry-all-errors -sLO https://github.com/WebAssembly/$repo/archive/$tag.tar.gz
2830
tar xzf $tag.tar.gz --strip-components=1 -C $cached_extracted_dir
2931
rm $tag.tar.gz
30-
rm -rf $cached_extracted_dir/wit/deps*
32+
rm -rf $cached_extracted_dir/${prefix}/deps*
3133
fi
3234

33-
cp -r $cached_extracted_dir/wit/* $path
35+
cp -r $cached_extracted_dir/${prefix}/* $path
3436
}
3537

3638
p2=0.2.6
@@ -65,6 +67,10 @@ mkdir -p crates/wasi-tls/wit/deps
6567
wkg get --format wit --overwrite "wasi:io@$p2" -o "crates/wasi-tls/wit/deps/io.wit"
6668
get_github wasi-tls v0.2.0-draft+505fc98 crates/wasi-tls/wit/deps/tls
6769

70+
rm -rf crates/wasi-tls/src/p3/wit/deps
71+
mkdir -p crates/wasi-tls/src/p3/wit/deps
72+
get_github wasi-tls 6781ae2 crates/wasi-tls/src/p3/wit/deps/tls wit-0.3.0-draft
73+
6874
rm -rf crates/wasi-config/wit/deps
6975
mkdir -p crates/wasi-config/wit/deps
7076
get_github wasi-config v0.2.0-rc.1 crates/wasi-config/wit/deps/config

crates/test-programs/artifacts/build.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,11 @@ impl Artifacts {
8787
s if s.starts_with("p1_") => "p1",
8888
s if s.starts_with("p2_http_") => "p2_http",
8989
s if s.starts_with("p2_api_") => "p2_api",
90+
s if s.starts_with("p2_tls_") => "p2_tls",
9091
s if s.starts_with("p2_") => "p2",
9192
s if s.starts_with("p3_http_") => "p3_http",
9293
s if s.starts_with("p3_api_") => "p3_api",
94+
s if s.starts_with("p3_tls_") => "p3_tls",
9395
s if s.starts_with("p3_") => "p3",
9496
s if s.starts_with("nn_") => "nn",
9597
s if s.starts_with("piped_") => "piped",
@@ -98,7 +100,6 @@ impl Artifacts {
98100
s if s.starts_with("dwarf_") => "dwarf",
99101
s if s.starts_with("config_") => "config",
100102
s if s.starts_with("keyvalue_") => "keyvalue",
101-
s if s.starts_with("tls_") => "tls",
102103
s if s.starts_with("async_") => "async",
103104
s if s.starts_with("fuzz_") => "fuzz",
104105
// 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: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
use anyhow::{Context as _, Result, anyhow};
2+
use core::future::Future;
3+
use test_programs::p3::wasi::sockets::ip_name_lookup::resolve_addresses;
4+
use test_programs::p3::wasi::sockets::types::{IpAddress, IpSocketAddress, TcpSocket};
5+
use test_programs::p3::wasi::tls::client::Connector;
6+
use test_programs::p3::wit_stream;
7+
8+
struct Component;
9+
10+
test_programs::p3::export!(Component);
11+
12+
const PORT: u16 = 443;
13+
14+
async fn test_tls_sample_application(domain: &str, ip: IpAddress) -> Result<()> {
15+
let request = format!(
16+
"GET / HTTP/1.1\r\nHost: {domain}\r\nUser-Agent: wasmtime-wasi-rust\r\nConnection: close\r\n\r\n"
17+
);
18+
19+
let sock = TcpSocket::create(ip.family()).unwrap();
20+
sock.connect(IpSocketAddress::new(ip, PORT))
21+
.await
22+
.context("tcp connect failed")?;
23+
24+
let conn = Connector::new();
25+
26+
let (sock_rx, sock_rx_fut) = sock.receive();
27+
let (tls_rx, tls_rx_fut) = conn.receive(sock_rx);
28+
29+
let (mut data_tx, data_rx) = wit_stream::new();
30+
let (tls_tx, tls_tx_err_fut) = conn.send(data_rx);
31+
let sock_tx_fut = sock.send(tls_tx);
32+
33+
Connector::connect(conn, domain.into())
34+
.await
35+
.context("tls handshake failed")?;
36+
let buf = data_tx.write_all(request.into()).await;
37+
assert!(buf.is_empty());
38+
39+
let response = tls_rx.collect().await;
40+
let response = String::from_utf8(response)?;
41+
if !response.contains("HTTP/1.1 200 OK") {
42+
return Err(anyhow!("server did not respond with 200 OK: {response}"));
43+
}
44+
drop(data_tx);
45+
sock_rx_fut.await.context("tcp recv")?;
46+
sock_tx_fut.await.context("tcp send")?;
47+
tls_rx_fut.await.context("tls recv")?;
48+
tls_tx_err_fut.await.context("tls send")?;
49+
50+
Ok(())
51+
}
52+
53+
/// This test sets up a TCP connection using one domain, and then attempts to
54+
/// perform a TLS handshake using another unrelated domain. This should result
55+
/// in a handshake error.
56+
async fn test_tls_invalid_certificate(_domain: &str, ip: IpAddress) -> Result<()> {
57+
const BAD_DOMAIN: &str = "wrongdomain.localhost";
58+
59+
let sock = TcpSocket::create(ip.family()).unwrap();
60+
sock.connect(IpSocketAddress::new(ip, PORT))
61+
.await
62+
.context("tcp connect failed")?;
63+
64+
let (_, data_rx) = wit_stream::new();
65+
let conn = Connector::new();
66+
67+
conn.receive(sock.receive().0);
68+
sock.send(conn.send(data_rx).0);
69+
70+
match Connector::connect(conn, BAD_DOMAIN.into()).await {
71+
Err(e) => {
72+
let debug_string = e.to_debug_string();
73+
// We're expecting an error regarding certificates in some form or
74+
// another. When we add more TLS backends this naive check will
75+
// likely need to be revisited/expanded:
76+
if debug_string.contains("certificate") || debug_string.contains("HandshakeFailure") {
77+
return Ok(());
78+
}
79+
Err(anyhow!(debug_string))
80+
}
81+
Ok(_) => panic!("expecting server name mismatch"),
82+
}
83+
}
84+
85+
async fn try_live_endpoints<'a, Fut>(test: impl Fn(&'a str, IpAddress) -> Fut)
86+
where
87+
Fut: Future<Output = Result<()>> + 'a,
88+
{
89+
// since this is testing remote endpoints to ensure system cert store works
90+
// the test uses a couple different endpoints to reduce the number of flakes
91+
const DOMAINS: &[&str] = &[
92+
"example.com",
93+
"api.github.com",
94+
"docs.wasmtime.dev",
95+
"bytecodealliance.org",
96+
"www.rust-lang.org",
97+
];
98+
99+
for &domain in DOMAINS {
100+
let result = (|| async {
101+
let ip = resolve_addresses(domain.into())
102+
.await?
103+
.first()
104+
.map(|a| a.to_owned())
105+
.ok_or_else(|| anyhow!("DNS lookup failed."))?;
106+
test(domain, ip).await
107+
})();
108+
109+
match result.await {
110+
Ok(()) => return,
111+
Err(e) => {
112+
eprintln!("test for {domain} failed: {e:#}");
113+
}
114+
}
115+
}
116+
117+
panic!("all tests failed");
118+
}
119+
120+
impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
121+
async fn run() -> Result<(), ()> {
122+
println!("sample app");
123+
try_live_endpoints(test_tls_sample_application).await;
124+
println!("invalid cert");
125+
try_live_endpoints(test_tls_invalid_certificate).await;
126+
Ok(())
127+
}
128+
}
129+
130+
fn main() {}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ wit_bindgen::generate!({
77
88
world testp3 {
99
include wasi:cli/imports@0.3.0-rc-2026-03-15;
10+
include wasi:tls/imports@0.3.0-draft;
1011
import wasi:http/types@0.3.0-rc-2026-03-15;
1112
import wasi:http/client@0.3.0-rc-2026-03-15;
1213
import wasi:http/handler@0.3.0-rc-2026-03-15;
1314
1415
export wasi:cli/run@0.3.0-rc-2026-03-15;
1516
}
1617
",
17-
path: "../wasi-http/src/p3/wit",
18+
path: [
19+
"../wasi-http/src/p3/wit",
20+
"../wasi-tls/src/p3/wit",
21+
],
1822
world: "wasmtime:test/testp3",
1923
default_bindings_module: "test_programs::p3",
2024
pub_export_macro: true,
@@ -44,3 +48,11 @@ pub mod service {
4448
},
4549
});
4650
}
51+
52+
impl std::fmt::Display for wasi::tls::types::Error {
53+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54+
f.write_str(&self.to_debug_string())
55+
}
56+
}
57+
58+
impl std::error::Error for wasi::tls::types::Error {}

crates/wasi-tls/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ description = "Wasmtime implementation of the wasi-tls API"
1212
workspace = true
1313

1414
[features]
15-
default = ["rustls"]
15+
default = ["p2", "rustls"]
16+
p2 = ["wasmtime-wasi/p2"]
17+
p3 = ["wasmtime-wasi/p3", "wasmtime/component-model-async", "tracing"]
1618
rustls = ["dep:rustls", "dep:tokio-rustls", "dep:webpki-roots"]
1719
nativetls = ["dep:native-tls", "dep:tokio-native-tls"]
1820
openssl = ["dep:openssl", "dep:tokio-openssl"]
@@ -27,6 +29,7 @@ tokio = { workspace = true, features = [
2729
] }
2830
wasmtime = { workspace = true, features = ["runtime", "component-model"] }
2931
wasmtime-wasi = { workspace = true }
32+
tracing = { workspace = true, optional = true }
3033
cfg-if = { workspace = true }
3134
tokio-rustls = { workspace = true, optional = true }
3235
rustls = { workspace = true, optional = true }

crates/wasi-tls/src/error.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use std::sync::Arc;
2+
3+
/// TLS error
4+
pub struct Error(Arc<String>);
5+
6+
impl Error {
7+
/// Creates a new error with the given message.
8+
pub fn msg<M>(message: M) -> Self
9+
where
10+
M: ToString,
11+
{
12+
Self(Arc::new(message.to_string()))
13+
}
14+
}
15+
impl Clone for Error {
16+
fn clone(&self) -> Self {
17+
Self(Arc::clone(&self.0))
18+
}
19+
}
20+
impl std::fmt::Debug for Error {
21+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22+
std::fmt::Debug::fmt(&self.0, f)
23+
}
24+
}
25+
impl std::fmt::Display for Error {
26+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27+
std::fmt::Display::fmt(&self.0, f)
28+
}
29+
}
30+
impl std::error::Error for Error {}
31+
impl From<std::io::Error> for Error {
32+
fn from(err: std::io::Error) -> Self {
33+
// Try to recover the original error:
34+
match err.downcast::<Error>() {
35+
Ok(e) => e,
36+
Err(io_err) => Self::msg(io_err),
37+
}
38+
}
39+
}
40+
impl From<Error> for std::io::Error {
41+
fn from(err: Error) -> Self {
42+
std::io::Error::other(err)
43+
}
44+
}

0 commit comments

Comments
 (0)