Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,7 @@ crates/v8-runtime/assets/generated/

# staged local wasm command fixtures (11MB+ binaries; never commit)
.local-cmds/

# Fetched Mozilla CA bundle (~230 KB). Pinned + fetched by `make -C toolchain/c
# ca-certificates`; staged into the sidecar at build time. Never committed.
crates/native-sidecar/assets/ca-certificates.crt
42 changes: 41 additions & 1 deletion crates/native-sidecar/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::{env, fs, path::PathBuf};
use std::{
env, fs,
path::{Path, PathBuf},
};

// Stage the base filesystem fixture into OUT_DIR. In-tree builds use the
// canonical AgentOS runtime-core fixture from the current workspace; the
Expand Down Expand Up @@ -31,4 +34,41 @@ fn main() {
error
)
});

stage_ca_bundle(&manifest_dir, &out_dir);
}

/// Stage the Mozilla CA bundle into OUT_DIR so it can be embedded via
/// `include_bytes!` and seeded into the VM at `/etc/ssl/certs/ca-certificates.crt`.
///
/// The ~230 KB PEM blob is never committed. It is fetched into
/// `assets/ca-certificates.crt` by `make -C toolchain/c ca-certificates`. When
/// the asset is absent (fresh checkout, or `cargo publish` verification with no
/// network) we stage an empty placeholder — the sidecar then simply skips
/// seeding the bundle, matching the Pyodide "asset unavailable" pattern. Runtime
/// TLS still works via `--cacert`/`SSL_CERT_FILE` overrides in that case.
fn stage_ca_bundle(manifest_dir: &Path, out_dir: &Path) {
let asset = manifest_dir.join("assets/ca-certificates.crt");
println!("cargo:rerun-if-changed={}", asset.display());

let dest = out_dir.join("ca-certificates.crt");
if asset.exists() {
fs::copy(&asset, &dest).unwrap_or_else(|error| {
panic!(
"failed to stage ca-certificates.crt from {} to {}: {}",
asset.display(),
dest.display(),
error
)
});
} else {
// Empty placeholder keeps the include_bytes! of the OUT_DIR copy valid.
fs::write(&dest, b"").unwrap_or_else(|error| {
panic!(
"failed to stage empty ca-certificates.crt placeholder to {}: {}",
dest.display(),
error
)
});
}
}
101 changes: 100 additions & 1 deletion crates/native-sidecar/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ const SHADOW_ROOT_BOOTSTRAP_DIRS: &[(&str, u32)] = &[
("/workspace", 0o755),
];

/// Mozilla CA bundle (the set Debian's `ca-certificates` produces), staged by
/// `build.rs` into `OUT_DIR`. It is empty when the `assets/ca-certificates.crt`
/// blob was absent at build time (fresh checkout / offline publish verify — run
/// `make -C toolchain/c ca-certificates` to populate it); an empty bundle is
/// simply not seeded. When present it is written into the VM shadow tree so
/// in-guest TLS (curl/wget's mbedTLS backend) resolves trust the Debian way via
/// `/etc/ssl/certs/ca-certificates.crt`.
const CA_CERTIFICATES_BUNDLE: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/ca-certificates.crt"));
const CA_CERTIFICATES_GUEST_PATH: &str = "/etc/ssl/certs/ca-certificates.crt";
/// Conventional `/etc/ssl/cert.pem` -> `certs/ca-certificates.crt` symlink that
/// OpenSSL's default `OPENSSLDIR` layout expects.
const CA_CERTIFICATES_SYMLINK_PATH: &str = "/etc/ssl/cert.pem";
const CA_CERTIFICATES_SYMLINK_TARGET: &str = "certs/ca-certificates.crt";

fn send_kernel_socket_readiness_event(
target: KernelSocketReadinessTarget,
readiness: SocketReadiness,
Expand Down Expand Up @@ -1753,6 +1768,49 @@ fn bootstrap_shadow_root(root: &Path) -> Result<(), SidecarError> {
))
})?;
}
seed_ca_certificates_bundle(root)?;
Ok(())
}

