From 02e238b39a8183cd036a2440f0be6efc2962ef9e Mon Sep 17 00:00:00 2001 From: Duncan Date: Mon, 17 Aug 2026 23:36:37 -0400 Subject: [PATCH] feat(egress): thread durable bearer capability through Blossom media auth (C2b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring the owner-derived Blossom bearers into the durable-capability world so a header minted under identity A cannot be attached after a transition to B. mint_media_get_auth and the do_upload t=upload mint now sign under a bounded egress lease (issuance is an ordinary leased operation, spec L4569-4570) and return/hold an OwnerIdentityCapability registered with its revocation handle. The four get-auth attach sites (media_download, personas::card, media_proxy x2) and the upload dispatch validate the bearer via admit_exercise() immediately before the HTTP send — a stale capability attaches nothing (get-auth stays fail-open) or uploads zero bytes. mint_media_get_auth becomes async; the ripple is a mechanical .await through its four already-async callers. Removes the register_owner_bearer allow(dead_code) now that C2b consumes it. Substrate coverage is unchanged: the stale-bearer zero-bytes and registration-completeness controls already pin the exercise behavior these guards depend on. 2438 lib tests pass, clippy + fmt clean. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/src-tauri/src/commands/media.rs | 54 +++++++++++++++---- .../src-tauri/src/commands/media_download.rs | 9 +++- .../src-tauri/src/commands/personas/card.rs | 14 +++-- desktop/src-tauri/src/media_proxy.rs | 14 +++-- .../src-tauri/src/owner_identity_egress.rs | 7 ++- 5 files changed, 78 insertions(+), 20 deletions(-) diff --git a/desktop/src-tauri/src/commands/media.rs b/desktop/src-tauri/src/commands/media.rs index 86a91a984..1d5f7aaa4 100644 --- a/desktop/src-tauri/src/commands/media.rs +++ b/desktop/src-tauri/src/commands/media.rs @@ -6,6 +6,7 @@ use tauri::State; use tokio_util::sync::CancellationToken; use crate::app_state::AppState; +use crate::owner_identity_egress::{BearerPolicy, OwnerIdentityCapability}; use crate::relay::{parse_json_response, relay_api_base_url_with_override, relay_error_message}; use super::media_transcode::{ @@ -347,8 +348,15 @@ pub(crate) fn sign_blossom_get_auth_header( )) } -/// Mint a `t=get` Authorization header value for a relay media fetch, or -/// `None` when signing is unavailable (identity in recovery mode). +/// Mint a `t=get` Authorization header value for a relay media fetch, paired +/// with the durable [`OwnerIdentityCapability`] that governs it, or `None` when +/// signing is unavailable (identity in recovery mode). +/// +/// The header is owner-derived bearer authority that a LATER HTTP transmission +/// attaches, so the caller must validate the returned capability +/// ([`OwnerIdentityCapability::admit_exercise`]) immediately before attaching +/// the header — a stale capability (its issuing identity superseded by a +/// transition) attaches nothing. /// /// Fail-open by design: while the relay's `BUZZ_REQUIRE_MEDIA_GET_AUTH` flag /// is off, an unauthenticated request still succeeds, so degrading to no @@ -359,7 +367,10 @@ pub(crate) fn sign_blossom_get_auth_header( /// Safety contract: callers must only attach the returned header to URLs /// constructed from (or validated against) the app's own relay base URL — /// never to third-party origins, where the bearer token would leak. -pub(crate) fn mint_media_get_auth(state: &AppState, base_url: &str) -> Option { +pub(crate) async fn mint_media_get_auth( + state: &AppState, + base_url: &str, +) -> Option<(String, OwnerIdentityCapability)> { let keys = match state.signing_keys() { Ok(k) => k, Err(e) => { @@ -367,13 +378,27 @@ pub(crate) fn mint_media_get_auth(state: &AppState, base_url: &str) -> Option Some(header), + // Issuance runs under a bounded egress lease: signing the bearer is an + // ordinary leased operation (spec L4569-4570). Registering the durable + // capability while the lease is held keeps its generation stamp coherent + // with the state that admitted the sign — no bump can slip between. + let lease = match crate::owner_identity_egress::try_admit_owner_identity_egress().await { + Ok(lease) => lease, + Err(e) => { + eprintln!("buzz-desktop: media get auth egress refused (unsigned request): {e}"); + return None; + } + }; + let header = match sign_blossom_get_auth_header(&keys, base_url, MEDIA_GET_AUTH_EXPIRY_SECS) { + Ok(header) => header, Err(e) => { eprintln!("buzz-desktop: media get auth signing failed (unsigned request): {e}"); - None + return None; } - } + }; + let bearer = crate::owner_identity_egress::register_owner_bearer(); + drop(lease); + Some((header, bearer)) } fn sign_blossom_upload_auth( @@ -440,9 +465,16 @@ async fn do_upload( 300 }; let base_url = relay_api_base_url_with_override(state); - let auth_event = { + // Issuance runs under a bounded egress lease (spec L4569-4570); the durable + // bearer registered under it carries the upload authority forward to the + // HTTP dispatch below, which validates it before each attempt. + let (auth_event, bearer) = { + let lease = crate::owner_identity_egress::try_admit_owner_identity_egress().await?; let keys = state.signing_keys()?; - sign_blossom_upload_auth(&keys, &sha256, expiry_secs, &base_url)? + let event = sign_blossom_upload_auth(&keys, &sha256, expiry_secs, &base_url)?; + let bearer = crate::owner_identity_egress::register_owner_bearer(); + drop(lease); + (event, bearer) }; let auth_header = format!( @@ -453,6 +485,10 @@ async fn do_upload( if let Some((app, progress_id)) = progress.as_ref() { emit_media_upload_phase(app, Some(progress_id.as_str()), "uploading"); } + // Validate the bearer immediately before transmitting the signed upload + // header: an identity superseded between minting and send uploads zero + // bytes rather than writing to the relay under stale authority. + bearer.admit_exercise()?; let mut resp = send_upload_attempt( state, UploadAttempt { diff --git a/desktop/src-tauri/src/commands/media_download.rs b/desktop/src-tauri/src/commands/media_download.rs index 7bc94da25..0c055b57c 100644 --- a/desktop/src-tauri/src/commands/media_download.rs +++ b/desktop/src-tauri/src/commands/media_download.rs @@ -292,8 +292,13 @@ async fn fetch_blob_bytes_with_cap( // `validate_download_url`, satisfying the mint_media_get_auth safety // contract (the token never leaves the relay origin). let relay_base = relay_api_base_url_with_override(state); - if let Some(auth) = mint_media_get_auth(state, &relay_base) { - req = req.header("authorization", auth); + if let Some((auth, bearer)) = mint_media_get_auth(state, &relay_base).await { + // Validate the durable bearer immediately before attaching it: a + // capability whose issuing identity was superseded by a transition + // attaches nothing (the fetch proceeds unauthenticated, fail-open). + if bearer.admit_exercise().is_ok() { + req = req.header("authorization", auth); + } } let resp = req.send().await.map_err(|e| classify_request_error(&e))?; diff --git a/desktop/src-tauri/src/commands/personas/card.rs b/desktop/src-tauri/src/commands/personas/card.rs index 29a5c35e6..a0bc2daf1 100644 --- a/desktop/src-tauri/src/commands/personas/card.rs +++ b/desktop/src-tauri/src/commands/personas/card.rs @@ -672,9 +672,17 @@ pub async fn mint_agent_card( // header ONLY for same-origin URLs so the token never leaves the // relay (same contract as `media_download.rs`). let relay_base = crate::relay::relay_api_base_url_with_override(&state); - let auth = is_same_origin(url, &relay_base) - .then(|| crate::commands::media::mint_media_get_auth(&state, &relay_base)) - .flatten(); + // Mint get-auth ONLY for same-origin URLs so the token never leaves + // the relay, then validate the durable bearer before attaching it — + // a superseded identity's header is dropped (fetch stays fail-open). + let auth = if is_same_origin(url, &relay_base) { + crate::commands::media::mint_media_get_auth(&state, &relay_base) + .await + .filter(|(_, bearer)| bearer.admit_exercise().is_ok()) + .map(|(header, _)| header) + } else { + None + }; fetch_avatar(url, auth.as_deref()).await? } _ => { diff --git a/desktop/src-tauri/src/media_proxy.rs b/desktop/src-tauri/src/media_proxy.rs index 21692ce52..9bc6ffa0d 100644 --- a/desktop/src-tauri/src/media_proxy.rs +++ b/desktop/src-tauri/src/media_proxy.rs @@ -59,8 +59,11 @@ async fn proxy_handler(AxumState(state): AxumState, req: Request) -> // `upstream_url` is always `{relay base}{path}`, so the token can't reach // a third-party origin (mint_media_get_auth safety contract). - if let Some(auth) = mint_media_get_auth(&app_state, &base_url) { - upstream = upstream.header("authorization", auth); + if let Some((auth, bearer)) = mint_media_get_auth(&app_state, &base_url).await { + // A bearer whose issuing identity was superseded attaches nothing. + if bearer.admit_exercise().is_ok() { + upstream = upstream.header("authorization", auth); + } } if let Some(range) = req.headers().get("range") { @@ -187,8 +190,11 @@ pub async fn handle_buzz_media( // `upstream_url` is always `{relay base}{path}`, so the token can't reach // a third-party origin (mint_media_get_auth safety contract). - if let Some(auth) = mint_media_get_auth(&state, &base) { - upstream = upstream.header("authorization", auth); + if let Some((auth, bearer)) = mint_media_get_auth(&state, &base).await { + // A bearer whose issuing identity was superseded attaches nothing. + if bearer.admit_exercise().is_ok() { + upstream = upstream.header("authorization", auth); + } } if let Some(range) = request.headers().get("range") { diff --git a/desktop/src-tauri/src/owner_identity_egress.rs b/desktop/src-tauri/src/owner_identity_egress.rs index 320ef53eb..75dbf9cac 100644 --- a/desktop/src-tauri/src/owner_identity_egress.rs +++ b/desktop/src-tauri/src/owner_identity_egress.rs @@ -69,6 +69,11 @@ //! - `managed_agents::persona_events`, `commands::personas::sharing`, //! `commands::channels` (×3), and the huddle STT task (per-send inline //! owner lease). +//! - **Durable bearers** — `commands::media::mint_media_get_auth` (Blossom +//! `t=get`) and the `do_upload` `t=upload` mint sign under a bounded lease +//! and register an [`OwnerIdentityCapability`]; the four +//! get-auth attach sites (`media_download`, `personas::card`, `media_proxy` +//! ×2) and the upload dispatch validate it before the HTTP send. //! //! Managed-agent keyed (`admit_managed_agent_egress`) — the four sink sites //! plus the git-workflow branch, variant selected at runtime by @@ -664,8 +669,6 @@ pub fn register_owner_session(cancel: CancellationToken) -> OwnerIdentityCapabil /// The returned capability stamps the current generation; every attach site /// validates it via [`OwnerIdentityCapability::admit_exercise`] before the HTTP /// dispatch. -// Consumed by the Blossom attach sites in C2b; remove allow when they land. -#[allow(dead_code)] pub fn register_owner_bearer() -> OwnerIdentityCapability { let generation = REGISTRY.generation.load(Ordering::Acquire); let mut durable = lock_durable();