- Move ephemeral.save() into try/except in all 5 command handlers (iface-up,
peer-add, peer-del, ephemeral-revoke, sweep) to catch save-time OSError
(missing dir, perms, disk full) and return JSON error instead of raw traceback.
- Honor --ttl 0 (immediate expiry) by explicit None check instead of silent
fallback to DEFAULT_TTL_S; clamp negatives to 0.
- Add regression test proving save-failure surfaces as JSON, not traceback.
Fixes: review feedback from feat/p2p-ephemeral-cli
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
A hard Depends blocked dpkg install on gk2, where python3-websockets was
removed so pip can provide websockets 16.0 (uvicorn version skew). Recommends
installs it on normal apt boards without blocking dpkg -i where it's absent.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
The joinlink base_url defaulted to https://assist.local (LAN-only), so a
shared assistance join link was unreachable over the internet. Derive the
public hub from [api].sso_cookie_domain in secubox.conf
(.gk2.secubox.in -> https://admin.gk2.secubox.in); ASSIST_BASE_URL/--base-url
still override; falls back to assist.local only when no domain is configured.
Same public-base-URL pattern as the Nextcloud _public_base_url fix.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Closes phantom-session DoS: assist resolvers bound to entry.author not
payload issued_by. Same author-vs-payload class as active_grants.
# Conflicts:
# packages/secubox-annuaire/annuaire/grants.py
The socle assist resolvers (active_session/console_active/pending_requests/
can_open) filtered sovereignty on payload[issued_by] — attacker-controllable
— instead of the authenticated entry.author. import_entries federates ANY
validly-signed op, so a mesh peer could sign ASSIST_SESSION_OPEN{issued_by=
victim} with its own key; the victim's active_session then counted it as its
own → phantom session trips the single-session invariant → DoS of real
sessions. Same author-vs-payload class fixed in active_grants (release-rings).
Bind self-scoped ops (SESSION_OPEN/CLOSE, CONSOLE_GRANT/REVOKE) to
_author == self_did (sovereign), and ASSIST_REQUEST to _author == issued_by
(self-certifying), fail-closed on missing author. Add _author accessor to
grants.py. Repair test fixtures to carry author (Journal.append always sets
it from the signing key; every assist op is self-certifying).
Follow-up: ASSIST_ACCEPT is not yet author-bound to the request's center_did
(distinct cross-op vector; accept payload has no issued_by).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
current_ring counted ANY author's RELEASE_PROMOTE/DEMOTE; sync-repo drives
reprepro copy off it, so a federated promote by an ungranted center would
publish fleet-wide. Like box_ring, count a ring op only when its verified
author holds an active capability=release grant from the honoring box.
Add current_ring(entries, evo_id, self_did=None): fail-closed author+grant
gate when self_did is given, unfiltered back-compat when None. Add public
published_evo_ids() and thread self_did through evolutions_in_ring. Callers
pass the sovereign self: verbs.release_promote/demote pass self_did; releasectl
cmd_list/cmd_sync_repo gain _self_did_public() (public did, no private key on
read paths); api /evolutions passes _self_did(). Read paths now use
published_evo_ids() instead of reaching into grants._op/_payload. Tests cover
ungranted promote ignored, granted promote counted, and author-less ignored.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
A GRANT_ISSUE is self-certifying (model.py Grant: entry.author == issued_by).
Grant ops federate via dir_sync, so a mesh peer can validly sign a GRANT_ISSUE
whose payload lies "issued_by=VICTIM" while author=ATTACKER; on import it is
well-formed and correctly signed. active_grants honored the payload's claimed
issued_by, letting a victim believe it delegated authority it never signed.
Add a fail-closed author check in the grant_issue branch: skip unless the
verified _author(entry) equals payload issued_by (missing/None author -> unequal
-> skipped). Repair bare-dict fixtures to carry author == issued_by (reflects
Journal.append reality). Regression test proves the forged release grant is not
honored and has_release_grant(ATTACKER, VICTIM) is False.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
secubox-release-api.service (User=secubox, /run/secubox/release.sock,
RuntimeDirectoryPreserve=yes, NoNewPrivileges=true — the API execs
secubox-releasectl directly, never via sudo), sudoers/secubox-release
scoped to /usr/sbin/secubox-releasectl only, debian/secubox-release.install
wiring every artifact (release/*.py, api/*.py, sbin, www/releases, menu.d,
nginx dropin, systemd unit, sudoers), and postinst enabling the API service
+ guarded traversal-only chmod o+x on the shared /etc/secubox/secrets
parent (same fix secubox-assist shipped in 0.2.1) — never chowning any
shared parent. secubox-annuaire bumped to ship annuaire/releases.py, the
ring resolver secubox-release consumes.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
590-releases.json belongs to secubox-release; requires should gate the
sidebar entry on that package being installed, matching the sibling
convention (secubox-appstore/secubox-p2p menu entries require themselves).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Operator webui for the release-rings control plane: promote/demote per
evolution, a publish form (artifacts JSON + notes), and a box×ring assign
form, all delegating to the /api/v1/release/* endpoints. Event delegation
only (no inline onclick), API-derived strings via textContent/dataset only
(no innerHTML) per the WEBUI-PANEL-GUIDELINES. Sidebar entry at /releases
(category mesh) and the nginx vhost (static alias + proxy to
release.sock via the proven rewrite + secubox-proxy.conf snippet pattern).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
GET /status and /health stay public; /evolutions, /box-ring, /publish,
/promote, /demote, /assign require JWT. Reads resolve current_ring/
box_ring in-process from the annuaire journal; every mutation shells
out to secubox-releasectl via argv list (never a shell string). self_did
resolves from the public node.did file (SECUBOX_SELF_DID env or
/etc/secubox/annuaire/node.did) — the private signing key is never read
by the API.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Center/repo/box CLI over annuaire.verbs.release_* + release/repo.py +
release/boxapply.py, mirroring secubox-assistctl's conventions (_key/_journal/
_self_did/_die/_dry, argparse subparsers, ValueError -> {"error": ...} exit 1).
sync-repo and apply are DRYRUN-guarded since reprepro/apt-get are not present
in test environments; secubox-release-repo is a thin exec wrapper for cron/timers.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Both request forms require a non-empty reason (and the targeted one a center
DID); guard client-side with a friendly notify() message instead of letting the
raw pydantic 'string_too_short' validation error surface. Mark motif required.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
The assist ctl runs as `secubox` (invoked by the API) and must read
/etc/secubox/secrets/annuaire/node.key to sign journal ops, but the shared
/etc/secubox/secrets parent is root:secubox-toolbox 0750 which secubox can't
traverse — so every mutating assist op (offer/request/session) failed with a
Permission denied the panel swallowed. postinst adds o+x (traversal-only) to
the parent; subdirs keep their 0700. Fixes the socle mutating path too.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
The offer/request-open/match-accept/joinlink POSTs swallowed failures with no
r.ok check, so a backend error (e.g. the ctl unable to sign) or a missing JWT
showed 'nothing' to the operator. Add a non-blocking notify() banner + a
postAction() helper that surfaces the API error detail (and a clear 401/403
'not authenticated' message), plus empty-tag guards. textContent only (XSS-safe).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
The tabs used abstract labels (Disponibilité/Inviter) that didn't map to what
the operator wants to do — publish an offer, launch an open request, share an
invite link. Relabel tabs+buttons+headers to action verbs with emoji + inline
hints explaining each; data-tab/data-act attributes unchanged (JS intact).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
- Journal de session renders raw JSON.stringify([]) when empty; replace
with an "Aucune demande en attente." empty-state and readable per-item
rows (DID/mode/scope/reason) built via textContent/createElement.
- Rename section header to "Demandes en attente" to match its content.
- Add short muted hints under the targeted (socle) and open (marketplace)
request forms so the operator understands which one to use.
- refresh() only re-fetches the active marketplace tab's endpoint on the
5s interval instead of all three; tab-switch still triggers an
immediate fetch.
- Guard dataset[k] assignment against undefined/null so buttons never
get a literal "undefined" data attribute.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
The id-shadowing consent-forgery fix rests on author_prefix being
collision-resistant; a 48-bit prefix was brute-forceable in days on a cluster
(targeted collision → reopen the shadow forge once the live join-path escalates
a forged match). Widen to 128-bit; ids auto-propagate via author_prefix().
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
offer_id/req_id were free-form attacker-choosable strings, so a signed mesh
peer could publish a SHADOW ASSIST_OFFER carrying a victim's exact offer_id;
the matcher's offers_by_id map (keyed by bare id) collapsed onto the shadow
(last-writer-wins), its own author-check passed trivially, and match_ready
could return True for the victim's real match without the victim ever
accepting.
Ids are now author-self-certifying: admission (active_offers/
active_open_requests, and offer-revoke) requires offer_id/req_id to carry a
12-hex prefix derived from the author's own DID (author_prefix/
_id_matches_author), making ids globally unique per author. secubox-assistctl
mints ids under the box's own prefix instead of bare "off-"/"orq-" strings.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
An ASSIST_MATCH_ACCEPT credited a side by checking only that the
accept's author owns the offer_id/req_id IT references — never that
those ids are the ones hashing to the target mid. Attacker C could post
his own active offer o2 + request r2 (passing the author-binding
checks), then submit both accept sides carrying match_id=mid (A's
offer + B's request) while pointing offer_id/req_id at o2/r2, forging
mutual consent for a rendezvous he was never party to.
match_ready now requires match_id(accept.offer_id, accept.req_id) ==
mid before crediting either side, so an accept can only ever speak for
the exact pair that produces the mid it targets.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
active_offers/active_open_requests/match_ready trusted attacker-controlled
payload fields (issued_by, side) instead of the authenticated entry author,
letting any signed mesh member revoke someone else's offer, forge a mutual
match-accept alone, or spoof issued_by onto a victim's DID. Bind every
admission check to entry.author, mirroring the existing socle resolvers in
assist.py/grants.py.
Also make matches() fail-closed on offers/requests missing their id
(previously raised KeyError instead of skipping the malformed entry).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
secubox-assist.install already globs assist/*.py so rendezvous.py/
joinlink.py/escalate.py ship without changes; secubox-annuaire's
`cp -r annuaire/*` already covers assist_match.py — both verified via
dpkg-deb -c on the built .debs. Added secubox-p2p to Depends (the
escalate flow's ephemeral peer add/revoke goes through it) and a
guarded, idempotent postinst block that provisions the wg-ephemeral/
10.11.0.0/24 range when secubox-p2p exposes the CLI for it, and
no-ops with a breadcrumb note otherwise — secubox-p2p has no
ephemeral-iface CLI yet, so this never invents one. Extended
test_packaging_dual.py to lock in shipped modules, the p2p Depends,
the still-scoped sudoers entry, and that postinst never touches the
shared /etc/secubox parent.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>