/// Seed the Mozilla CA bundle into the shadow root at
/// `/etc/ssl/certs/ca-certificates.crt` (plus the conventional
/// `/etc/ssl/cert.pem` symlink) so guest TLS clients resolve trust the standard
/// Debian way. No-op when the bundle was not staged at build time.
fn seed_ca_certificates_bundle(root: &Path) -> Result<(), SidecarError> {
if CA_CERTIFICATES_BUNDLE.is_empty() {
return Ok(());
}

let bundle_path = shadow_path_for_guest(root, CA_CERTIFICATES_GUEST_PATH);
if let Some(parent) = bundle_path.parent() {
fs::create_dir_all(parent).map_err(|error| {
SidecarError::Io(format!(
"failed to create shadow CA certs directory {}: {error}",
parent.display()
))
})?;
}
fs::write(&bundle_path, CA_CERTIFICATES_BUNDLE).map_err(|error| {
SidecarError::Io(format!(
"failed to seed CA bundle {}: {error}",
bundle_path.display()
))
})?;
fs::set_permissions(&bundle_path, fs::Permissions::from_mode(0o644)).map_err(|error| {
SidecarError::Io(format!(
"failed to set CA bundle mode on {}: {error}",
bundle_path.display()
))
})?;

let symlink_path = shadow_path_for_guest(root, CA_CERTIFICATES_SYMLINK_PATH);
let _ = fs::remove_file(&symlink_path);
std::os::unix::fs::symlink(CA_CERTIFICATES_SYMLINK_TARGET, &symlink_path).map_err(|error| {
SidecarError::Io(format!(
"failed to seed CA bundle symlink {}: {error}",
symlink_path.display()
))
})?;
Ok(())
}

