/* RFC-1124: Fleet claim signing and verification — canonicalization and sign/verify wrappers for FleetClaim, built on the existing Ed25519 signBytes/verifyBytes primitives from sign.ts. Analogous to dht-sign.ts but for the git-native fleet claims registry claim shape. Do not handle key storage or management — private keys arrive as env vars. Do not assemble W3C VC envelopes — claims use a simple signature string, not VCProof. Do not modify sign.ts — this module wraps signBytes/verifyBytes. Do not implement claim legitimacy rules (self-claim vs transfer claim) — that lives in the engine's claims-repo. RFC-1124: initial fleet claim signing module — FleetClaim contract, claimBytes, signClaim, verifyClaim. */ /** * @warpgogol/werkstatt-shared/passport/claim-sign — fleet claim signing (RFC-1124) * * Uses signBytes/verifyBytes from sign.ts with a dedicated canonicalization * function for fleet claims. The signature field is excluded from * canonicalization (it is computed over the claim data without the signature). */ import { fromHex } from "@warpgogol/werkstatt-shared/signing"; import { signBytes, toMultibase, verifyBytes } from "./sign.ts"; /** * One signed claim file: claims/.json in the fleet claims repo. * * The claim is self-contained for offline signature verification: * `signerPublicKey` is embedded, so a verifier needs nothing but the claim * file to check the signature. Legitimacy (is this signer allowed to claim?) * is decided by the engine — self-claims sign with the creator key, transfer * claims sign with the PREVIOUS creator's key and carry `authorizationHash` * binding them to the bordbuch `handover` event. */ export interface FleetClaim { /** SHA-256 of the canonical passport.json at claim time. */ passportHash: string; systemId: string; /** did:web-style creator identity from passport.creator.identity. */ creatorIdentity: string; /** deriveInstanceId(creator.publicKey) — first 16 hex of sha256. */ instanceId: string; /** ISO 8601 — LWW field. */ claimedAt: string; /** Bordbuch tip hash at claim time. */ bordbuchHead: string; /** * Tombstone marker — non-null ONLY on a tombstone file: a claim explicitly * superseded without a same-id successor (e.g. system-id migration). * Normal replacement never writes a tombstone — the new claim replaces the * file and git history is the supersession chain. */ supersededBy: string | null; /** * Hex Ed25519 public key of the signer — same format as * passport.creator.publicKey, so deriveInstanceId(signerPublicKey) matches * instanceId for self-claims. Embedded so the claims repo is self-contained * for offline verification. Equals the creator's key for self-claims; * equals the PREVIOUS creator's key for transfer claims. */ signerPublicKey: string; /** * Transfer claims only: sha256 of the handover-authorization.json that * authorizes the creator change (matches bordbuch `handover` event * metadata.authorizationHash). Null for self-claims. */ authorizationHash: string | null; /** Ed25519 signature over the canonical JSON of all fields above. */ signature: string; } /** * FleetClaim without the signature field — the data that is actually signed. */ export type FleetClaimData = Omit; /** * Produce canonical UTF-8 bytes from a fleet claim (excluding signature). * DETERMINISM: identical inputs → identical bytes (sorted-key JSON, no whitespace). */ export function claimBytes(claim: FleetClaimData): Uint8Array { const sorted: Record = {}; for (const key of Object.keys(claim).sort()) { sorted[key] = (claim as Record)[key]; } const canonical = JSON.stringify(sorted); return new TextEncoder().encode(canonical); } /** * Sign a fleet claim and return a multibase Ed25519 signature string. * * @param claim — claim data (without signature) * @param privateKeyHex — 32-byte Ed25519 private key as hex (from env secret) * @returns multibase base58btc signature string */ export async function signClaim(claim: FleetClaimData, privateKeyHex: string): Promise { return signBytes(privateKeyHex, claimBytes(claim)); } /** * Verify a fleet claim signature against the claim's embedded signerPublicKey. * * @param claim — full fleet claim (including signature) * @returns true if signature is valid */ export async function verifyClaim(claim: FleetClaim): Promise { const { signature, ...claimData } = claim; // signerPublicKey is hex (passport.creator.publicKey format); verifyBytes // expects multibase — convert here so callers never juggle encodings. const signerMultibase = toMultibase(fromHex(claim.signerPublicKey)); return verifyBytes(signerMultibase, claimBytes(claimData), signature); }