Files
archy/core
archipelagoandClaude Opus 5 f0c289a415 feat(marketplace): implement the DID signature layer that was only specified
docs/marketplace-protocol.md described a full authorship-verification chain and
was marked "shipped end-to-end". It wasn't: `signatures.manifest_hash` and
`signatures.did_signature` existed only as two struct fields that nothing read.
The authenticity actually delivered was the Nostr event's NIP-01 Schnorr
signature — which proves who *relayed* an event, not who *authored* the manifest
inside it. Anyone could republish someone else's manifest under their own DID.

Implemented:

- `canonical_signing_bytes` / `manifest_digest` — the signed preimage is the
  manifest as canonical JSON (recursively sorted keys, no whitespace) with
  `signatures` omitted, SHA-256'd. Canonicalisation is load-bearing, not
  cosmetic: `container.env` is a HashMap with per-process random iteration
  order, and `serde_json::Map` is only sorted while the `preserve_order` feature
  stays off — a feature any crate in the graph can enable for everyone via
  feature unification. Either would make the digest vary between runs, so
  signatures would fail *intermittently*, which is far worse to diagnose than
  failing cleanly.
- `sign_manifest` / `verify_manifest_signature` — Ed25519 over the 32 raw digest
  bytes, verified against the key `author.did` encodes (reusing the existing
  `identity::pubkey_bytes_from_did_key`).
- `publish` signs before broadcasting, fills `author.did` when empty, and
  **refuses** to publish under a DID this node cannot sign as — otherwise we'd
  spray manifests across every relay that every verifier then rejects.
- `discover` verifies before caching. A `missing` signature is a normal
  unsigned publisher: listed, but earning no identity trust. An `invalid` one is
  tampered or forged, so it is **dropped entirely** and logged — it fails closed
  rather than appearing behind a warning badge a user can click through.

Trust scoring now requires proof for both identity-derived factors:

- The 30-point identity factor was `did.starts_with("did:")`. An unsigned
  manifest with a plausible DID string and a pinned image scored 65 —
  "Community" — on no cryptography at all. It now scores 35, "Unverified".
- **The 20-point federation factor is gated too**, which the original spec did
  not say. An unverified `author.did` is just a string the publisher chose, so
  without this an attacker could copy the DID of a peer the user federates with
  and be rewarded for impersonating the party they trust most.

`marketplace.verify` now returns the signature verdict separately from the
advisory policy issues — `valid` has always meant "passes the advisory security
checks", so conflating it with authenticity would have been its own trap.

Tests (22 pass), weighted to the adversarial cases: tampering; tampering that
also rewrites `manifest_hash` while reusing the stolen signature; signing with
key A while claiming B's DID; undecodable did:keys including the old
`z6MkTest123` fixture that used to score 30/30; malformed base64 and
wrong-length signatures; digest stability across map insertion order; the digest
ignoring the `signatures` block; the federation-impersonation case; and a legacy
cache without the new field loading as `missing` rather than defaulting trusted.

Protocol doc rewritten so the preimage rules are normative — a third-party
implementation that canonicalises differently produces signatures we reject, so
"sorted keys, no whitespace, signatures omitted, sign the raw digest" now has to
be stated exactly rather than sketched.

Not included: surfacing the verdict in Marketplace.vue, which reads only
trust_score/trust_tier today. The field reaches the frontend; where the badge
goes is a UI call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 05:39:55 -04:00
..
2026-01-24 22:59:20 +00:00
2026-01-24 22:59:20 +00:00