From 3f2c428c9bcb15776209231c117d47ecae70ff3c Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:00:20 -0700 Subject: [PATCH 1/2] secret-source leak check: a credential that cannot be presented reaches the caller as one fixed-class refusal (kernel cell); the all-doors scan asserts the class and keeps its RED arm (status row 127) --- crates/busbar-kernel/tests/plane_driver.rs | 226 ++++++++++++++++++ crates/busbar/src/root/tests/secret_source.rs | 4 + .../src/root/tests/secret_source_class.rs | 108 +++++++++ 3 files changed, 338 insertions(+) create mode 100644 crates/busbar/src/root/tests/secret_source_class.rs diff --git a/crates/busbar-kernel/tests/plane_driver.rs b/crates/busbar-kernel/tests/plane_driver.rs index f875520817..efe9964b16 100644 --- a/crates/busbar-kernel/tests/plane_driver.rs +++ b/crates/busbar-kernel/tests/plane_driver.rs @@ -2122,3 +2122,229 @@ async fn a_vetoed_units_refusal_names_the_hook_and_writes_its_rejected_row() { )] ); } + +// ── a credential that cannot be presented ─────────────────────────────────────────────────────── + +/// What one failing secret leaves behind: the reference, and every string of it a caller must never +/// be handed (the variable, the path, the module, the reference as written). +struct Failing { + secret: busbar_contract::secret_ref::SecretRef, + markers: Vec, +} + +/// THE FAILING SECRETS this cell plants, one per source shape, each with its own markers: an unset +/// variable, a file that is not there, and a module no secret plugin answers. +fn failing_secrets() -> Vec { + use busbar_contract::secret_ref::SecretRef; + let pid = std::process::id(); + let var = format!("BUSBAR_KERNEL_SECRET_MARKER_{pid}"); + std::env::remove_var(&var); + let path = std::env::temp_dir() + .join(format!("busbar-kernel-secret-marker-{pid}")) + .display() + .to_string(); + let _ = std::fs::remove_file(&path); + let module = format!("secret-marker-module-{pid}"); + let opaque = format!("secret-marker-setting-{pid}"); + let plugin: SecretRef = serde_json::from_value( + serde_json::json!({ "module": module, "settings": { "at": opaque } }), + ) + .expect("a module reference"); + [ + (SecretRef::env(&var), vec![var.clone()]), + (SecretRef::file(&path), vec![path.clone()]), + (plugin, vec![module.clone()]), + ] + .into_iter() + .map(|(secret, mut markers)| { + markers.push(secret.describe()); + Failing { secret, markers } + }) + .collect() +} + +/// The markers `text` carries, without case. +fn markers_in(text: &str, failing: &Failing) -> Vec { + let lower = text.to_ascii_lowercase(); + failing + .markers + .iter() + .filter(|m| lower.contains(&m.to_ascii_lowercase())) + .cloned() + .collect() +} + +/// A FAR END WHOSE MEMBERS' CREDENTIALS CANNOT BE PRESENTED, the two ways production's far end meets +/// a secret that did not resolve: the member's one auth call is refused (its auth plugin holds no +/// credential), so nothing is sent; and the member's program is never spawned (the connector refuses +/// the open), so its attempt fails over before any byte reaches the plane. Each member's operator +/// text, naming the source, is held here, where the plugin and the connector hold theirs: nothing on +/// the far end's seam can carry it to the kernel. +struct Unpresentable { + /// The refusal the secret source gave the operator. + operator_text: String, +} + +/// The member whose auth call is refused. +const UNBOUND: &str = "unbound"; +/// The member whose program is never spawned. +const UNSPAWNED: &str = "unspawned"; + +impl busbar_kernel::plane_driver::FarEnd for Unpresentable { + fn member<'a>( + &'a self, + _: &'a busbar_contract::caps::Pass, + attempt_no: u32, + ) -> impl Future + Send + 'a { + use busbar_kernel::plane_driver::Pick; + let pick = match [UNBOUND, UNSPAWNED].get(attempt_no as usize - 1) { + Some(m) => Pick::Member { + name: (*m).to_string(), + pool: "pool-a".to_string(), + passthrough: false, + provider: "acme".to_string(), + }, + None => Pick::Exhausted { + status: 503, + retry_after: Some(2), + }, + }; + async move { pick } + } + + async fn send( + &self, + _: &busbar_contract::caps::Pass, + request: busbar_kernel::plane_driver::OutboundRequest, + ) -> bool { + request.member != UNBOUND + } + + async fn next( + &self, + _: &busbar_contract::caps::Pass, + ) -> Option { + Some(busbar_kernel::plane_driver::FarPiece { + last: true, + fail_over: true, + ..busbar_kernel::plane_driver::FarPiece::default() + }) + } + + fn failure( + &self, + _: &busbar_contract::caps::Pass, + ) -> Option<&'static str> { + Some("credential_unavailable") + } + + async fn write( + &self, + _: &busbar_contract::caps::Pass, + _: busbar_kernel::plane_driver::OutboundRequest, + ) -> bool { + false + } +} + +/// Run one unit through the one loop over any far end; its outcome. +async fn drive_over( + units: &busbar_kernel::plane_driver::PlaneUnits<'_, common::TestUnits, F, cases::Caller>, +) -> busbar_contract::caps::Outcome { + use busbar_kernel::teller::{run_unit_async, AccrualMeter, Ended, Kernel, Run}; + let kernel = Kernel::new(); + let (gauge, canary, leases, meter) = ( + busbar_kernel::slice::ConcurrencyGauge::new(), + busbar_contract::caps::Canary::new(), + busbar_kernel::slice::LeaseCell::new(), + AccrualMeter::new(), + ); + let cell = common::cell(&kernel); + let run = Run { + cell: &cell, + parent: None, + leases: &leases, + gauge: &gauge, + canary: &canary, + meter: &meter, + }; + match run_unit_async(&kernel, units, &common::ctx(7), run, units).await { + Ended::Settled { end, .. } => end.outcome(), + Ended::AlreadySettled => panic!("nothing else holds this unit's cell"), + } +} + +/// A SECRET THAT DOES NOT RESOLVE REACHES THE CALLER AS A FIXED-CLASS REFUSAL NAMING NO SOURCE +/// (status row 127; the cell the retired tool-plane crate's `SetupRefusal::client_message` redaction +/// was until #511). For each failing source (an unset variable, a missing file, a module nothing +/// answers) the kernel's own resolver refuses with the operator's text, which names the source (the +/// control: the markers are there to be found). A unit whose every member's credential cannot be +/// presented (the auth call refused; the program never spawned) ends at the walk's terminal: the +/// plane's `refusal` is handed the one reason (`breaker_open`, class `Unreachable`) and that +/// reason's own word, and the caller's render is byte-identical whichever source failed and carries +/// none of its markers. RED: a kernel that handed the plane any operator text would fail the +/// double's own rule (a kernel refusal's text is its reason's word, else FAULT and an empty body), +/// the byte-identity across sources, and the marker scan. +#[tokio::test] +async fn a_credential_that_cannot_be_presented_reaches_the_caller_as_a_fixed_class_refusal() { + use busbar_contract::abi::plane::{class_of, RefusalClass}; + use busbar_contract::caps::{Outcome as UnitOutcome, ReasonCode, StepName}; + assert_eq!(class_of(ReasonCode::BreakerOpen), RefusalClass::Unreachable); + let mut renders = Vec::new(); + for failing in failing_secrets() { + let operator_text = busbar_kernel::config::secret::resolve_linked_string(&failing.secret) + .expect_err("the reference does not resolve"); + assert!( + !markers_in(&operator_text, &failing).is_empty(), + "the control: the operator's text names the source: {operator_text}" + ); + let r = rig(Way::Double, BufferCaps::default(), cases::Book::default()); + let (steps, far, caller) = ( + common::TestUnits::passing(), + Unpresentable { operator_text }, + cases::Caller::default(), + ); + assert!(!markers_in(&far.operator_text, &failing).is_empty()); + let units = r + .driver + .unit(&steps, &far, &caller, cases::arrival("/call", b"p"), 0); + let outcome = drive_over(&units).await; + assert!( + matches!( + outcome, + UnitOutcome::Refused(StepName::Route, ReasonCode::BreakerOpen) + | UnitOutcome::Failed(StepName::Route, ReasonCode::BreakerOpen) + ), + "{}: {outcome:?}", + failing.secret.describe() + ); + let rendered = units.take_rendered().expect("the terminal is rendered"); + let render = format!( + "{} {:?} {}", + rendered.status, + rendered.fields, + String::from_utf8_lossy(&rendered.body) + ); + assert_eq!( + rendered.body, b"refused:503:breaker_open:retry=2", + "the plane is handed the fixed reason and its own word: {render}" + ); + let found = markers_in(&render, &failing); + assert!( + found.is_empty(), + "the caller was handed {found:?}: {render}" + ); + assert_eq!( + caller.text(), + "", + "nothing reached the caller but the refusal" + ); + renders.push((rendered.status, rendered.fields, rendered.body)); + } + renders.dedup(); + assert_eq!( + renders.len(), + 1, + "one fixed class: the caller cannot tell which source failed" + ); +} diff --git a/crates/busbar/src/root/tests/secret_source.rs b/crates/busbar/src/root/tests/secret_source.rs index 813c4a2b15..edffce24e7 100644 --- a/crates/busbar/src/root/tests/secret_source.rs +++ b/crates/busbar/src/root/tests/secret_source.rs @@ -428,3 +428,7 @@ async fn decisions_a_provider_key_gone_by_an_apply_reaches_the_caller_without_it .expect("the body reads"); assert_clean(instance, status, &headers, &body, &secret); } + +// The scan's fixed-class half and its kept RED arm (status row 127). +#[path = "secret_source_class.rs"] +mod class; diff --git a/crates/busbar/src/root/tests/secret_source_class.rs b/crates/busbar/src/root/tests/secret_source_class.rs new file mode 100644 index 0000000000..41af3c070a --- /dev/null +++ b/crates/busbar/src/root/tests/secret_source_class.rs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE SCAN'S FIXED-CLASS HALF AND ITS KEPT RED ARM (status row 127). The scan beside this file +//! proves a failing secret's source never reaches a door plane's caller; this file proves two more +//! things about the same renders: +//! * the refusal is a FIXED CLASS: two different sources failing (an unset variable, a missing file) +//! hand the caller the same status and the same body, so nothing in it depends on which source +//! failed or how; +//! * the scan SEES a leak: the operator's text spliced into a real door's caller render (what the +//! retired `SetupRefusal::client_message` redaction stood in front of until #511) is found by the +//! same detector and fails the same assertion every door's cell makes. A scan that could not fail +//! would prove nothing; this arm stays in the tree so it cannot quietly go blind. + +use axum::http::{HeaderMap, StatusCode}; +use busbar_contract::secret_ref::SecretRef; + +use super::{ + assert_clean, call, member_secrets_installed, missing, program_section, + refused_naming_its_source, rendered, source_in, unset, +}; +use crate::root::door_steps::tests::tool_door::try_rig_tools; +use crate::root::serve::planes_tests::{Published, PUBLISHING}; + +/// One caller-facing answer: its status, its headers and its body. +type Answer = (StatusCode, HeaderMap, Vec); + +/// The caller's answer to a call on a member program whose `env` reference is `secret`, which does +/// not resolve, served by the tool door as `instance`. +async fn refused_call(instance: &'static str, secret: &SecretRef) -> Answer { + let _published = Published(instance); + let rig = try_rig_tools(instance, 0, program_section(secret)) + .unwrap_or_else(|e| panic!("{instance}: a member's env is not resolved at compose: {e}")); + call(&rig).await +} + +/// THE REFUSAL IS ONE FIXED CLASS: an unset variable and a missing file, each the `env` reference of +/// a member program, reach the caller as the same status and the same body, both free of either +/// source. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn two_failing_sources_reach_the_caller_as_one_fixed_refusal() { + let _one = PUBLISHING.lock().await; + member_secrets_installed(); + let cases = [ + ("tools-class-env", SecretRef::env(unset("TOOLS_CLASS_ENV"))), + ( + "tools-class-file", + SecretRef::file(missing("tools-class-file")), + ), + ]; + let mut answers = Vec::new(); + for (instance, secret) in &cases { + refused_naming_its_source(secret); + let (status, headers, body) = refused_call(instance, secret).await; + for (_, other) in &cases { + assert_clean(instance, status, &headers, &body, other); + } + answers.push((status, body)); + } + let (first, second) = (&answers[0], &answers[1]); + assert_eq!( + first.0, second.0, + "the refusal's status does not depend on the source that failed" + ); + assert_eq!( + String::from_utf8_lossy(&first.1), + String::from_utf8_lossy(&second.1), + "the refusal's body does not depend on the source that failed" + ); +} + +/// THE KEPT RED ARM: the scan catches a leak. A real tool-door caller render for a reference that +/// does not resolve is first proven clean; the operator's text for the same reference is then +/// spliced into its body in place of the connector's fixed words (exactly the leak #511's redaction +/// stood in front of), and the same detector names the source and the same assertion fails. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn the_scan_catches_a_source_spliced_into_a_real_door_render() { + let _one = PUBLISHING.lock().await; + member_secrets_installed(); + let fixed = busbar_contract::conn::ConnError::Refused.text(); + let cases = [ + ("tools-red-env", SecretRef::env(unset("TOOLS_RED_ENV"))), + ("tools-red-file", SecretRef::file(missing("tools-red-file"))), + ]; + for (instance, secret) in &cases { + let operator = refused_naming_its_source(secret); + let (status, headers, body) = refused_call(instance, secret).await; + assert_clean(instance, status, &headers, &body, secret); + let text = String::from_utf8_lossy(&body).into_owned(); + let leaked = if text.contains(fixed) { + text.replace(fixed, &operator) + } else { + format!("{text}{operator}") + }; + let found = source_in(&rendered(status, &headers, leaked.as_bytes()), secret); + assert!( + !found.is_empty(), + "{instance}: the scan must find the spliced source in:\n{leaked}" + ); + let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + assert_clean(instance, status, &headers, leaked.as_bytes(), secret); + })); + assert!( + caught.is_err(), + "{instance}: the cell every door runs must fail on a render that carries {found:?}" + ); + } +} From 496cf1755169e976cb9f4beb28114305813ffd43 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:06:03 -0700 Subject: [PATCH 2/2] re-judge 3f2c428c9bcb under the pinned engine