Skip to content
Merged
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
11 changes: 6 additions & 5 deletions src/orb/federated-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,12 @@ export function canonicalizeFederatedBundleBody(body: FederatedSignalBundleBody)
/**
* HMAC-sign a bundle body so a receiving instance can verify it was not tampered with in transit.
*
* TODO(#6477): the KEY-TRUST scheme (how a peer establishes/rotates the key it verifies against) is #6477's
* design decision and is deliberately NOT invented here. Until it lands, the signing key is this instance's
* existing dedicated anonymization secret (getOrCreateAnonSecret) as a placeholder: it makes the bundle
* tamper-evident to anyone who already holds the key, but it does NOT yet establish peer trust. #6480 (the
* import side) is explicitly blocked on #6477 for exactly that reason.
* KEY-TRUST (#6477, ratified as an allowlist design — NOT a PKI/reputation system): the signing key is this
* instance's dedicated anonymization secret (getOrCreateAnonSecret), which makes the bundle tamper-evident to
* anyone holding that key. Peer trust itself is established on the RECEIVING side, where #6480 has SHIPPED
* (src/orb/federated-import.ts): an importing operator explicitly adds a peer's verification key to
* `federatedIntelligence.peerKeys` (fail-closed when unset), and only an allowlisted key can verify a bundle.
* So this signature is the tamper-evidence layer; the receiver's allowlist is what turns it into peer trust.
*/
export function signFederatedBundle(body: FederatedSignalBundleBody, key: string): string {
return createHmac("sha256", key).update(canonicalizeFederatedBundleBody(body)).digest("hex");
Expand Down
14 changes: 8 additions & 6 deletions src/orb/federated-collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
// pull — GET peer bundles from it.
//
// SCOPE — deliberately NOT the import side. A pulled bundle is fetched, shape-checked and RETURNED; it is
// never signature-verified, never trust-gated, and never persisted. That is #6480's job, and #6480 is blocked
// on #6477 (the key-trust/anti-poisoning design). Verifying here would not merely be out of scope, it would be
// WRONG: there is no trust anchor to verify against yet, and inventing one is exactly what #6477 exists to
// prevent (see the TODO(#6477) note on signFederatedBundle in ./federated-bundle.ts).
// never signature-verified, never trust-gated, and never persisted. That is #6480's job, which SHIPPED
// separately in ./federated-import.ts: it trust-gates each bundle against the operator's
// `federatedIntelligence.peerKeys` allowlist per #6477's ratified key-trust/anti-poisoning design. Verifying
// HERE would still be wrong — the trust anchor lives with the importer that decides what to fold in, not with
// the transport client that only fetches (see the KEY-TRUST note on signFederatedBundle in ./federated-bundle.ts).
//
// NO DEFAULT COLLECTOR, BY DESIGN. The client only ever talks to an endpoint the operator configured in
// `.loopover.yml`. There is no hardcoded fallback and no auto-discovery — this codebase's self-host posture
Expand Down Expand Up @@ -164,8 +165,9 @@ export async function pushFederatedBundle(manifest: ManifestSlice, db: D1Databas
*
* Returns [] — having touched nothing — unless the operator opted in AND configured a pull-armed collector.
* Bundles are shape-checked and returned; unrecognized entries are dropped. They are deliberately NOT
* signature-verified or trust-gated — that is #6480, blocked on #6477. Returns [] rather than throwing on any
* failure, so an unreachable or hostile collector is indistinguishable from "no peers yet" to every caller.
* signature-verified or trust-gated — that is #6480's job, shipped in ./federated-import.ts (the peerKeys
* allowlist). Returns [] rather than throwing on any failure, so an unreachable or hostile collector is
* indistinguishable from "no peers yet" to every caller.
*/
export async function pullPeerBundles(manifest: ManifestSlice, opts: CollectorOpts = {}): Promise<FederatedSignalBundle[]> {
const endpoint = resolveCollectorEndpoint(manifest, "pull");
Expand Down