mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(egress): thread durable bearer capability through Blossom media auth (C2b)
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<BearerPolicy> 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 <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
@@ -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<String> {
|
||||
pub(crate) async fn mint_media_get_auth(
|
||||
state: &AppState,
|
||||
base_url: &str,
|
||||
) -> Option<(String, OwnerIdentityCapability<BearerPolicy>)> {
|
||||
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<St
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match sign_blossom_get_auth_header(&keys, base_url, MEDIA_GET_AUTH_EXPIRY_SECS) {
|
||||
Ok(header) => 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 {
|
||||
|
||||
@@ -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))?;
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -59,8 +59,11 @@ async fn proxy_handler(AxumState(state): AxumState<ProxyState>, 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") {
|
||||
|
||||
@@ -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<BearerPolicy>`]; 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<BearerPolicy> {
|
||||
let generation = REGISTRY.generation.load(Ordering::Acquire);
|
||||
let mut durable = lock_durable();
|
||||
|
||||
Reference in New Issue
Block a user