diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 99a6b0204d59..dea990e06a6f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -931,10 +931,17 @@ jobs: # Run the tests! - run: cargo test -p wasmtime-wasi-nn --features ${{ matrix.feature }} - # Test `wasmtime-wasi-tls-nativetls` in its own job. This is because it - # depends on OpenSSL, which is not easily available on all platforms. - test_wasi_tls_nativetls: - name: Test wasi-tls using native-tls provider + # Test `wasmtime-wasi-tls-nativetls` & `wasmtime-wasi-tls-openssl` in their + # own job. This is because they depends on OpenSSL, which is not easily + # available on all platforms. + # + # The Windows base image has OpenSSL installed by default, but not in a way + # that is automatically discoverable by `openssl-sys`. We need to configure + # the OPENSSL_DIR & OPENSSL_LIB_DIR paths manually. + # Additionally, the GH actions Windows image does not ship with a CA cert + # bundle, so we use the one from cUrl. + test_wasi_tls: + name: Test wasi-tls using native-tls & openssl providers needs: determine if: needs.determine.outputs.run-full runs-on: ${{ matrix.os }} @@ -946,6 +953,19 @@ jobs: with: submodules: true - uses: ./.github/actions/install-rust + - name: Configure OpenSSL (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + mkdir "C:\Program Files\OpenSSL\certs" + $sslCertFile = "C:\Program Files\OpenSSL\certs\cacert.pem" + $opensslDir = "C:\Program Files\OpenSSL" + $opensslLibDir = "C:\Program Files\OpenSSL\lib\VC\x64\MD" + curl.exe -o $sslCertFile https://curl.se/ca/cacert.pem + "SSL_CERT_FILE=$sslCertFile" | Out-File -FilePath $env:GITHUB_ENV -Append + "OPENSSL_DIR=$opensslDir" | Out-File -FilePath $env:GITHUB_ENV -Append + "OPENSSL_LIB_DIR=$opensslLibDir" | Out-File -FilePath $env:GITHUB_ENV -Append + - run: cargo test -p wasmtime-wasi-tls-openssl - run: cargo test -p wasmtime-wasi-tls-nativetls # Test the `wasmtime-fuzzing` crate. Split out from the main tests because @@ -1258,7 +1278,7 @@ jobs: - doc - micro_checks - special_tests - - test_wasi_tls_nativetls + - test_wasi_tls - clippy - monolith_checks - platform_checks diff --git a/Cargo.lock b/Cargo.lock index 5adfd6c34940..57e94b2264f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2557,9 +2557,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags 2.9.4", "cfg-if", @@ -2589,9 +2589,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -3794,6 +3794,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-openssl" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59df6849caa43bb7567f9a36f863c447d95a11d5903c9cc334ba32576a27eadd" +dependencies = [ + "openssl", + "openssl-sys", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.25.0" @@ -5159,6 +5170,21 @@ dependencies = [ "wasmtime-wasi-tls", ] +[[package]] +name = "wasmtime-wasi-tls-openssl" +version = "41.0.0" +dependencies = [ + "anyhow", + "futures", + "openssl", + "test-programs-artifacts", + "tokio", + "tokio-openssl", + "wasmtime", + "wasmtime-wasi", + "wasmtime-wasi-tls", +] + [[package]] name = "wasmtime-wast" version = "41.0.0" diff --git a/Cargo.toml b/Cargo.toml index ca68c5e23156..83812e85d5b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -164,6 +164,7 @@ members = [ "crates/wasi-preview1-component-adapter", "crates/wasi-preview1-component-adapter/verify", "crates/wasi-tls-nativetls", + "crates/wasi-tls-openssl", "crates/debugger", "crates/wizer/fuzz", "crates/wizer/tests/regex-test", @@ -251,6 +252,7 @@ wasmtime-wasi-keyvalue = { path = "crates/wasi-keyvalue", version = "41.0.0" } wasmtime-wasi-threads = { path = "crates/wasi-threads", version = "41.0.0" } wasmtime-wasi-tls = { path = "crates/wasi-tls", version = "41.0.0" } wasmtime-wasi-tls-nativetls = { path = "crates/wasi-tls-nativetls", version = "41.0.0" } +wasmtime-wasi-tls-openssl = { path = "crates/wasi-tls-openssl", version = "41.0.0" } wasmtime-wast = { path = "crates/wast", version = "=41.0.0" } # Internal Wasmtime-specific crates. @@ -423,6 +425,8 @@ tokio-rustls = "0.25.0" rustls = "0.22.0" tokio-native-tls = "0.3.1" native-tls = "0.2.11" +tokio-openssl = "0.6.5" +openssl = "0.10.75" webpki-roots = "0.26.0" itertools = "0.14.0" base64 = "0.22.1" diff --git a/ci/run-tests.py b/ci/run-tests.py index b6a28454b928..8e08a7b87d19 100755 --- a/ci/run-tests.py +++ b/ci/run-tests.py @@ -10,6 +10,9 @@ # - wasmtime-wasi-tls-nativetls: the openssl dependency does not play nice with # cross compilation. This crate is tested in a separate CI job. # +# - wasmtime-wasi-tls-openssl: the openssl dependency does not play nice with +# cross compilation. This crate is tested in a separate CI job. +# # - wasmtime-fuzzing: enabling all features brings in OCaml which is a pain to # configure for all targets, so it has its own CI job. # @@ -27,6 +30,7 @@ args.append('--exclude=test-programs') args.append('--exclude=wasmtime-wasi-nn') args.append('--exclude=wasmtime-wasi-tls-nativetls') +args.append('--exclude=wasmtime-wasi-tls-openssl') args.append('--exclude=wasmtime-fuzzing') args.append('--exclude=wasm-spec-interpreter') args.append('--exclude=veri_engine') diff --git a/crates/wasi-tls-openssl/Cargo.toml b/crates/wasi-tls-openssl/Cargo.toml new file mode 100644 index 000000000000..feead31bae08 --- /dev/null +++ b/crates/wasi-tls-openssl/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "wasmtime-wasi-tls-openssl" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +repository = "https://github.com/bytecodealliance/wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +description = "Wasmtime implementation of the wasi-tls API, using OpenSSL for TLS support." + +[lints] +workspace = true + +[dependencies] +wasmtime-wasi-tls = { workspace = true } +tokio = { workspace = true } +tokio-openssl = { workspace = true } +openssl = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +test-programs-artifacts = { workspace = true } +wasmtime = { workspace = true, features = ["runtime", "component-model"] } +wasmtime-wasi = { workspace = true } +tokio = { workspace = true, features = ["macros"] } +futures = { workspace = true } diff --git a/crates/wasi-tls-openssl/src/lib.rs b/crates/wasi-tls-openssl/src/lib.rs new file mode 100644 index 000000000000..554dfdc010a8 --- /dev/null +++ b/crates/wasi-tls-openssl/src/lib.rs @@ -0,0 +1,94 @@ +//! The `openssl` provider. + +use openssl::ssl::{SslConnector, SslMethod}; +use std::{ + io, + pin::{Pin, pin}, +}; +use wasmtime_wasi_tls::{TlsProvider, TlsStream, TlsTransport}; + +type BoxFuture = std::pin::Pin + Send>>; + +/// The `openssl` provider. +pub struct OpenSslProvider { + _priv: (), +} + +impl TlsProvider for OpenSslProvider { + fn connect( + &self, + server_name: String, + transport: Box, + ) -> BoxFuture>> { + async fn connect_impl( + server_name: String, + transport: Box, + ) -> Result { + // Per the `openssl` crate's recommendation, we're using the + // `SslConnector` to set up a Ssl object with secure defaults: + // + // https://docs.rs/openssl/latest/openssl/ssl/struct.SslConnector.html + // > OpenSSL's default configuration is highly insecure. This + // > connector manages the OpenSSL structures, configuring cipher + // > suites, session options, hostname verification, and more. + let config = SslConnector::builder(SslMethod::tls_client())? + .build() + .configure()?; + let ssl = config.into_ssl(&server_name)?; + let mut stream = tokio_openssl::SslStream::new(ssl, transport)?; + Pin::new(&mut stream).connect().await?; + Ok(OpenSslStream(stream)) + } + + Box::pin(async move { + let stream = connect_impl(server_name, transport) + .await + .map_err(|e| io::Error::other(e))?; + Ok(Box::new(stream) as Box) + }) + } +} + +impl Default for OpenSslProvider { + fn default() -> Self { + Self { _priv: () } + } +} + +struct OpenSslStream(tokio_openssl::SslStream>); + +impl TlsStream for OpenSslStream {} + +impl tokio::io::AsyncRead for OpenSslStream { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + pin!(&mut self.as_mut().0).poll_read(cx, buf) + } +} + +impl tokio::io::AsyncWrite for OpenSslStream { + fn poll_write( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + pin!(&mut self.as_mut().0).poll_write(cx, buf) + } + + fn poll_flush( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + pin!(&mut self.as_mut().0).poll_flush(cx) + } + + fn poll_shutdown( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + pin!(&mut self.as_mut().0).poll_shutdown(cx) + } +} diff --git a/crates/wasi-tls-openssl/tests/main.rs b/crates/wasi-tls-openssl/tests/main.rs new file mode 100644 index 000000000000..107fbb103ee4 --- /dev/null +++ b/crates/wasi-tls-openssl/tests/main.rs @@ -0,0 +1,70 @@ +use anyhow::{Result, anyhow}; +use wasmtime::{ + Store, + component::{Component, Linker, ResourceTable}, +}; +use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView, p2::bindings::Command}; +use wasmtime_wasi_tls::{LinkOptions, WasiTls, WasiTlsCtx, WasiTlsCtxBuilder}; + +struct Ctx { + table: ResourceTable, + wasi_ctx: WasiCtx, + wasi_tls_ctx: WasiTlsCtx, +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi_ctx, + table: &mut self.table, + } + } +} + +async fn run_test(path: &str) -> Result<()> { + let provider = Box::new(wasmtime_wasi_tls_openssl::OpenSslProvider::default()); + let ctx = Ctx { + table: ResourceTable::new(), + wasi_ctx: WasiCtx::builder() + .inherit_stderr() + .inherit_network() + .allow_ip_name_lookup(true) + .build(), + wasi_tls_ctx: WasiTlsCtxBuilder::new().provider(provider).build(), + }; + + let engine = test_programs_artifacts::engine(|config| { + config.async_support(true); + }); + let mut store = Store::new(&engine, ctx); + let component = Component::from_file(&engine, path)?; + + let mut linker = Linker::new(&engine); + wasmtime_wasi::p2::add_to_linker_async(&mut linker)?; + let mut opts = LinkOptions::default(); + opts.tls(true); + wasmtime_wasi_tls::add_to_linker(&mut linker, &mut opts, |h: &mut Ctx| { + WasiTls::new(&h.wasi_tls_ctx, &mut h.table) + })?; + + let command = Command::instantiate_async(&mut store, &component, &linker).await?; + command + .wasi_cli_run() + .call_run(&mut store) + .await? + .map_err(|()| anyhow!("command returned with failing exit status")) +} + +macro_rules! assert_test_exists { + ($name:ident) => { + #[expect(unused_imports, reason = "just here to assert it exists")] + use self::$name as _; + }; +} + +test_programs_artifacts::foreach_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 +} diff --git a/scripts/publish.rs b/scripts/publish.rs index 88366a2254af..b0a6763860c5 100644 --- a/scripts/publish.rs +++ b/scripts/publish.rs @@ -81,6 +81,7 @@ const CRATES_TO_PUBLISH: &[&str] = &[ "wasmtime-wasi-threads", "wasmtime-wasi-tls", "wasmtime-wasi-tls-nativetls", + "wasmtime-wasi-tls-openssl", "wasmtime-wast", "wasmtime-internal-c-api-macros", "wasmtime-c-api-impl", @@ -103,6 +104,7 @@ const PUBLIC_CRATES: &[&str] = &[ "wasmtime-wasi", "wasmtime-wasi-tls", "wasmtime-wasi-tls-nativetls", + "wasmtime-wasi-tls-openssl", "wasmtime-wasi-http", "wasmtime-wasi-nn", "wasmtime-wasi-config",