Expand Down Expand Up @@ -2150,8 +2208,11 @@ mod tests {
use super::{
bootstrap_native_root_filesystem, bootstrap_shadow_root,
materialize_shadow_root_snapshot_entries, native_root_plugin_from_config,
prune_kernel_command_stub, shadow_path_for_guest, KERNEL_COMMAND_STUB,
prune_kernel_command_stub, shadow_path_for_guest, CA_CERTIFICATES_BUNDLE,
CA_CERTIFICATES_GUEST_PATH, CA_CERTIFICATES_SYMLINK_PATH, CA_CERTIFICATES_SYMLINK_TARGET,
KERNEL_COMMAND_STUB,
};
use std::path::Path;
use crate::plugins::chunked_local::ChunkedLocalMountPlugin;
use crate::protocol::{
RootFilesystemDescriptor, RootFilesystemEntry, RootFilesystemEntryKind,
Expand Down Expand Up @@ -2207,6 +2268,44 @@ mod tests {
fs::remove_dir_all(&root).expect("temp shadow root should be removed");
}

#[test]
fn bootstrap_shadow_root_seeds_ca_bundle_when_present() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be monotonic")
.as_nanos();
let root =
std::env::temp_dir().join(format!("agentos-native-sidecar-ca-test-{unique}"));
fs::create_dir_all(&root).expect("temp shadow root should be created");

bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed");

let bundle = shadow_path_for_guest(&root, CA_CERTIFICATES_GUEST_PATH);
let symlink = shadow_path_for_guest(&root, CA_CERTIFICATES_SYMLINK_PATH);

if CA_CERTIFICATES_BUNDLE.is_empty() {
// No asset staged at build time — seeding is intentionally skipped.
assert!(
!bundle.exists(),
"CA bundle must not be seeded when the build asset is absent"
);
} else {
let seeded = fs::read(&bundle).expect("CA bundle should be seeded");
assert_eq!(
seeded, CA_CERTIFICATES_BUNDLE,
"seeded CA bundle should match the embedded asset"
);
let target = fs::read_link(&symlink).expect("cert.pem symlink should be seeded");
assert_eq!(
target,
Path::new(CA_CERTIFICATES_SYMLINK_TARGET),
"cert.pem should point at certs/ca-certificates.crt"
);
}

fs::remove_dir_all(&root).expect("temp shadow root should be removed");
}

#[test]
fn native_root_config_opens_chunked_local_as_persistent_root() {
let unique = SystemTime::now()
Expand Down
75 changes: 75 additions & 0 deletions toolchain/c/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,28 @@ CURL_UPSTREAM_OVERLAY_FILES := $(wildcard $(CURL_UPSTREAM_OVERLAY_DIR)/lib/*.c $
ZLIB_VERSION := 1.3.1
ZLIB_URL := https://zlib.net/zlib-$(ZLIB_VERSION).tar.gz
ZLIB_BUILD_DIR := $(BUILD_DIR)/zlib
# mbedTLS 3.6 LTS — in-guest TLS for curl/wget/git (see
# docs-internal/networking-parity-spec.md). Use the prepared release tarball
# (`mbedtls-<ver>.tar.bz2`), which bundles the generated sources and the
# `framework` submodule; the bare GitHub source archive does not. Everything
# ships under library/ in the 3.6 LTS line (crypto is not yet split into a
# separate tf-psa-crypto tree as on 4.x/master), so `make lib` needs no
# submodule fetch. We cross-compile every library/*.c against the wasi-sdk
# sysroot, mirroring the zlib rule, plus a getentropy() entropy shim.
MBEDTLS_VERSION := 3.6.2
MBEDTLS_URL := https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-$(MBEDTLS_VERSION)/mbedtls-$(MBEDTLS_VERSION).tar.bz2
# Deferred `=`: LIBS_CACHE is defined further down, so expand it lazily.
MBEDTLS_SRC_DIR = $(LIBS_CACHE)/mbedtls-$(MBEDTLS_VERSION)
MBEDTLS_BUILD_DIR := $(BUILD_DIR)/mbedtls
MBEDTLS_CONFIG_DIR := mbedtls
MBEDTLS_LIBS := $(MBEDTLS_BUILD_DIR)/libmbedtls.a $(MBEDTLS_BUILD_DIR)/libmbedx509.a $(MBEDTLS_BUILD_DIR)/libmbedcrypto.a
# Mozilla CA bundle (the set Debian's ca-certificates produces) shipped in the
# VM at /etc/ssl/certs/ca-certificates.crt. Pinned URL + fetch rule only; the
# ~230 KB PEM blob is never committed (the sidecar stages it via build.rs into
# OUT_DIR, falling back to an empty placeholder when absent — see
# crates/native-sidecar/build.rs). `make ca-certificates` populates the asset.
CACERT_URL := https://curl.se/ca/cacert.pem
CACERT_ASSET := ../../crates/native-sidecar/assets/ca-certificates.crt
WGET_VERSION := 1.24.5
WGET_URL := https://ftp.gnu.org/gnu/wget/wget-$(WGET_VERSION).tar.gz
WGET_UPSTREAM_BUILD_DIR := $(BUILD_DIR)/wget-upstream
Expand Down Expand Up @@ -545,6 +567,59 @@ $(ZLIB_BUILD_DIR)/libz.a: $(LIBS_CACHE)/zlib-$(ZLIB_VERSION)/zlib.h $(PATCHED_SY
done
$(WASI_SDK_DIR)/bin/llvm-ar rcs $@ $(ZLIB_BUILD_DIR)/*.o

# --- mbedTLS 3.6 LTS for wasm32-wasip1 ---
# Fetch/extract the prepared release tarball, then cross-compile every
# library/*.c (plus the getentropy entropy shim) against the wasi-sdk sysroot,
# splitting objects into the three canonical archives by filename the same way
# mbedTLS's own Makefile does (crypto / x509 / tls). Single-threaded; entropy
# via getentropy() (MBEDTLS_ENTROPY_HARDWARE_ALT); TLS 1.3 on (default in 3.6).
$(MBEDTLS_SRC_DIR)/library/ssl_tls.c:
@echo "Fetching mbedTLS $(MBEDTLS_VERSION)..."
@mkdir -p $(LIBS_CACHE)
@curl -fSL "$(MBEDTLS_URL)" -o "$(LIBS_CACHE)/mbedtls-$(MBEDTLS_VERSION).tar.bz2"
@rm -rf "$(MBEDTLS_SRC_DIR)"
@tar -xjf "$(LIBS_CACHE)/mbedtls-$(MBEDTLS_VERSION).tar.bz2" -C "$(LIBS_CACHE)"

.PHONY: mbedtls
mbedtls: $(MBEDTLS_LIBS)

$(MBEDTLS_LIBS): $(MBEDTLS_SRC_DIR)/library/ssl_tls.c \
$(MBEDTLS_CONFIG_DIR)/wasi_user_config.h \
$(MBEDTLS_CONFIG_DIR)/mbedtls_wasi_entropy.c \
$(WASI_SDK_DIR)/bin/clang
@echo "=== Building mbedTLS $(MBEDTLS_VERSION) for wasm32-wasip1 ==="
@mkdir -p $(MBEDTLS_BUILD_DIR)/crypto $(MBEDTLS_BUILD_DIR)/x509 $(MBEDTLS_BUILD_DIR)/tls
@for src in $(MBEDTLS_SRC_DIR)/library/*.c $(MBEDTLS_CONFIG_DIR)/mbedtls_wasi_entropy.c; do \
name=$$(basename "$${src%.c}"); \
case "$$name" in \
x509*|pkcs7) grp=x509 ;; \
ssl_*|ssl|mps_*|debug|net_sockets) grp=tls ;; \
*) grp=crypto ;; \
esac; \
$(CC) --target=wasm32-wasip1 --sysroot=$(SYSROOT) -O2 \
-I$(MBEDTLS_SRC_DIR)/include -I$(MBEDTLS_CONFIG_DIR) \
-DMBEDTLS_USER_CONFIG_FILE='"wasi_user_config.h"' \
-c "$$src" -o "$(MBEDTLS_BUILD_DIR)/$$grp/$$name.o" || exit 1; \
done
@rm -f $(MBEDTLS_LIBS)
$(WASI_SDK_DIR)/bin/llvm-ar rcs $(MBEDTLS_BUILD_DIR)/libmbedcrypto.a $(MBEDTLS_BUILD_DIR)/crypto/*.o
$(WASI_SDK_DIR)/bin/llvm-ar rcs $(MBEDTLS_BUILD_DIR)/libmbedx509.a $(MBEDTLS_BUILD_DIR)/x509/*.o
$(WASI_SDK_DIR)/bin/llvm-ar rcs $(MBEDTLS_BUILD_DIR)/libmbedtls.a $(MBEDTLS_BUILD_DIR)/tls/*.o
@echo "=== mbedTLS libs built ==="
@ls -l $(MBEDTLS_LIBS)

# --- CA bundle (Mozilla / Debian ca-certificates) ---
# Fetch the pinned Mozilla CA bundle into the sidecar asset staged by build.rs.
# The blob is gitignored; run this once to enable real HTTPS verification.
.PHONY: ca-certificates
ca-certificates: $(CACERT_ASSET)

$(CACERT_ASSET):
@echo "Fetching Mozilla CA bundle from $(CACERT_URL)..."
@mkdir -p $(dir $(CACERT_ASSET))
@curl -fSL "$(CACERT_URL)" -o "$(CACERT_ASSET)"
@echo "CA bundle staged at $(CACERT_ASSET) ($$(wc -c < $(CACERT_ASSET)) bytes)"

$(NATIVE_DIR)/sqlite3_cli: $(NATIVE_DIR)/sqlite3
cp $< $@

Expand Down
60 changes: 60 additions & 0 deletions toolchain/c/mbedtls/mbedtls_wasi_entropy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Hardware entropy shim for the wasm32-wasip1 (WASI) mbedTLS build.
*
* The WASI build sets MBEDTLS_NO_PLATFORM_ENTROPY (no /dev/urandom fopen path)
* and MBEDTLS_ENTROPY_HARDWARE_ALT, which routes the entropy module through
* this mbedtls_hardware_poll() implementation. We satisfy it with getentropy(),
* the WASI random primitive (backed by the kernel's random device layer) that
* the repo's host-side wasi_tls.c already uses. getentropy() fills at most 256
* bytes per call, so we loop for larger requests.
*/

#include <stddef.h>
#include <time.h> /* clock_gettime(), CLOCK_MONOTONIC */
#include <unistd.h> /* getentropy() — declared by wasi-libc <unistd.h> */

#include "mbedtls/build_info.h"
#include "mbedtls/platform_util.h" /* mbedtls_ms_time_t */

/*
* WASI mbedtls_ms_time() over clock_gettime(CLOCK_MONOTONIC). Enabled by
* MBEDTLS_PLATFORM_MS_TIME_ALT in the WASI user config (see wasi_user_config.h),
* which suppresses mbedTLS's own platform-detected definition.
*/
#if defined(MBEDTLS_PLATFORM_MS_TIME_ALT)
mbedtls_ms_time_t mbedtls_ms_time(void)
{
struct timespec tv;
if (clock_gettime(CLOCK_MONOTONIC, &tv) != 0) {
return (mbedtls_ms_time_t) time(NULL) * 1000;
}
return (mbedtls_ms_time_t) tv.tv_sec * 1000 +
(mbedtls_ms_time_t) (tv.tv_nsec / 1000000);
}
#endif /* MBEDTLS_PLATFORM_MS_TIME_ALT */

int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len,
size_t *olen)
{
(void)data;

size_t filled = 0;
while (filled < len) {
size_t chunk = len - filled;
if (chunk > 256) {
chunk = 256; /* getentropy() rejects requests larger than 256. */
}
if (getentropy(output + filled, chunk) != 0) {
if (olen != NULL) {
*olen = filled;
}
return -1; /* MBEDTLS_ERR_ENTROPY_SOURCE_FAILED at the call site. */
}
filled += chunk;
}

if (olen != NULL) {
*olen = len;
}
return 0;
}
44 changes: 44 additions & 0 deletions toolchain/c/mbedtls/wasi_user_config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* mbedTLS user-config overrides for the wasm32-wasip1 (WASI) build.
*
* This file is included via -DMBEDTLS_USER_CONFIG_FILE after mbedTLS's own
* default `mbedtls_config.h`, so it only needs to describe the deltas required
* to cross-compile cleanly against the wasi-sdk sysroot. The default 3.6 config
* already enables TLS 1.2 + TLS 1.3, X.509, PK, PSA crypto, and MBEDTLS_FS_IO
* (needed for curl/wget's mbedtls_x509_crt_parse_file on /etc/ssl/certs).
*
* Deltas:
* - Single-threaded: MBEDTLS_THREADING_C stays off (default) — no pthreads.
* - Entropy: WASI has no /dev/urandom fopen path, so disable platform entropy
* and route the entropy module through mbedtls_hardware_poll(), which we
* implement over getentropy() (see mbedtls_wasi_entropy.c). getentropy() is
* the same primitive the repo's host-side wasi_tls.c already relies on.
* - Networking: MBEDTLS_NET_C pulls in BSD <sys/socket.h> select()/socket()
* that wasi-libc does not fully provide. curl/wget own the transport and
* only need the TLS record + X.509 layers, so drop mbedTLS's own sockets.
* - Timing: MBEDTLS_TIMING_C uses gettimeofday()/select() hardware timers that
* are unnecessary here (no DTLS retransmission timers in the curl path).
*/

#ifndef MBEDTLS_WASI_USER_CONFIG_H
#define MBEDTLS_WASI_USER_CONFIG_H

/* Entropy: replace the POSIX /dev/urandom source with a getentropy() poll. */
#undef MBEDTLS_PLATFORM_ENTROPY
#define MBEDTLS_NO_PLATFORM_ENTROPY
#define MBEDTLS_ENTROPY_HARDWARE_ALT

/*
* mbedtls_ms_time(): mbedTLS's built-in POSIX branch only compiles when it can
* see _POSIX_VERSION, but it guards the <unistd.h> include behind unix/__unix
* macros that clang does not define for wasm32, so the platform detection falls
* through to `#error "No mbedtls_ms_time available"`. Provide it ourselves over
* clock_gettime(CLOCK_MONOTONIC) (available in wasi-libc) via the ALT hook.
*/
#define MBEDTLS_PLATFORM_MS_TIME_ALT

/* Drop mbedTLS's own BSD-sockets and hardware-timing modules for WASI. */
#undef MBEDTLS_NET_C
#undef MBEDTLS_TIMING_C

#endif /* MBEDTLS_WASI_USER_CONFIG_H */