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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
run: |
python3 -m pip install --quiet --user --break-system-packages fastapi httpx uvicorn
python3 -m unittest trust/tests/test_bridge_shell.py trust/tests/test_shell_helper.py \
trust/tests/test_bridge_desktop.py
trust/tests/test_bridge_desktop.py trust/tests/test_local_auth.py

ui:
strategy:
Expand Down
130 changes: 91 additions & 39 deletions bazaar/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ bazaar::install() {
return 1
fi

if ! [[ "$key" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
ui::fail "Invalid tool key: ${key}"
log::error "rejected install: invalid key shape"
return 1
fi

ui::header "Installing: ${name}"
install_dir="${TURINGOS_BAZAAR_DIR}/${key}"
marker="${install_dir}/.installed"
Expand Down Expand Up @@ -99,31 +105,42 @@ bazaar::_install_npx() {
local pkg_spec="$package"
[[ -n "$version" ]] && pkg_spec="${package}@${version}"

# Verify sha256 of upstream tarball if pinned
if [[ -n "$sha256" && -n "$version" ]]; then
tarball_url=$(npm view "$pkg_spec" dist.tarball 2>/dev/null || echo "")
if [[ -n "$tarball_url" ]] && command -v curl &>/dev/null && (command -v sha256sum &>/dev/null || command -v shasum &>/dev/null); then
ui::info "Verifying SHA-256 for ${pkg_spec}..."
tmp_tarball=$(mktemp "${install_dir}/pkg.XXXXXX.tgz" 2>/dev/null || mktemp /tmp/turingos_pkg.XXXXXX.tgz 2>/dev/null || echo "${install_dir}/pkg.tgz")
if curl --silent --fail --max-time 15 --location "$tarball_url" -o "$tmp_tarball" 2>/dev/null; then
local actual_hash
if command -v sha256sum &>/dev/null; then
actual_hash=$(sha256sum "$tmp_tarball" | awk '{print $1}')
else
actual_hash=$(shasum -a 256 "$tmp_tarball" | awk '{print $1}')
fi
rm -f "$tmp_tarball" 2>/dev/null || true
if [[ "$actual_hash" != "$sha256" ]]; then
ui::fail "SHA-256 mismatch for ${pkg_spec}: expected ${sha256}, got ${actual_hash}"
log::error "sha256 mismatch: key=${key} expected=${sha256} actual=${actual_hash}"
return 1
fi
ui::ok "SHA-256 verified (${actual_hash:0:12}...)"
else
rm -f "$tmp_tarball" 2>/dev/null || true
fi
fi
# Fail closed: an npm package must be pinned to a version and a sha256,
# and the upstream tarball must hash to exactly that value.
if [[ -z "$sha256" || -z "$version" ]]; then
ui::fail "Refusing to install ${key}: registry entry has no pinned version/sha256"
log::error "unpinned npm entry rejected: key=${key}"
return 1
fi
if ! command -v curl &>/dev/null || ! (command -v sha256sum &>/dev/null || command -v shasum &>/dev/null); then
ui::fail "Cannot verify ${pkg_spec}: curl and sha256sum/shasum are required"
return 1
fi
tarball_url=$(npm view "$pkg_spec" dist.tarball 2>/dev/null || echo "")
if [[ -z "$tarball_url" ]]; then
ui::fail "Cannot resolve tarball for ${pkg_spec}; refusing unverified install"
return 1
fi
ui::info "Verifying SHA-256 for ${pkg_spec}..."
tmp_tarball=$(mktemp "${install_dir}/pkg.XXXXXX.tgz" 2>/dev/null || mktemp /tmp/turingos_pkg.XXXXXX.tgz 2>/dev/null) || return 1
if ! curl --silent --fail --max-time 15 --location "$tarball_url" -o "$tmp_tarball" 2>/dev/null; then
rm -f "$tmp_tarball" 2>/dev/null || true
ui::fail "Failed to download ${pkg_spec} for verification"
return 1
fi
local actual_hash
if command -v sha256sum &>/dev/null; then
actual_hash=$(sha256sum "$tmp_tarball" | awk '{print $1}')
else
actual_hash=$(shasum -a 256 "$tmp_tarball" | awk '{print $1}')
fi
rm -f "$tmp_tarball" 2>/dev/null || true
if [[ "$actual_hash" != "$sha256" ]]; then
ui::fail "SHA-256 mismatch for ${pkg_spec}: expected ${sha256}, got ${actual_hash}"
log::error "sha256 mismatch: key=${key} expected=${sha256} actual=${actual_hash}"
return 1
fi
ui::ok "SHA-256 verified (${actual_hash:0:12}...)"

# Look the package up without running it (a stdio MCP server never exits)
ui::info "Checking npm package: ${pkg_spec}"
Expand Down Expand Up @@ -176,20 +193,32 @@ bazaar::_install_archive() {
fi
fi

if [[ -n "$sha256" ]]; then
local actual_hash
if command -v sha256sum &>/dev/null; then
actual_hash=$(sha256sum "$tmp_archive" | awk '{print $1}')
else
actual_hash=$(shasum -a 256 "$tmp_archive" | awk '{print $1}')
fi
if [[ "$actual_hash" != "$sha256" ]]; then
ui::fail "SHA-256 mismatch for ${key}: expected ${sha256}, got ${actual_hash}"
log::error "sha256 mismatch: key=${key} expected=${sha256} actual=${actual_hash}"
rm -f "$tmp_archive" 2>/dev/null || true
return 1
fi
ui::ok "SHA-256 verified (${actual_hash:0:12}...)"
# Fail closed: archives must carry a pinned sha256.
if [[ -z "$sha256" ]]; then
ui::fail "Refusing to install ${key}: archive entry has no sha256"
log::error "unpinned archive entry rejected: key=${key}"
rm -f "$tmp_archive" 2>/dev/null || true
return 1
fi
local actual_hash
if command -v sha256sum &>/dev/null; then
actual_hash=$(sha256sum "$tmp_archive" | awk '{print $1}')
else
actual_hash=$(shasum -a 256 "$tmp_archive" | awk '{print $1}')
fi
if [[ "$actual_hash" != "$sha256" ]]; then
ui::fail "SHA-256 mismatch for ${key}: expected ${sha256}, got ${actual_hash}"
log::error "sha256 mismatch: key=${key} expected=${sha256} actual=${actual_hash}"
rm -f "$tmp_archive" 2>/dev/null || true
return 1
fi
ui::ok "SHA-256 verified (${actual_hash:0:12}...)"

# Refuse archives with absolute or parent-escaping member paths
if tar -tzf "$tmp_archive" 2>/dev/null | grep -Eq '(^/|(^|/)\.\.(/|$))'; then
ui::fail "Archive contains unsafe paths; refusing to unpack"
rm -f "$tmp_archive" 2>/dev/null || true
return 1
fi

mkdir -p "${install_dir}/unpacked"
Expand Down Expand Up @@ -265,10 +294,22 @@ bazaar::_auto_build() {

# ─── Env Var Check ────────────────────────────────────────────────────────────

# Registry-supplied names are used in ${!name}; anything but a plain identifier
# (e.g. "a[$(cmd)]") would be evaluated by bash, so reject it.
bazaar::_valid_env_name() {
[[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]
}

bazaar::_check_env_vars() {
local key="$1" env_var missing=()
while IFS= read -r env_var; do
[[ -n "$env_var" && -z "${!env_var:-}" ]] && missing+=("$env_var")
[[ -z "$env_var" ]] && continue
if ! bazaar::_valid_env_name "$env_var"; then
ui::fail "Registry entry ${key} has an invalid env var name; refusing"
log::error "invalid env_required name rejected: key=${key}"
return 1
fi
[[ -z "${!env_var:-}" ]] && missing+=("$env_var")
done < <(registry::get_array "$key" env_required)
(( ${#missing[@]} )) || return 0

Expand Down Expand Up @@ -301,9 +342,20 @@ bazaar::_edit_claude_config() {
bazaar::_register_mcp() {
local key="$1" arg var pattern env_vars=() args=() env_args=()
mapfile -t env_vars < <(registry::get_array "$key" env_required)
for var in "${env_vars[@]}"; do
bazaar::_valid_env_name "$var" || { log::error "invalid env_required name: key=${key}"; return 1; }
done

# Pin the registered package to the verified version, not the latest tag
local pkg ver
pkg=$(registry::get_field "$key" package)
ver=$(registry::get_field "$key" version)

# Args may reference ${HOME} or a required env var; expand them now
while IFS= read -r arg; do
if [[ -n "$pkg" && -n "$ver" && "$arg" == "$pkg" ]]; then
arg="${pkg}@${ver}"
fi
for var in HOME "${env_vars[@]}"; do
pattern='${'"$var"'}'
arg="${arg//"$pattern"/${!var:-}}"
Expand Down
36 changes: 23 additions & 13 deletions bazaar/registry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,23 @@ registry::_fetch_remote() {
if jq -e '(.entries // .tools) | type == "object"' "$tmp" &>/dev/null; then
# Verify Ed25519 detached signature if public key is present
pubkey="${TURINGOS_BAZAAR_PUBKEY:-/etc/turingos/trust/bazaar.pub}"
if [[ ! -f "$pubkey" && -f "${ROOT:-.}/trust/keys/bazaar.pub" ]]; then
pubkey="${ROOT:-.}/trust/keys/bazaar.pub"
local root="${TURINGOS_ROOT:-${ROOT:-.}}"
if [[ ! -f "$pubkey" && -f "${root}/trust/keys/bazaar.pub" ]]; then
pubkey="${root}/trust/keys/bazaar.pub"
fi
if [[ -f "$pubkey" ]] && command -v openssl &>/dev/null; then
# Fail closed: a trusted key, openssl and a valid signature are all required.
if [[ ! -f "$pubkey" ]] || ! command -v openssl &>/dev/null; then
ui::warn "Bazaar: cannot verify catalog (missing public key or openssl). Rejecting remote index."
log::error "bazaar index not verifiable: pubkey=${pubkey}"
rm -f "$tmp" 2>/dev/null || true
return 1
fi
{
local sig_url="${TURINGOS_BAZAAR_REMOTE_URL}.sig"
tmp_sig=$(mktemp "${cache}.sig.XXXXXX" 2>/dev/null || mktemp /tmp/turingos_bazaar_sig.XXXXXX 2>/dev/null || echo "${cache}.sig")
tmp_sig=$(mktemp "${cache}.sig.XXXXXX" 2>/dev/null || mktemp /tmp/turingos_bazaar_sig.XXXXXX 2>/dev/null) || {
rm -f "$tmp" 2>/dev/null || true
return 1
}
local sig_fetched=0
if [[ "$sig_url" =~ ^file:// ]]; then
local sig_path="${sig_url#file://}"
Expand All @@ -67,17 +78,16 @@ registry::_fetch_remote() {
fi
fi

if (( sig_fetched )); then
if ! openssl pkeyutl -verify -pubin -inkey "$pubkey" -rawin -in "$tmp" -sigfile "$tmp_sig" &>/dev/null; then
ui::warn "Bazaar index signature verification failed! Rejecting untrusted catalog."
log::error "bazaar index signature verification failed against $pubkey"
rm -f "$tmp" "$tmp_sig" 2>/dev/null || true
return 1
fi
log::audit BAZAAR_INDEX_VERIFIED "pubkey=${pubkey}"
if (( ! sig_fetched )) || \
! openssl pkeyutl -verify -pubin -inkey "$pubkey" -rawin -in "$tmp" -sigfile "$tmp_sig" &>/dev/null; then
ui::warn "Bazaar index signature missing or invalid! Rejecting untrusted catalog."
log::error "bazaar index signature missing/invalid against $pubkey"
rm -f "$tmp" "$tmp_sig" 2>/dev/null || true
return 1
fi
log::audit BAZAAR_INDEX_VERIFIED "pubkey=${pubkey}"
rm -f "$tmp_sig" 2>/dev/null || true
fi
}

mkdir -p "$(dirname "$cache")" 2>/dev/null || true
mv -f "$tmp" "$cache"
Expand Down
45 changes: 25 additions & 20 deletions daemon/src/bazaar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};

pub const DEFAULT_REMOTE_URL: &str =
"https://uncoalesced.github.io/turingos-bazaar/v1/index.json";
pub const DEFAULT_REMOTE_URL: &str = "https://uncoalesced.github.io/turingos-bazaar/v1/index.json";
pub const CACHE_TTL_SECS: u64 = 86400; // 24 hours

pub fn bazaar_dir() -> PathBuf {
Expand Down Expand Up @@ -79,19 +78,23 @@ pub fn public_key_path() -> PathBuf {
}

pub fn verify_signature(data: &[u8], sig: &[u8]) -> bool {
// Fail closed: any problem verifying means the index is not trusted.
let pubkey = public_key_path();
if !pubkey.is_file() {
return true;
return false;
}

let tmp_data = cache_path().with_file_name(".verify_data.tmp");
let tmp_sig = cache_path().with_file_name(".verify_sig.tmp");
let pid = std::process::id();
let tmp_data = cache_path().with_file_name(format!(".verify_data.{pid}.tmp"));
let tmp_sig = cache_path().with_file_name(format!(".verify_sig.{pid}.tmp"));

if let Some(parent) = tmp_data.parent() {
let _ = fs::create_dir_all(parent);
}
if fs::write(&tmp_data, data).is_err() || fs::write(&tmp_sig, sig).is_err() {
return true;
let _ = fs::remove_file(&tmp_data);
let _ = fs::remove_file(&tmp_sig);
return false;
}

let status = std::process::Command::new("openssl")
Expand All @@ -114,7 +117,7 @@ pub fn verify_signature(data: &[u8], sig: &[u8]) -> bool {

match status {
Ok(s) => s.success(),
Err(_) => true,
Err(_) => false,
}
}

Expand Down Expand Up @@ -148,24 +151,28 @@ pub async fn fetch_index() -> (Value, bool) {
if let Ok(bytes) = res.bytes().await {
if let Ok(parsed) = serde_json::from_slice::<Value>(&bytes) {
if parsed.get("entries").is_some() || parsed.get("tools").is_some() {
// If detached signature exists, verify it
// A valid detached signature is mandatory. Missing,
// unfetchable or invalid => discard the remote index
// and fall through to stale cache / bundled snapshot.
let sig_url = format!("{r_url}.sig");
let mut verified = false;
if let Ok(sig_res) = client.get(&sig_url).send().await {
if sig_res.status().is_success() {
if let Ok(sig_bytes) = sig_res.bytes().await {
if !verify_signature(&bytes, &sig_bytes) {
eprintln!("turingosd: bazaar index signature verification FAILED!");
return (parsed, true);
}
verified = verify_signature(&bytes, &sig_bytes);
}
}
}

if let Some(parent) = cache.parent() {
let _ = fs::create_dir_all(parent);
if verified {
if let Some(parent) = cache.parent() {
let _ = fs::create_dir_all(parent);
}
let _ = fs::write(&cache, &bytes);
return (parsed, false);
}
let _ = fs::write(&cache, &bytes);
return (parsed, false);
eprintln!(
"turingosd: bazaar index signature missing or invalid; ignoring remote index"
);
}
}
}
Expand Down Expand Up @@ -380,9 +387,7 @@ pub async fn list(project_path: Option<&str>) -> Value {
let (index, offline) = fetch_index().await;
let installed = installed_tools();
let project_dir = project_path.map(Path::new);
let disabled = project_dir
.map(get_disabled_items)
.unwrap_or_default();
let disabled = project_dir.map(get_disabled_items).unwrap_or_default();

let empty_map = serde_json::Map::new();
let entries_map = index
Expand Down
8 changes: 6 additions & 2 deletions daemon/src/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ pub fn spawn(app: AppRef) {
Json(crate::bazaar::list(None).await)
}

async fn bazaar_install(axum::extract::Json(params): axum::extract::Json<Value>) -> Json<Value> {
async fn bazaar_install(
axum::extract::Json(params): axum::extract::Json<Value>,
) -> Json<Value> {
let key = params.get("key").and_then(Value::as_str).unwrap_or("");
Json(crate::bazaar::install(key).await)
}

async fn bazaar_rules(
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
axum::extract::Query(params): axum::extract::Query<
std::collections::HashMap<String, String>,
>,
) -> Json<Value> {
let path = params.get("path").map(String::as_str).unwrap_or(".");
Json(crate::bazaar::get_rules(path).await)
Expand Down
9 changes: 5 additions & 4 deletions debian-live/config/includes.chroot/etc/turingos/hyprland.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
# is fine — this file only needs to define the TuringOS desktop contract.
#
# The shell window (class turingos-shell — the Brave --app window
# turingos-kiosk opens, always on Chromium's X11 ozone platform so the
# class survives) IS the desktop: pinned to every workspace, maximized
# edge to edge, frameless. Everything else floats above it, opened
# turingos-kiosk opens on Chromium's Wayland ozone platform, where Brave
# ignores --class and takes its app id from the --app URL instead:
# brave-127.0.0.1__-Default) IS the desktop: pinned to every workspace,
# maximized edge to edge, frameless. Everything else floats above it, opened
# centered — the cascade. This is the twin of the openbox rules in hook
# 0475; the keybinds below are the twin of what openbox's rc.xml provides.

Expand Down Expand Up @@ -83,7 +84,7 @@ misc {
# The shell is the desktop: no frame, all workspaces, full work area
windowrule {
name = shell-desktop
match:class = ^(turingos-shell)$
match:class = ^(turingos-shell|(brave|chrome|chromium)-127\.0\.0\.1__.*)$
float = on
maximize = on
border_size = 0
Expand Down
Loading
Loading