Skip to content

Swarm Phase 2: src/services/signature.ts — Ed25519 sign/verify with JSON canonical form #77

@Dewinator

Description

@Dewinator

Why

Fourth step of the Swarm Foundation Plan v1. Once node identity is in place, every record that EVER leaves this node must be signed. This issue creates the reusable signature service that all later swarm phases (sync, hub-anchors, peer-exchange) will call.

Background — read this BEFORE starting

Recall:

  • SWARM-PLAN v1 (b899a80a-738e-40c9-8b5d-a85e3df79922) — Phase 2 (provenance & signatures)
  • SWARM-Verfassung (57388366-4807-47d8-aed3-819ac21b1722) — pillar 2 (generalisierung-vor-sharing implies anything-that-leaves-must-be-attributable)
  • SWARM-Sharing-Trennlinie (9139bc45-e1bf-47cb-957a-e75dc17f83f5) — what types of records will be signed (lessons, hub_anchors, node_advertisements)

JSON Canonical Form: research the simplest TypeScript-friendly approach. Two viable paths:

  • RFC 8785 (JCS) — there's an npm package canonicalize (~1KB, no deps)
  • Custom: sort keys recursively, no whitespace, UTF-8 — write yourself if avoiding deps is preferred

If unsure: WebSearch "json canonical form RFC 8785 typescript 2026" and pick the path with the smallest dependency footprint.

What this issue delivers

A new file src/services/signature.ts (or mcp-server/src/services/signature.ts — match where existing services live):

// pseudo-API, adjust to project style:
export function canonicalize(value: unknown): string
export function sign(record: object, privateKeyPem: string): string  // base64 signature
export function verify(record: object, signature: string, publicKey: Uint8Array): boolean

// Convenience wrapper used by the swarm endpoints:
export function signWithSelfKey(record: object): { record: object; signature: string; signed_at: string }

Use node:crypto (createSign('sha256') won't work for Ed25519 — use crypto.sign(null, data, privateKey) per Node 14+ API). Confirm with WebSearch if uncertain.

The signed payload format is: canonicalize the record EXCLUDING the signature field itself, then sign the resulting bytes. Document this clearly in the file's JSDoc.

Acceptance criteria

  • File signature.ts exists at the matched path
  • Tests under the project's existing test directory:
    • Round-trip: sign then verify with the matching public key returns true
    • Tampered record (change one byte) → verify returns false
    • Verify with WRONG public key → returns false
    • Canonical form is deterministic across reorderings ({a:1, b:2} and {b:2, a:1} produce the same signature)
    • Empty/missing signature field is excluded from the signed bytes
  • Type-checked, no any smuggled in
  • README or JSDoc explains the canonical form choice (and links to the spec from Swarm Phase 0: SWARM_SPEC.md — wire-format spec for the decentralized mycelium swarm #74 if landed)

Hard constraints

  • DO NOT add any networking, HTTP, or libp2p code. This issue is JUST the crypto primitives.
  • DO NOT integrate this into existing record-creation paths yet. That's a follow-up issue (Phase 2.5: wire signature into lesson-synthesizer).
  • DO NOT touch the migration files or the node-identity script — read-only dependency on the public key.

Out of scope

  • Wiring signatures into actual lesson creation (next issue, will be created after this is merged)
  • Key rotation, revocation lists (v2)
  • Multi-signature schemes (v2)

Dependency

Conceptually depends on #74 (spec for canonical form) and #76 (node identity exists). The crypto primitives can be built and tested in isolation though — proceed even if those aren't merged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-eligibleAutonomous agent loop is allowed to pick thisfoundationPhase-0/1 Fundament für ein größeres FeatureswarmSchwarm-Foundation: dezentrale P2P-Architektur

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions