Harden browser isolation and native dependencies
ober
22205e28ae2b59888f5543e925de092b4d881734
--- a/.build.yml +++ b/.build.yml @@ -3,6 +3,7 @@ oauth: pages.sr.ht/PAGES:RW packages: - cmake - g++ + - git - ninja-build - qt6-base-dev - qt6-webengine-dev @@ -24,11 +25,10 @@ environment: LATEST_SITE: lisp.srht.site LATEST_SUBDIR: /jerboa-browser/latest LATEST_TARBALL: jerboa-browser-linux-amd64.tar.gz + JERBOA_COMMIT: a18b57415c011b66091749cb382d73346f163365 + JERBOA_TREE: 2272f217b3d3af8d39cd12358c509cb3a1b0f92c sources: - https://git.sr.ht/~lisp/jerboa-browser - # Chez Scheme is vendored in the jerboa repo (vendor/ChezScheme); cloned so CI - # can build it and the Jerboa stdlib closure into the native binary. - - https://git.sr.ht/~lisp/jerboa artifacts: # Saved + downloadable from the build job page. Path is relative to ~ (home); # the repo is cloned to ~/jerboa-browser and build-tarball.sh writes here. @@ -37,6 +37,16 @@ artifacts: # Offline Qt snapshot PNGs from the test-gui task (the harness output). - jerboa-browser/gui-snapshots.tar.gz tasks: + - fetch-jerboa: | + # Dependency source is fetched at the consumer-owned commit/tree lock; + # SourceHut's sources list otherwise follows a mutable repository head. + mkdir jerboa + git -C jerboa init -q + git -C jerboa remote add origin https://git.sr.ht/~lisp/jerboa + git -C jerboa fetch -q --depth 1 origin "$JERBOA_COMMIT" + test "$(git -C jerboa rev-parse FETCH_HEAD)" = "$JERBOA_COMMIT" + test "$(git -C jerboa show -s --format=%T FETCH_HEAD)" = "$JERBOA_TREE" + git -C jerboa checkout -q --detach "$JERBOA_COMMIT" - chez: | # Build Chez from the source vendored in the jerboa repo, so the binary # embeds the in-repo Chez (no external/stock Chez). Install to /usr/local @@ -66,12 +76,8 @@ tasks: SCHEME="$(command -v scheme)" "$J" pkg verify "$PWD/jerboa-browser-0.1.0.jpkg" SCHEME="$(command -v scheme)" "$J" pkg verify --reproduce - setup-rust: | - # Build tasks need a current Rust toolchain for vendored crates using - # language features newer than Debian bookworm's packaged rustc. - if [ ! -x "$HOME/.cargo/bin/cargo" ]; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal --default-toolchain stable - fi + cd jerboa-browser + sh support/ensure-rust.sh echo 'source "$HOME/.cargo/env"' >> ~/.buildenv - setup-hut: | # bookworm has Go but not hut. Install hut locally so the final task can @@ -91,16 +97,12 @@ tasks: # cdylib once into jerbuild's cache so they can dlopen it. The compiled # binary links these symbols statically (built above) and needs none. The # bundled crate's Cargo.lock is v4, so keep using the rustup toolchain. - if [ ! -x "$HOME/.cargo/bin/cargo" ]; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal --default-toolchain stable - fi export PATH="$HOME/.cargo/bin:$PATH" JH="$(jerbuild --jerboa-home)" # The crypto FFI exports (jerboa_sha*, _aead_*, _hmac_*, …) are behind the # crate's "crypto" Cargo feature; a default build omits them. crypto's deps # are pure-Rust, so no system libs are needed. - cargo build --release --no-default-features --features crypto \ + cargo build --locked --release --no-default-features --features crypto \ --manifest-path "$JH/jerboa-native-rs/Cargo.toml" ls -l "$JH"/jerboa-native-rs/target/release/libjerboa_native.* || true - verify: | new file mode 100644 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.patch -whitespace --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,8 @@ permissions: contents: read env: - JERBOA_VERSION: v0.2.3 + JERBOA_VERSION: v0.2.4 + RUST_TOOLCHAIN: "1.94.1" JERBUILD: ${{ github.workspace }}/.jerboa/bin/jerbuild QT_QPA_PLATFORM: offscreen QTWEBENGINE_CHROMIUM_FLAGS: --disable-gpu @@ -18,9 +19,11 @@ env: jobs: verify: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false - name: Install system tools run: | @@ -29,14 +32,17 @@ jobs: sudo apt-get install -y --no-install-recommends \ build-essential git curl ca-certificates cmake pkg-config \ qt6-base-dev qt6-webengine-dev xvfb - command -v cargo - cargo --version + + - name: Install pinned Rust toolchain + run: | + sh support/ensure-rust.sh + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Install Jerboa toolchain run: sh support/ensure-jerboa.sh "$JERBOA_VERSION" .jerboa/bin - name: Install cargo-audit - run: cargo install cargo-audit --locked + run: cargo install cargo-audit --version 0.22.2 --locked - name: Build Qt WebEngine backend run: | --- a/.github/workflows/security-baseline.yml +++ b/.github/workflows/security-baseline.yml @@ -11,9 +11,11 @@ permissions: jobs: baseline: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false - name: Required release files run: | --- a/.jerboa/security.json +++ b/.jerboa/security.json @@ -10,7 +10,7 @@ "docs": ["README.md", "SECURITY.md", "docs/**", "*.md"] }, "policy": { - "failOn": ["critical", "high"], + "failOn": ["critical", "high", "medium"], "imports": { "directChezscheme": "deny" }, "ffi": { "allowed": true, "requireLazyLoading": true, "requireBoundaryDocs": true }, "browser": { @@ -18,11 +18,15 @@ "targetSandboxProof": "marker-validated-fail-closed", "defaultProfile": "off-record", "persistentProfileRequiresEncryptedVault": true, - "envPassphrase": "deny" + "envPassphrase": "deny", + "schemeCapabilities": "independent-allowlist", + "filesystemRoot": "absolute-canonical-required", + "vaultHostPaths": "descriptor-relative-no-follow-private" }, "process": { "shellInterpolation": "deny" }, "network": { "denyByDefaultCapabilities": true }, - "secrets": { "forbidLogging": true, "ramOnlyPasswordVault": true, "strictStateFilePermissions": true } + "secrets": { "forbidLogging": true, "ramOnlyPasswordVault": true, "strictStateFilePermissions": true }, + "supplyChain": { "lock": "supply-chain.lock", "mutableRefs": "deny", "cohostedChecksums": "deny" } }, "suppressions": [] } --- a/.jerbuild +++ b/.jerbuild @@ -7,4 +7,11 @@ (entry "scheme/browser-main.ss") (output "jerboa-browser") (libdirs "vendor/jerboa-yubikey/lib" "scheme" "vendor/jerboa-fuse/lib") -(pre-build "sh support/ensure-vendor.sh jerboa-yubikey https://git.sr.ht/~lisp/jerboa-yubikey") +(static-native #t) +(extra-sources "support/browser_executable_identity.c") +(ffi-symbols "support/browser-ffi-symbols.list") +(os-libs + (Darwin "-lm -lpthread -lncurses -liconv -lc++") + (Linux "-lm -ldl -lpthread -lncurses -lstdc++") + (FreeBSD "-lm -lpthread -lncurses -lstdc++")) +(pre-build "sh support/ensure-vendor.sh jerboa-yubikey https://git.sr.ht/~lisp/jerboa-yubikey e448afca926ca9ad40fd98effa8d6264cb1019f5 67e4417511b63c0b41b1b9c5dc1c7d897df51f08") --- a/Dockerfile.qt +++ b/Dockerfile.qt @@ -12,15 +12,22 @@ # id=$(podman create jerboa-browser-qt-builder) # podman cp $id:/out/jerboa-browser-static-qt.tar.gz . && podman rm $id -ARG BASE_IMAGE=docker.io/library/ubuntu:24.04 +ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90 FROM ${BASE_IMAGE} AS builder -ARG JERBOA_VERSION=v0.2.3 +ARG JERBOA_VERSION=v0.2.4 +ARG JBROWSER_SOURCE_COMMIT +ARG JBROWSER_SOURCE_TREE +ARG APT_SNAPSHOT=20260701T000000Z ENV DEBIAN_FRONTEND=noninteractive ENV PATH=/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN sed -Ei \ + -e "s#https?://(archive|security)\\.ubuntu\\.com/ubuntu/?#https://snapshot.ubuntu.com/ubuntu/${APT_SNAPSHOT}/#g" \ + -e "s#https?://ports\\.ubuntu\\.com/ubuntu-ports/?#https://snapshot.ubuntu.com/ubuntu/${APT_SNAPSHOT}/#g" \ + /etc/apt/sources.list.d/ubuntu.sources && \ + apt-get update && apt-get install -y --no-install-recommends \ bash \ ca-certificates \ cmake \ @@ -40,12 +47,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ xz-utils \ && rm -rf /var/lib/apt/lists/* -RUN curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal && \ - rustup default stable && \ +COPY support/ensure-rust.sh /tmp/ensure-rust.sh +RUN sh /tmp/ensure-rust.sh && \ cargo --version && \ rustc -vV -COPY support/ensure-jerboa.sh /tmp/ensure-jerboa.sh +COPY support/ensure-jerboa.sh support/jerboa-release-sha256.txt /tmp/ RUN sh /tmp/ensure-jerboa.sh "$JERBOA_VERSION" /usr/local/bin && \ command -v jerbuild && \ jerbuild --version && \ @@ -54,7 +61,9 @@ RUN sh /tmp/ensure-jerboa.sh "$JERBOA_VERSION" /usr/local/bin && \ COPY . /src WORKDIR /src -RUN BUNDLE_QT=1 bash packaging/linux/build-tarball.sh && \ +RUN BUNDLE_QT=1 JBROWSER_SOURCE_COMMIT="$JBROWSER_SOURCE_COMMIT" \ + JBROWSER_SOURCE_TREE="$JBROWSER_SOURCE_TREE" \ + bash packaging/linux/build-tarball.sh && \ mkdir -p /out && \ cp qt-webengine/build/jerboa-browser-*-linux-*.tar.gz \ /out/jerboa-browser-static-qt.tar.gz && \ @@ -70,6 +79,8 @@ RUN BUNDLE_QT=1 bash packaging/linux/build-tarball.sh && \ tar tzf "$tb" | grep -E '/libexec/QtWebEngineProcess$' >/dev/null && \ tar tzf "$tb" | grep -E '/libexec/qt.conf$' >/dev/null && \ tar tzf "$tb" | grep -E '/resources/qtwebengine_resources\.pak$' >/dev/null && \ + tar tzf "$tb" | grep -E '/PROVENANCE\.txt$' >/dev/null && \ + tar tzf "$tb" | grep -E '/SOURCE-SHA256\.txt$' >/dev/null && \ bin_path="$(tar tzf "$tb" | grep -E '/bin/jerboa-browser\.bin$' | head -n 1)" && \ tar xzf "$tb" -O "$bin_path" > /tmp/jerboa-browser && \ file /tmp/jerboa-browser --- a/Makefile +++ b/Makefile @@ -1,23 +1,19 @@ # jerbuild bundles Chez Scheme + the jerboa stdlib, so building the browser # entry needs only `jerbuild` + a C compiler. CI uses a pinned project-local # release toolchain; developer machines can still override JERBUILD explicitly. -JERBOA_VERSION ?= v0.2.3 +JERBOA_VERSION ?= v0.2.4 +JERBOA_BUNDLE_SHA256 ?= 64efc7750859ac555905204cec289454c83642e4ea9b8f673985cf0fcead6194 JERBOA_TOOL_DIR ?= $(CURDIR)/.jerboa/bin -JERBOA_SIBLING_TOOL_DIR ?= $(CURDIR)/../jerboa/dist -JERBUILD ?= $(shell if [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then \ - printf '%s\n' "$(JERBOA_TOOL_DIR)/jerbuild"; \ -elif [ -x "$(JERBOA_SIBLING_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_SIBLING_TOOL_DIR)/jerboa" ]; then \ - printf '%s\n' "$(JERBOA_SIBLING_TOOL_DIR)/jerbuild"; \ -elif command -v jerbuild >/dev/null 2>&1 && command -v jerboa >/dev/null 2>&1; then \ - command -v jerbuild; \ -else \ - printf '%s\n' "$(JERBOA_TOOL_DIR)/jerbuild"; \ -fi) -JH := $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null) +JERBUILD ?= $(JERBOA_TOOL_DIR)/jerbuild +# Recursive on purpose: check-jerbuild may install the pinned local tool before +# recipes expand this value. +JH = $(shell "$(JERBUILD)" --jerboa-home 2>/dev/null) JERBOA_YUBIKEY_URL ?= https://git.sr.ht/~lisp/jerboa-yubikey +JERBOA_YUBIKEY_COMMIT ?= e448afca926ca9ad40fd98effa8d6264cb1019f5 +JERBOA_YUBIKEY_TREE ?= 67e4417511b63c0b41b1b9c5dc1c7d897df51f08 JERBOA_YUBIKEY_DIR := vendor/jerboa-yubikey JERBOA_YUBIKEY_LIB := $(JERBOA_YUBIKEY_DIR)/lib -JEXEC := $(JERBUILD) exec --libdirs $(CURDIR)/$(JERBOA_YUBIKEY_LIB):$(CURDIR)/scheme:$(CURDIR)/vendor/jerboa-fuse/lib:$(JH)/lib +JEXEC = JERBOA_BROWSER_DEV_NATIVE=1 JERBOA_BROWSER_LIB=$(CURDIR)/qt-webengine/build/libjerboa_browser.$(SOEXT) $(JERBUILD) exec --libdirs $(CURDIR)/$(JERBOA_YUBIKEY_LIB):$(CURDIR)/scheme:$(CURDIR)/vendor/jerboa-fuse/lib:$(JH)/lib JERBUILD_ERROR := "ERROR: jerbuild unavailable (or '$(JERBUILD) --jerboa-home' failed). Run make ensure-jerboa-tools, install jerbuild, or set JERBUILD=/path/to/jerbuild." BIN_DIR ?= $(HOME)/.local/bin BIN ?= jerboa-browser @@ -30,70 +26,92 @@ SANDBOX_DIR ?= dist/webengine-sandbox CC ?= cc CARGO ?= cargo CARGO_AUDIT ?= $(shell command -v cargo-audit 2>/dev/null || printf '%s/.cargo/bin/cargo-audit' "$$HOME") +CARGO_AUDIT_VERSION ?= 0.22.2 PODMAN ?= podman PODMAN_PLATFORM ?= linux/arm64 PODMAN_BUILD_FLAGS ?= -STATIC_QT_BASE_IMAGE ?= docker.io/library/ubuntu:24.04 +STATIC_QT_BASE_IMAGE ?= docker.io/library/ubuntu:24.04@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90 STATIC_QT_IMAGE ?= jerboa-browser-qt-builder STATIC_QT_TARBALL ?= jerboa-browser-static-qt-linux-arm64.tar.gz +STATIC_QT_APT_SNAPSHOT ?= 20260701T000000Z # Modules that use (std crypto native-rust) need libjerboa_native dlopen-able at # runtime — in the interpreter (jerbuild exec: tests, repl) AND in the compiled -# binary: jerbuild does not statically register the Rust crypto symbols, so the -# binary dlopens the lib too (else it aborts with "no entry for -# jerboa_last_error"). native-rust probes ./lib/libjerboa_native.* (CWD-relative) -# and the dynamic loader path, so stage a symlink under ./lib from where jerbuild -# built it (its cache, after `make binary`) or a sibling jerboa checkout. (The -# static-qt tarball bundles a copy in lib/; its launcher adds lib/ to -# LD_LIBRARY_PATH so the binary finds it from any CWD.) +# binary. Stage a symlink under ./lib from jerbuild's cache; release project +# DSOs are located from the registered canonical executable path, never CWD or +# ambient dynamic-loader variables. SOEXT := $(if $(filter Darwin,$(shell uname -s)),dylib,so) FUSE_SHARED_FLAG := $(if $(filter Darwin,$(shell uname -s)),-dynamiclib,-shared) FUSE_SHIM := libjerboa_fuse_mount.so +ifeq ($(shell uname -s),Darwin) +FUSE_OPENSSL_PREFIX ?= $(shell brew --prefix openssl@4 2>/dev/null || brew --prefix openssl@3 2>/dev/null || echo /opt/homebrew/opt/openssl@4) +FUSE_OPENSSL_CFLAGS ?= -I$(FUSE_OPENSSL_PREFIX)/include +FUSE_OPENSSL_LIBS ?= -L$(FUSE_OPENSSL_PREFIX)/lib -lcrypto +else +FUSE_OPENSSL_CFLAGS ?= $(shell pkg-config --cflags openssl 2>/dev/null) +FUSE_OPENSSL_LIBS ?= $(shell pkg-config --libs openssl 2>/dev/null || printf '%s\n' -lcrypto) +endif YUBIKEY_NATIVE_SHARED := $(JERBOA_YUBIKEY_DIR)/yubikey-native/target/release/libyubikey_native.$(SOEXT) # `make binary` links the static archive from a temp dir and leaves no shared # lib behind, so build the cdylib from jerbuild's bundled crate if it isn't # already cached, then symlink it where native-rust probes (./lib, CWD-relative). -lib/libjerboa_native.$(SOEXT): | check-jerbuild +.PHONY: FORCE +FORCE: + +lib/libjerboa_native.$(SOEXT): FORCE | check-jerbuild @mkdir -p lib @src="$(JH)/jerboa-native-rs/target/release/libjerboa_native.$(SOEXT)"; \ if [ ! -f "$$src" ] && [ -f "$(JH)/jerboa-native-rs/Cargo.toml" ]; then \ echo "building libjerboa_native via cargo (one-time)..."; \ - cargo build --release --no-default-features --features crypto \ + cargo build --locked --release --no-default-features --features crypto \ --manifest-path "$(JH)/jerboa-native-rs/Cargo.toml"; \ fi; \ if [ -f "$$src" ]; then ln -sf "$$src" "$@" && echo "staged $@ -> $$src"; \ else echo "note: libjerboa_native.$(SOEXT) unavailable; crypto tests may fail"; fi -$(FUSE_SHIM): vendor/jerboa-fuse/src/mount_helper.c - $(CC) $(FUSE_SHARED_FLAG) -fPIC -O2 -o $@ $< +$(FUSE_SHIM): vendor/jerboa-fuse/src/mount_helper.c support/browser_secure_fs.c support/browser_secure_fs.h + $(CC) $(FUSE_SHARED_FLAG) -fPIC -O2 -Wall -Wextra -Werror \ + $(FUSE_OPENSSL_CFLAGS) -I support -o $@ \ + vendor/jerboa-fuse/src/mount_helper.c support/browser_secure_fs.c \ + $(FUSE_OPENSSL_LIBS) vendor-yubikey: - sh support/ensure-vendor.sh jerboa-yubikey "$(JERBOA_YUBIKEY_URL)" + sh support/ensure-vendor.sh jerboa-yubikey "$(JERBOA_YUBIKEY_URL)" \ + "$(JERBOA_YUBIKEY_COMMIT)" "$(JERBOA_YUBIKEY_TREE)" @test -f "$(JERBOA_YUBIKEY_LIB)/yubikey/auth.sls" || \ { echo "ERROR: missing $(JERBOA_YUBIKEY_LIB)/yubikey/auth.sls" >&2; exit 1; } + @cmp -s support/yubikey-native-Cargo.lock \ + "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.lock" || \ + { echo "ERROR: YubiKey Cargo lock overlay mismatch" >&2; exit 1; } lib/libyubikey_native.$(SOEXT): vendor-yubikey - $(CARGO) build --manifest-path $(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml --release --no-default-features + $(CARGO) build --locked --manifest-path $(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml --release --no-default-features @mkdir -p lib cp -f $(YUBIKEY_NATIVE_SHARED) $@ yubikey-native: lib/libyubikey_native.$(SOEXT) -.PHONY: all build binary install run test test-all test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-securestore test-adblock test-gui test-buffers test-native repl security audit sbom reproducibility-report binary-smoke webengine-sandbox-evidence verify release-evidence clean help check-jerbuild check-podman ensure-jerboa-tools static-qt podman-static-qt vendor-yubikey yubikey-native +.PHONY: all build binary install run test test-all test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-securestore test-adblock test-gui test-buffers test-native test-supply-chain test-native-loader repl security audit sbom reproducibility-report binary-smoke webengine-sandbox-evidence verify release-evidence clean help check-jerbuild check-podman ensure-jerboa-tools static-qt podman-static-qt vendor-yubikey yubikey-native .DEFAULT_GOAL := help all: binary check-jerbuild: - @test -n "$(JH)" || { echo $(JERBUILD_ERROR) >&2; exit 1; } + @if [ "$(JERBUILD)" = "$(JERBOA_TOOL_DIR)/jerbuild" ] && \ + { [ ! -x "$(JERBUILD)" ] || [ "$$(cat "$(JERBOA_TOOL_DIR)/.jerboa-version" 2>/dev/null || true)" != "$(JERBOA_VERSION)" ]; }; then \ + sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \ + fi + @"$(JERBUILD)" --jerboa-home >/dev/null 2>&1 || { echo $(JERBUILD_ERROR) >&2; exit 1; } + @if [ "$(JERBUILD)" = "$(JERBOA_TOOL_DIR)/jerbuild" ]; then \ + "$(JERBUILD)" --version | grep -q 'bundled-lib sha256=$(JERBOA_BUNDLE_SHA256)' || \ + { echo "ERROR: selected Jerboa bundle digest does not match supply-chain.lock" >&2; exit 1; }; \ + fi ensure-jerboa-tools: @if [ -x "$(JERBOA_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_TOOL_DIR)/jerboa" ]; then \ echo "=== Using downloaded Jerboa toolchain: $(JERBOA_TOOL_DIR) ==="; \ - elif [ -x "$(JERBOA_SIBLING_TOOL_DIR)/jerbuild" ] && [ -x "$(JERBOA_SIBLING_TOOL_DIR)/jerboa" ]; then \ - echo "=== Using sibling Jerboa release toolchain: $(JERBOA_SIBLING_TOOL_DIR) ==="; \ - elif command -v jerbuild >/dev/null 2>&1 && command -v jerboa >/dev/null 2>&1; then \ - echo "=== Using Jerboa toolchain from PATH: $$(command -v jerbuild) ==="; \ + elif [ -x "$(JERBUILD)" ]; then \ + echo "=== Using explicit Jerboa toolchain: $(JERBUILD) ==="; \ else \ echo "=== Fetching Jerboa $(JERBOA_VERSION) release tools into $(JERBOA_TOOL_DIR) ==="; \ sh support/ensure-jerboa.sh "$(JERBOA_VERSION)" "$(JERBOA_TOOL_DIR)"; \ @@ -135,7 +153,13 @@ test: test-securestore QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \ $(JEXEC) scheme/browser-test.ss -test-all: test test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-adblock test-gui test-buffers test-native +test-all: test test-keymap test-minibuffer test-commands test-keys test-nav test-hint test-pass test-polish test-adblock test-gui test-buffers test-native test-supply-chain + +test-supply-chain: + sh test/supply-chain-test.sh + +test-native-loader: + JERBUILD="$(JERBUILD)" bash test/native-loader-security-test.sh # Stage 8 secure-store unit tests: bookmark (de)serialize, AES-256-GCM seal/open, # RSA-OAEP wrap + simulated card unwrap, tamper detection, masked-DK zeroing. @@ -212,21 +236,27 @@ test-native: security: sh scripts/security-check.sh + JERBUILD="$(JERBUILD)" bash test/native-loader-security-test.sh -audit: +audit: vendor-yubikey @mkdir -p "$(DIST_DIR)" - @if [ -x "$(CARGO_AUDIT)" ] && [ -f qt-webengine/vendor/adblock-rust-ffi/Cargo.toml ]; then \ - cd qt-webengine/vendor/adblock-rust-ffi && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-adblock-ffi.txt"; \ - else \ - echo "cargo-audit not installed or adblock Cargo.toml missing; release owner must review before publication." > "$(DIST_DIR)/rustsec-adblock-ffi.txt"; \ - fi - @if [ -x "$(CARGO_AUDIT)" ] && [ -f "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" ]; then \ - cd "$(JERBOA_YUBIKEY_DIR)/yubikey-native" && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-yubikey-native.txt"; \ - else \ - echo "cargo-audit not installed or YubiKey native Cargo.toml missing; release owner must review before publication." > "$(DIST_DIR)/rustsec-yubikey-native.txt"; \ - fi - -sbom: + @test -x "$(CARGO_AUDIT)" || { echo "cargo-audit is required" >&2; exit 1; } + @"$(CARGO_AUDIT)" --version | grep -q " $(CARGO_AUDIT_VERSION)$$" || \ + { echo "cargo-audit $(CARGO_AUDIT_VERSION) is required" >&2; exit 1; } + @test -f qt-webengine/vendor/adblock-rust-ffi/Cargo.lock + @cd qt-webengine/vendor/adblock-rust-ffi && \ + "$(CARGO_AUDIT)" audit --deny warnings > "$(CURDIR)/$(DIST_DIR)/rustsec-adblock-ffi.txt" + @test -f "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.lock" + @cd "$(JERBOA_YUBIKEY_DIR)/yubikey-native" && \ + "$(CARGO_AUDIT)" audit --deny warnings > "$(CURDIR)/$(DIST_DIR)/rustsec-yubikey-native.txt" + @test -f "$(JH)/jerboa-native-rs/Cargo.lock" + @cd "$(JH)/jerboa-native-rs" && \ + "$(CARGO_AUDIT)" audit --deny warnings > "$(CURDIR)/$(DIST_DIR)/rustsec-jerboa-native.txt" + @if [ -d "$$HOME/.cargo/advisory-db/.git" ]; then \ + git -C "$$HOME/.cargo/advisory-db" rev-parse HEAD > "$(DIST_DIR)/rustsec-advisory-db-commit.txt"; \ + else echo unavailable > "$(DIST_DIR)/rustsec-advisory-db-commit.txt"; fi + +sbom: vendor-yubikey lib/libjerboa_native.$(SOEXT) JBROWSER_SBOM_DIR="$(SBOM_DIR)" \ JERBUILD="$(JERBUILD)" \ BIN="$(BIN)" \ @@ -276,9 +306,9 @@ release-evidence: verify command -v "$(CARGO)" >/dev/null 2>&1 && "$(CARGO)" --version || true; \ } > "$(DIST_DIR)/build-environment.txt" @QT_QPA_PLATFORM=offscreen QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu ./jerboa-browser version > "$(DIST_DIR)/browser-version.txt" 2>&1 || true - @find scheme qt-webengine/src qt-webengine/tests include support packaging -type f \( -name '*.ss' -o -name '*.sls' -o -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.sh' -o -name '*.md' -o -name 'CMakeLists.txt' \) -print | sort | xargs shasum -a 256 > "$(DIST_DIR)/source-sha256.txt" - @find qt-webengine/vendor/adblock-rust-ffi "$(JERBOA_YUBIKEY_DIR)/yubikey-native" -maxdepth 2 -type f \( -name Cargo.toml -o -name Cargo.lock \) -print 2>/dev/null | sort | xargs shasum -a 256 > "$(DIST_DIR)/native-cargo-sha256.txt" - @shasum -a 256 Makefile .jerbuild .jerboa/security.json SECURITY.md docs/threat-model.md docs/credential-handling.md docs/ffi-boundary.md docs/webengine-sandbox.md docs/release-evidence.md scripts/security-check.sh scripts/sbom.sh scripts/reproducibility-report.sh scripts/binary-smoke.sh scripts/webengine-sandbox-evidence.sh scripts/sanitize-evidence.sh > "$(DIST_DIR)/release-inputs-sha256.txt" + @sh scripts/source-manifest.sh > "$(DIST_DIR)/source-sha256.txt" + @shasum -a 256 qt-webengine/vendor/adblock-rust-ffi/Cargo.toml qt-webengine/vendor/adblock-rust-ffi/Cargo.lock "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" support/yubikey-native-Cargo.lock > "$(DIST_DIR)/native-cargo-sha256.txt" + @shasum -a 256 Makefile .jerbuild .jerboa/security.json supply-chain.lock rust-toolchain.toml SECURITY.md docs/threat-model.md docs/credential-handling.md docs/ffi-boundary.md docs/webengine-sandbox.md docs/release-evidence.md scripts/security-check.sh scripts/sbom.sh scripts/reproducibility-report.sh scripts/binary-smoke.sh scripts/webengine-sandbox-evidence.sh scripts/sanitize-evidence.sh support/jerboa-release-sha256.txt support/ensure-rust.sh > "$(DIST_DIR)/release-inputs-sha256.txt" @shasum -a 256 "$(BIN)" > "$(DIST_DIR)/binary-sha256.txt" @if command -v otool >/dev/null 2>&1; then \ { otool -L "$(BIN)" 2>/dev/null || true; otool -L qt-webengine/build/libjerboa_browser.* 2>/dev/null || true; otool -L lib/libyubikey_native.* 2>/dev/null || true; } > "$(DIST_DIR)/native-linkage.txt"; \ @@ -288,21 +318,16 @@ release-evidence: verify echo "no otool/ldd available" > "$(DIST_DIR)/native-linkage.txt"; \ fi @if command -v "$(CARGO)" >/dev/null 2>&1 && [ -f qt-webengine/vendor/adblock-rust-ffi/Cargo.toml ]; then \ - "$(CARGO)" metadata --format-version 1 --manifest-path qt-webengine/vendor/adblock-rust-ffi/Cargo.toml > "$(DIST_DIR)/cargo-metadata-adblock-ffi.json"; \ + "$(CARGO)" metadata --locked --format-version 1 --manifest-path qt-webengine/vendor/adblock-rust-ffi/Cargo.toml > "$(DIST_DIR)/cargo-metadata-adblock-ffi.json"; \ fi @if command -v "$(CARGO)" >/dev/null 2>&1 && [ -f "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" ]; then \ - "$(CARGO)" metadata --format-version 1 --manifest-path "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" > "$(DIST_DIR)/cargo-metadata-yubikey-native.json"; \ - fi - @if [ -x "$(CARGO_AUDIT)" ] && [ -f qt-webengine/vendor/adblock-rust-ffi/Cargo.toml ]; then \ - cd qt-webengine/vendor/adblock-rust-ffi && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-adblock-ffi.txt"; \ - else \ - echo "cargo-audit not installed or adblock Cargo.toml missing; release owner must review before publication." > "$(DIST_DIR)/rustsec-adblock-ffi.txt"; \ - fi - @if [ -x "$(CARGO_AUDIT)" ] && [ -f "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" ]; then \ - cd "$(JERBOA_YUBIKEY_DIR)/yubikey-native" && "$(CARGO_AUDIT)" audit > "$(CURDIR)/$(DIST_DIR)/rustsec-yubikey-native.txt"; \ - else \ - echo "cargo-audit not installed or YubiKey native Cargo.toml missing; release owner must review before publication." > "$(DIST_DIR)/rustsec-yubikey-native.txt"; \ + "$(CARGO)" metadata --locked --format-version 1 --manifest-path "$(JERBOA_YUBIKEY_DIR)/yubikey-native/Cargo.toml" > "$(DIST_DIR)/cargo-metadata-yubikey-native.json"; \ fi + @test -x "$(CARGO_AUDIT)" + @cd qt-webengine/vendor/adblock-rust-ffi && "$(CARGO_AUDIT)" audit --deny warnings > "$(CURDIR)/$(DIST_DIR)/rustsec-adblock-ffi.txt" + @cd "$(JERBOA_YUBIKEY_DIR)/yubikey-native" && "$(CARGO_AUDIT)" audit --deny warnings > "$(CURDIR)/$(DIST_DIR)/rustsec-yubikey-native.txt" + @cd "$(JH)/jerboa-native-rs" && "$(CARGO_AUDIT)" audit --deny warnings > "$(CURDIR)/$(DIST_DIR)/rustsec-jerboa-native.txt" + @if [ -d "$$HOME/.cargo/advisory-db/.git" ]; then git -C "$$HOME/.cargo/advisory-db" rev-parse HEAD > "$(DIST_DIR)/rustsec-advisory-db-commit.txt"; else echo unavailable > "$(DIST_DIR)/rustsec-advisory-db-commit.txt"; fi @rm -rf "$(DIST_DIR)/sbom" "$(DIST_DIR)/reproducibility" "$(DIST_DIR)/binary-smoke" "$(DIST_DIR)/webengine-sandbox" @cp -R "$(SBOM_DIR)" "$(DIST_DIR)/sbom" @cp -R "$(REPRO_DIR)" "$(DIST_DIR)/reproducibility" @@ -329,10 +354,15 @@ static-qt: podman-static-qt podman-static-qt: check-podman @echo "=== Building jerboa-browser Linux Qt artifact with Podman ===" + @test -z "$$(git status --porcelain --untracked-files=all)" || \ + { echo "static release packaging requires a clean tree" >&2; exit 1; } $(PODMAN) pull --platform $(PODMAN_PLATFORM) $(STATIC_QT_BASE_IMAGE) $(PODMAN) build $(PODMAN_BUILD_FLAGS) --platform $(PODMAN_PLATFORM) \ --build-arg BASE_IMAGE="$(STATIC_QT_BASE_IMAGE)" \ --build-arg JERBOA_VERSION="$(JERBOA_VERSION)" \ + --build-arg APT_SNAPSHOT="$(STATIC_QT_APT_SNAPSHOT)" \ + --build-arg JBROWSER_SOURCE_COMMIT="$$(git rev-parse HEAD)" \ + --build-arg JBROWSER_SOURCE_TREE="$$(git rev-parse 'HEAD^{tree}')" \ -f Dockerfile.qt -t $(STATIC_QT_IMAGE) . @id=$$($(PODMAN) create $(STATIC_QT_IMAGE)) && \ trap '$(PODMAN) rm $$id >/dev/null 2>&1 || true' EXIT INT TERM && \ @@ -387,11 +417,11 @@ help: @echo " make binary-smoke start the binary through offscreen version" @echo " make webengine-sandbox-evidence record WebEngine sandbox status" @echo " make release-evidence write release evidence under dist/release-evidence" - @echo " make vendor-yubikey clone jerboa-yubikey into vendor/" + @echo " make vendor-yubikey verify/fetch locked jerboa-yubikey into vendor/" @echo " make yubikey-native build lib/libyubikey_native for direct USB" @echo " make test run the (browser) test suite (interpreter)" @echo " make test-gui offline Qt GUI/snapshot harness (offscreen, headless)" @echo " make repl REPL with (browser) preloaded (interpreter)" @echo " make clean remove build artifacts" @echo "" - @echo " env: JERBOA_HOME (default ../jerboa), SCHEME, JERBOA_BROWSER_LIB" + @echo " dev env: JERBOA_BROWSER_DEV_NATIVE=1 with canonical JERBOA_BROWSER_LIB" --- a/README.md +++ b/README.md @@ -115,7 +115,8 @@ access controller trusts only the browser PID and descendants such as is not reused by the browser. Defaults: - Vault file: `$XDG_DATA_HOME/jerboa-browser/browser.vault` -- Mountpoint: `$XDG_RUNTIME_DIR/jerboa-browser/vault-<browser-pid>` (or `/tmp`) +- Mountpoint: a fresh 0700 CSPRNG-named directory directly under a validated + `$XDG_RUNTIME_DIR` (or the platform's canonical sticky temporary directory) - Size: `JERBOA_BROWSER_VAULT_BLOCKS` blocks, default 65536 (256 MiB) The production default is YubiKey PIV unlock. With `JERBOA_BROWSER_VAULT=1`, the @@ -139,12 +140,23 @@ The browser intentionally does not accept a vault passphrase through environment variables and does not embed a build-local fallback secret. Use the YubiKey PIV default, a protected passphrase file, or the interactive prompt. +An explicit `JERBOA_BROWSER_VAULT_MOUNT` must be an absolute, new path beneath +a trusted parent; pre-existing paths and symlinks are rejected. The vault data +directory is owner-only, and an existing vault file must be a 0600 regular file +owned by the current user with exactly one link. + +URL authority is scheme-specific. `network` permits only `http`, `https`, `ws`, +and `wss`; it never implies local content or files. Application-created `data`, +`blob`, `qrc`, and `about:blank` pages require `local-content`. File URLs require +`filesystem` plus an absolute `JERBOA_BROWSER_FILESYSTEM_ROOT`, and targets must +canonicalize beneath that root. + ## Build / test ```sh make binary # the self-contained native ./jerboa-browser make static-qt # bundled Linux Qt/WebEngine tarball via Podman (linux/arm64) -make vendor-yubikey # clone https://git.sr.ht/~lisp/jerboa-yubikey into vendor/ +make vendor-yubikey # verify/fetch the locked YubiKey commit and tree make yubikey-native # build lib/libyubikey_native for direct USB YubiKey access make test # (browser) suite + secure-store unit tests; also test-{keymap,…,polish} make verify # full test set, security checks, and dependency audit evidence --- a/SECURITY.md +++ b/SECURITY.md @@ -20,7 +20,7 @@ requires: - The Qt WebEngine backend builds and its native tests pass on every supported target OS. - RustSec/native dependency audits are clean or documented as accepted release - risks. + risks; missing audit tooling and RustSec warnings fail closed. - Target-OS evidence confirms the Chromium/Qt WebEngine sandbox is enabled. Run WebEngine evidence with `JBROWSER_RUN_WEBENGINE_PROCESS_SMOKE=1`, `JBROWSER_REQUIRE_WEBENGINE_PROCESS_SMOKE=1`, @@ -35,6 +35,9 @@ requires: - Web content is hostile. Do not grant network, downloads, clipboard, file, or persistent-storage capabilities unless the caller explicitly requested them. +- Network authority permits only HTTP(S)/WS(S). Local synthetic content and + canonical-rooted file reads require their own capabilities; custom and + JavaScript navigation schemes remain denied. - The default WebEngine profile must remain off-record. Persistent cookies, cache, local storage, and downloads require the browser-owned encrypted vault. - Do not accept browser-profile vault secrets through process environment @@ -47,6 +50,9 @@ requires: `(chezscheme)` imports are denied in production/test source. - Native boundaries must be documented, length-checked, and covered by release evidence. +- Release inputs must match `supply-chain.lock`. Do not replace pinned commits, + trees, OCI digests, installer hashes, action SHAs, or compiler versions with + tags/default branches or co-hosted checksums. ## Reporting --- a/build-binary.ss +++ b/build-binary.ss @@ -174,6 +174,9 @@ (fprintf o "#include \"scheme.h\"\n") (fprintf o "#include \"jb_petite_boot.h\"\n#include \"jb_scheme_boot.h\"\n") (fprintf o "#include \"jb_boot.h\"\n#include \"jb_program.h\"\n\n") + (fprintf o "static char jwb_bootstrap_exe[PATH_MAX];\n") + (fprintf o "static const char *jwb_bootstrap_executable_path(void) {\n") + (fprintf o " return jwb_bootstrap_exe[0] ? jwb_bootstrap_exe : NULL;\n}\n\n") (fprintf o "int main(int argc, char *argv[]) {\n") (fprintf o " /* Record the real exe path so (browser) finds its sibling lib/:\n") (fprintf o " the embedded program runs from a temp file below, so argv[0]\n") @@ -181,10 +184,10 @@ (fprintf o " { char exe[PATH_MAX];\n") (fprintf o "#if defined(__linux__)\n") (fprintf o " ssize_t en = readlink(\"/proc/self/exe\", exe, sizeof(exe)-1);\n") - (fprintf o " if (en > 0) { exe[en] = 0; setenv(\"JERBOA_BROWSER_EXE\", exe, 1); }\n") + (fprintf o " if (en > 0) { exe[en] = 0; if (!realpath(exe, jwb_bootstrap_exe)) return 1; }\n") (fprintf o "#elif defined(__APPLE__)\n") (fprintf o " uint32_t esz = sizeof(exe);\n") - (fprintf o " if (_NSGetExecutablePath(exe, &esz) == 0) setenv(\"JERBOA_BROWSER_EXE\", exe, 1);\n") + (fprintf o " if (_NSGetExecutablePath(exe, &esz) == 0 && !realpath(exe, jwb_bootstrap_exe)) return 1;\n") (fprintf o "#endif\n") (fprintf o " }\n") (fprintf o " char prog_path[256];\n") @@ -196,6 +199,7 @@ (fprintf o " perror(\"write\"); close(fd); unlink(prog_path); return 1; }\n") (fprintf o " close(fd);\n") (fprintf o " Sscheme_init(NULL);\n") + (fprintf o " Sforeign_symbol(\"jwb_bootstrap_executable_path\", (void *)jwb_bootstrap_executable_path);\n") (fprintf o " Sregister_boot_file_bytes(\"petite\", (void*)petite_boot_data, petite_boot_size);\n") (fprintf o " Sregister_boot_file_bytes(\"scheme\", (void*)scheme_boot_data, scheme_boot_size);\n") (fprintf o " Sregister_boot_file_bytes(\"jerboa-browser\", (void*)jb_boot_data, jb_boot_size);\n") --- a/docs/ffi-boundary.md +++ b/docs/ffi-boundary.md @@ -21,6 +21,9 @@ FFI expectations: - `libjerboa_browser` wraps Qt WebEngine and the adblock-rust FFI. - `libyubikey_native` handles direct USB CCID/FIDO2 support when present. - `libjerboa_fuse_mount.so` is the browser-owned encrypted vault mount helper. + It also exports fixed-signature descriptor-relative secure-filesystem wrappers + for private runtime directories and vault files; Scheme never calls variadic + `open(2)` for that boundary. - `libjerboa_native` provides Rust crypto used by secure-store tests and builds. All native dependencies must be represented in release evidence with source or @@ -38,3 +41,9 @@ Release review should re-check: - UTF-8/NUL/length handling for URL, JavaScript, title, status, and path inputs. - Adblock FFI pointer cleanup for redirect buffers. - Context destruction order: views before profile/interceptor cleanup. + The native handle registry records the parent context and rejects context + release while any child view remains, preventing profile use-after-free even + when a caller violates the documented order. +- Scheme-by-scheme URL policy at direct-load, navigation/redirect, request, and + download boundaries. +- Vault path component, owner, permission, type, and link-count validation. new file mode 100644 --- /dev/null +++ b/docs/finding-closure.md @@ -0,0 +1,75 @@ +# 56sol Finding Closure + +This repository closes the applicable project security-audit findings as +follows. Release evidence is valid only for the exact source and dependency +hashes emitted by `make release-evidence`. + +## BRW-001 — network capability could load local files + +- `browser_url_policy.cpp` is the single scheme-authority decision point. + `NETWORK` permits only HTTP(S) and WS(S). `LOCAL_CONTENT`, `FILESYSTEM`, and + `DEVTOOLS` are independent bits; unknown, JavaScript, extension, and custom + schemes fail closed. +- `FILESYSTEM` requires an existing absolute non-symlink + `JERBOA_BROWSER_FILESYSTEM_ROOT`. Every file target is canonicalized and + checked beneath that root. Remote file authorities are forbidden. +- The policy is enforced before direct loads, for navigation/redirects in the + page subclass, for all profile requests (frames, workers, XHR, WebSockets, + and subresources), and again for downloads. +- `jwb_url_policy_test` exercises the hostile scheme/capability matrix, + traversal, outside-root, remote-file, and symlink cases. `browser-test.ss` + dynamically proves a network-only renderer cannot load `/etc/hosts`, data, + qrc, or JavaScript URLs. + +## BRW-002 — predictable and untrusted vault paths + +- The fallback is a 128-bit CSPRNG directory created descriptor-relatively + beneath a verified sticky temporary directory; no username or PID appears in + its name. An explicit mount override is exclusive and must not pre-exist. +- `browser_secure_fs.c` walks every component with held directory descriptors + and `O_NOFOLLOW`. Runtime directories are 0700. Vault files are created with + `O_EXCL|O_NOFOLLOW|O_CLOEXEC`, mode 0600, and existing files must be regular, + owned by the effective uid, and single-link. +- The vendored blockstore now calls fixed-signature wrappers rather than + variadic libc `open(2)`. Mount directories are removed through a verified + parent descriptor after unmount. +- `jwb_secure_fs_test` covers unpredictability, exact modes, pre-existing + paths, symlinked parents/finals, hard links, wrong modes, cleanup, and + fail-closed secret input without a controlling terminal. PIN/passphrase + prompts save, disable, and exactly restore termios through `/dev/tty`; they no + longer invoke a shell or accept redirected stdin. +- The no-controlling-terminal probe uses `posix_spawn` to enter a fresh test + image before calling the secret reader; it does not resume C++ after raw + `fork`. +- Passphrase files are opened through the same no-follow descriptor walk and + must be bounded 0600, owner-only, single-link regular files. + +## Supply, dependency, and assurance closure + +- `support/jerboa-release-sha256.txt` is a consumer-owned trust root for Jerboa + tools; a checksum downloaded beside an archive is never trusted. +- The default build uses only that project-local pinned toolchain; sibling and + ambient PATH tool selection was removed. +- YubiKey and SourceHut Jerboa source fetches require exact commits and Git tree + IDs. Existing vendor snapshots carry an exact lock. Default branches cannot + enter builds (`SUPPLY-01`, `SUPPLY-05`). +- Ubuntu is pinned by OCI digest and an immutable signed apt snapshot; + Rust/rustup/cargo-audit are exact versions, + and rustup-init is verified against per-architecture consumer-owned hashes + (`SUPPLY-01`, `SUPPLY-03`, `ASSURANCE-01`). +- GitHub actions use full commit IDs with persisted checkout credentials + disabled (`SUPPLY-04`). +- The consumer-owned YubiKey lock upgrades `anyhow` to 1.0.103 and release audits deny + RustSec warnings (`DEP-01`). +- `supply-chain.lock`, Cargo locks, native/source hashes, compiler and system + package versions, resolved Cargo metadata, and the RustSec database commit + are retained in the SBOM/evidence bundle (`ASSURANCE-01`). Secret scans are + explicitly a covered-pattern gate, not proof that arbitrary credentials are + absent (`ASSURANCE-02`). +- Linux archives embed a complete source manifest and provenance record. SBOM + generation treats arbitrary or stale tarballs as unverified unless commit, + tree, source bytes, and supply lock all match the current release candidate. + +`test/supply-chain-test.sh` exercises hostile vendor-lock and transport cases; +`scripts/security-check.sh` rejects mutable action refs, mutable Rust +bootstraps, unpinned container bases, and missing lock material. --- a/docs/release-evidence.md +++ b/docs/release-evidence.md @@ -66,8 +66,13 @@ Expected files include: - `binary-sha256.txt` - `native-linkage.txt` - Cargo metadata for adblock and YubiKey native crates when available -- RustSec audit outputs for adblock and YubiKey native crates when - `cargo-audit` is installed +- `supply-chain.lock`, its hash, toolchain versions, resolved system package + versions, and the RustSec advisory database commit +- current and packaged source manifests plus packaged `PROVENANCE.txt`; a + pre-existing static Qt tarball is labeled `present-unverified` unless its + commit, tree, full source manifest, and supply lock match the current source +- warning-denying RustSec audit outputs for adblock, YubiKey native, and the + resolved Jerboa native crypto crate - `sbom/` - `reproducibility/` - `binary-smoke/` @@ -77,6 +82,10 @@ Expected files include: supplied - `result.txt` with `release_evidence_status=pass` -Release owners must treat missing `cargo-audit`, skipped Qt native tests, or -missing target-OS sandbox/process evidence as production blockers, even when the -local gate exits successfully for developer convenience. +Missing the pinned `cargo-audit` now fails the gate. Skipped Qt native tests or +missing target-OS sandbox/process evidence remain production blockers; target +sandbox proof is intentionally collected only on a matching release host. + +The high-confidence secret scan covers the formats named in the security gate; +it is not evidence that arbitrary raw application credentials cannot exist. +Review credential-bearing files and binary smoke evidence separately. --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -38,8 +38,12 @@ vendored YubiKey/FUSE dependencies. `JERBOA_BROWSER_VAULT=1`, which mounts a browser-owned encrypted FUSE vault. - Chromium/Qt WebEngine sandbox must remain enabled. Release source must not set or document `QTWEBENGINE_DISABLE_SANDBOX`. -- Network, downloads, popups, local-file access, clipboard access, and - persistent storage are capability-gated per context. +- Network, application-local content, downloads, popups, canonical-rooted + local-file access, clipboard access, and persistent storage are independently + capability-gated per context. +- Vault runtime paths are random owner-only directories. Host path traversal is + descriptor-relative/no-follow; vault files are exclusive 0600 single-link + files owned by the effective user. - Passwords are RAM-only, masked in heap storage, and wiped after transient reconstitution. - Bookmark persistence uses AES-256-GCM with a random data key wrapped to a @@ -78,3 +82,5 @@ vendored YubiKey/FUSE dependencies. `status=blocked-target-proof`. - No repeated binary reproducibility, SBOM/provenance, or no-secret binary startup evidence for the release candidate. +- Any mismatch against `supply-chain.lock`, any mutable CI action/toolchain + bootstrap, or any RustSec warning. --- a/include/jerboa_browser.h +++ b/include/jerboa_browser.h @@ -51,7 +51,10 @@ typedef enum { JWB_CAP_DOWNLOADS = 1u << 3, JWB_CAP_PERSISTENT_STORAGE = 1u << 4, JWB_CAP_DEVTOOLS = 1u << 5, - JWB_CAP_POPUPS = 1u << 6 + JWB_CAP_POPUPS = 1u << 6, + /* In-process content schemes (data/blob/qrc and inert about:blank). This is + * deliberately separate from NETWORK and FILESYSTEM authority. */ + JWB_CAP_LOCAL_CONTENT = 1u << 7 } JwbCapabilityFlags; typedef void (*JwbStringCallback)(const uint8_t *ptr, --- a/packaging/README.md +++ b/packaging/README.md @@ -115,13 +115,14 @@ produce the executable. With `BUNDLE_QT=1`, the package also stages Qt libraries, platform plugins, `QtWebEngineProcess`, WebEngine `.pak` resources, ICU data, locales, and the -shared-library dependency closure under the package root. The launcher -`bin/jerboa-browser` sets `LD_LIBRARY_PATH`, `QT_PLUGIN_PATH`, -`QML2_IMPORT_PATH`, `QT_TRANSLATIONS_PATH`, `QTWEBENGINEPROCESS_PATH`, -`QTWEBENGINE_RESOURCES_PATH`, `QTWEBENGINE_LOCALES_PATH`, and -`JERBOA_FUSE_MOUNT_LIB`, then execs `bin/jerboa-browser.bin`. `qt.conf` files -next to the native binary and `QtWebEngineProcess` point Qt at the package root. -The Chromium sandbox remains enabled. +shared-library dependency closure under the package root. The launcher sets +fixed bundle-local Qt plugin, QML, translation, helper, resource, and locale +paths, then execs `bin/jerboa-browser.bin`. Project DSOs are selected from the +native bootstrap's canonical executable path and validated before loading; the +launcher does not set `LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`, or project-library +override variables. `qt.conf` files next to the native binary and +`QtWebEngineProcess` point Qt at the package root. The Chromium sandbox +remains enabled. Browser profile persistence is opt-in with `JERBOA_BROWSER_VAULT=1`. The browser mounts its own encrypted vault and points Qt cache/storage paths inside --- a/packaging/linux/build-tarball.sh +++ b/packaging/linux/build-tarball.sh @@ -21,6 +21,14 @@ JERBOA_HOME="${JERBOA_HOME:-$HOME/mine/jerboa}" SCHEME="${SCHEME:-$JERBOA_HOME/.chez/bin/scheme}" BUNDLE_QT="${BUNDLE_QT:-0}" +sha256_value() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + copy_tree_if_exists() { local src="$1" dst="$2" if [ -d "$src" ]; then @@ -158,17 +166,13 @@ bundle_qt_runtime() { #!/bin/sh set -eu APPDIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" -export LD_LIBRARY_PATH="$APPDIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" export QT_PLUGIN_PATH="$APPDIR/plugins" -export QML2_IMPORT_PATH="$APPDIR/qml${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" -export QT_TRANSLATIONS_PATH="$APPDIR/translations${QT_TRANSLATIONS_PATH:+:$QT_TRANSLATIONS_PATH}" -export JERBOA_BROWSER_LIB="${JERBOA_BROWSER_LIB:-$APPDIR/lib/libjerboa_browser.so}" -export QTWEBENGINEPROCESS_PATH="${QTWEBENGINEPROCESS_PATH:-$APPDIR/libexec/QtWebEngineProcess}" -export QTWEBENGINE_RESOURCES_PATH="${QTWEBENGINE_RESOURCES_PATH:-$APPDIR/resources}" -export QTWEBENGINE_LOCALES_PATH="${QTWEBENGINE_LOCALES_PATH:-$APPDIR/resources/qtwebengine_locales}" -export QTWEBENGINE_DICTIONARIES_PATH="${QTWEBENGINE_DICTIONARIES_PATH:-$APPDIR/resources/qtwebengine_dictionaries}" -export JERBOA_FUSE_MOUNT_LIB="${JERBOA_FUSE_MOUNT_LIB:-$APPDIR/lib/libjerboa_fuse_mount.so}" -export JERBOA_BROWSER_EXE="${JERBOA_BROWSER_EXE:-$APPDIR/bin/jerboa-browser.bin}" +export QML2_IMPORT_PATH="$APPDIR/qml" +export QT_TRANSLATIONS_PATH="$APPDIR/translations" +export QTWEBENGINEPROCESS_PATH="$APPDIR/libexec/QtWebEngineProcess" +export QTWEBENGINE_RESOURCES_PATH="$APPDIR/resources" +export QTWEBENGINE_LOCALES_PATH="$APPDIR/resources/qtwebengine_locales" +export QTWEBENGINE_DICTIONARIES_PATH="$APPDIR/resources/qtwebengine_dictionaries" exec "$APPDIR/bin/jerboa-browser.bin" "$@" EOF chmod +x "$STAGE/bin/jerboa-browser" "$STAGE/bin/jerboa-browser.bin" @@ -289,18 +293,17 @@ if ( cd "$REPO" && make check-jerbuild >/dev/null 2>&1 ); then # The binary re-execs its embedded program from a temp file and dlopens # the Qt backend (libjerboa_browser) + the Rust crypto lib # (libjerboa_native, for the secure store) at runtime. jerbuild's - # launcher does not export JERBOA_BROWSER_EXE, so the in-binary "find the - # lib next to me" probe lands in /tmp. Install the real ELF as - # bin/jerboa-browser.bin behind a small launcher that points the loader - # at the bundled lib/ regardless of the caller's CWD — so the tarball is - # runnable as-is (the whole point of `make static-qt`). + # embedded bootstrap registers the canonical executable path, so + # project DSOs are selected from the adjacent bundle without CWD or + # ambient loader-policy variables. Keep the real ELF behind a small + # relocatable launcher for symlink resolution. cp -f "$REPO/jerboa-browser" "$STAGE/bin/jerboa-browser.bin" chmod +x "$STAGE/bin/jerboa-browser.bin" cat > "$STAGE/bin/jerboa-browser" <<'WRAP' #!/bin/sh # Launcher for the relocatable Jerboa Browser tarball: resolve our install dir -# (following symlinks) and point the native binary at the bundled lib/ before -# exec'ing it. Override JERBOA_BROWSER_LIB to use a different Qt backend build. +# (following symlinks) before exec'ing it. The native bootstrap locates and +# validates the adjacent lib/ directory itself. set -eu self=$0 while [ -L "$self" ]; do @@ -311,8 +314,6 @@ while [ -L "$self" ]; do esac done here=$(CDPATH= cd -- "$(dirname -- "$self")/.." && pwd) -export JERBOA_BROWSER_LIB="${JERBOA_BROWSER_LIB:-$here/lib/libjerboa_browser.so}" -export LD_LIBRARY_PATH="$here/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" exec "$here/bin/jerboa-browser.bin" "$@" WRAP chmod +x "$STAGE/bin/jerboa-browser" @@ -440,6 +441,40 @@ Encrypted browser vault: JERBOA_BROWSER_VAULT_YUBIKEY_PROVISION=1. EOF +echo "==> recording source provenance" +sh "$REPO/scripts/source-manifest.sh" "$REPO" > "$STAGE/SOURCE-SHA256.txt" +source_manifest_sha256=$(sha256_value "$STAGE/SOURCE-SHA256.txt") +supply_chain_lock_sha256=$(sha256_value "$REPO/supply-chain.lock") +if git -C "$REPO" rev-parse --git-dir >/dev/null 2>&1; then + source_commit=$(git -C "$REPO" rev-parse HEAD) + source_tree=$(git -C "$REPO" rev-parse 'HEAD^{tree}') + if [ -n "$(git -C "$REPO" status --porcelain --untracked-files=all)" ]; then + source_state=dirty + else + source_state=clean + fi +else + source_commit=${JBROWSER_SOURCE_COMMIT:-} + source_tree=${JBROWSER_SOURCE_TREE:-} + [ "${#source_commit}" -eq 40 ] && [ "${#source_tree}" -eq 40 ] || { + echo "container builds require JBROWSER_SOURCE_COMMIT and JBROWSER_SOURCE_TREE" >&2 + exit 1 + } + source_state=declared-container-context +fi +if [ "$source_state" = dirty ] && [ "${JBROWSER_ALLOW_DIRTY_PACKAGE:-0}" != 1 ]; then + echo "refusing release package from a dirty source tree" >&2 + exit 1 +fi +cat > "$STAGE/PROVENANCE.txt" <<EOF +format=1 +source_commit=$source_commit +source_tree=$source_tree +source_state=$source_state +source_manifest_sha256=$source_manifest_sha256 +supply_chain_lock_sha256=$supply_chain_lock_sha256 +EOF + echo "==> creating $TARBALL" tar -C "$BUILD" -czf "$TARBALL" "$(basename "$STAGE")" echo " $TARBALL (native binary: $([ "$HAVE_BIN" = 1 ] && echo yes || echo no))" --- a/qt-webengine/CMakeLists.txt +++ b/qt-webengine/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.19) -project(jerboa_browser LANGUAGES CXX) +project(jerboa_browser LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -19,6 +19,7 @@ add_library(jerboa_browser SHARED src/browser_view.cpp src/browser_page.cpp src/browser_policy.cpp + src/browser_url_policy.cpp src/browser_ffi.cpp src/browser_automation.cpp src/browser_window.cpp @@ -50,7 +51,7 @@ if(JWB_ADBLOCK) add_custom_command( OUTPUT ${ADBLOCK_LIB} COMMAND ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${ADBLOCK_TARGET_DIR} - ${CARGO_EXE} build --release --manifest-path ${ADBLOCK_DIR}/Cargo.toml + ${CARGO_EXE} build --locked --release --manifest-path ${ADBLOCK_DIR}/Cargo.toml ${ADBLOCK_RANLIB_COMMAND} COMMENT "Building vendored adblock-rust staticlib (cargo build --release)" VERBATIM) --- a/qt-webengine/src/browser_handles.cpp +++ b/qt-webengine/src/browser_handles.cpp @@ -13,6 +13,7 @@ namespace { struct HandleEntry { jwb::HandleKind kind; void *payload; + JwbHandle parent; }; std::mutex g_mutex; @@ -37,10 +38,10 @@ namespace jwb { void set_last_error(const std::string &msg) { g_last_error = msg; } void clear_last_error() { g_last_error.clear(); } -JwbHandle handle_alloc(HandleKind kind, void *payload) { +JwbHandle handle_alloc(HandleKind kind, void *payload, JwbHandle parent) { std::lock_guard<std::mutex> lock(g_mutex); JwbHandle h = g_next++; - g_registry.emplace(h, HandleEntry{kind, payload}); + g_registry.emplace(h, HandleEntry{kind, payload, parent}); return h; } @@ -78,6 +79,15 @@ bool handle_release(JwbHandle h, HandleKind expected, void **out_payload, *err = JWB_ERR_INVALID_HANDLE; return false; } + if (expected == HandleKind::Context) { + for (const auto &entry : g_registry) { + if (entry.second.parent == h) { + set_last_error("context still has live views"); + *err = JWB_ERR_INVALID_ARGUMENT; + return false; + } + } + } if (out_payload) *out_payload = it->second.payload; g_registry.erase(it); return true; @@ -124,7 +134,7 @@ JWB_API JwbStatus jwb_view_new(JwbHandle context, JwbHandle *out) { return st; void *view_obj = jwb::backend_view_create(ctx_obj, &st); if (!view_obj) return st; - *out = jwb::handle_alloc(jwb::HandleKind::View, view_obj); + *out = jwb::handle_alloc(jwb::HandleKind::View, view_obj, context); return JWB_OK; } --- a/qt-webengine/src/browser_internal.h +++ b/qt-webengine/src/browser_internal.h @@ -27,7 +27,9 @@ enum class HandleKind { Context, View, Window }; // Allocate a handle owning `payload`. Handles are monotonic and never reused, // so a freed handle stays permanently invalid — stale/double-free is rejected // cleanly rather than aliasing a later object. -JwbHandle handle_alloc(HandleKind kind, void *payload); +// `parent` records ownership dependencies (views retain their context). A +// parent handle cannot be released while a live child remains. +JwbHandle handle_alloc(HandleKind kind, void *payload, JwbHandle parent = 0);