/* Facilitates Ed25519 key generation, signing, and verification for verifiable credentials via shared signing core (RFC-0921). Do not handle raw key storage or management. Do not perform network operations or external API calls. Do not parse or validate credential content beyond signing/verification. RFC-0921: delegate Ed25519 sign/verify/keygen to shared signing core (@warpgogol/werkstatt-shared/signing). Remove @noble/ed25519 import. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ /** * @warpgogol/werkstatt-shared/passport — Ed25519 VC signing helpers * * DNA-34 / RFC-0028 * * Uses the Web Crypto API (available in Node.js 16+ and all modern browsers). * No private keys are ever written to disk by this module. * * SECURITY CONTRACT: * - Private keys arrive as hex-encoded (32 bytes) parameters supplied by callers. * Scope-split env convention: site-scoped signing (site passport, agent * surface) reads `PASSPORT_SIGNING_KEY` from the site `.env` * (`readEnvFileValue` first — ambient env may carry a different identity); * operator-scoped signing (DHT, swim, identity credentials, mission commits) * reads `WERKSTATT_OPERATOR_SIGNING_KEY` from the workshop root `.env`. * The two names MUST NOT be conflated — shadowing produces AGS-09-style * invalid proofs. * - This module never logs, writes, or returns private key material. */ import bs58 from "bs58"; import { generateKeyPair as signingGenerateKeyPair, signBytes as signingSignBytes, verifyBytes as signingVerifyBytes, fromHex, toHex, } from "@warpgogol/werkstatt-shared/signing"; import type { VCProof, VerifiableCredential } from "./schema.ts"; // --------------------------------------------------------------------------- // Multibase base58btc encoding (prefix "z") // --------------------------------------------------------------------------- /** Encode bytes as multibase base58btc (prefix "z"). */ export function toMultibase(bytes: Uint8Array): string { return "z" + bs58.encode(bytes); } /** Decode multibase base58btc string (prefix "z") to bytes. */ export function fromMultibase(multibase: string): Uint8Array { if (!multibase.startsWith("z")) { throw new Error( `Unsupported multibase prefix "${multibase[0]}". Only "z" (base58btc) is supported.`, ); } return bs58.decode(multibase.slice(1)); } // --------------------------------------------------------------------------- // The canonical credential subject digest // --------------------------------------------------------------------------- /** The data signed in the VC proof — composition hash + commit SHA. */ export interface CredentialSubjectDigest { systemHash: string; commitSha: string; issuedAt: string; appId: string; } /** * Produce the canonical UTF-8 bytes that are signed/verified. * DETERMINISM: identical inputs → identical bytes. */ export function credentialBytes(subject: CredentialSubjectDigest): Uint8Array { // Stable JSON: sorted keys, no whitespace const canonical = JSON.stringify({ appId: subject.appId, commitSha: subject.commitSha, issuedAt: subject.issuedAt, systemHash: subject.systemHash, }); return new TextEncoder().encode(canonical); } // --------------------------------------------------------------------------- // Key generation // --------------------------------------------------------------------------- /** * Generate a new Ed25519 keypair. * @returns { privateKeyHex, publicKeyBytes, publicKeyMultibase } * * The private key is returned as a hex string for one-time copy into a * GitHub Actions secret. It is NEVER written to disk by this function. */ export async function generateKeypair(): Promise<{ privateKeyHex: string; publicKeyBytes: Uint8Array; publicKeyMultibase: string; }> { const keyPair = await signingGenerateKeyPair(); return { privateKeyHex: toHex(keyPair.privateKey), publicKeyBytes: keyPair.publicKey, publicKeyMultibase: toMultibase(keyPair.publicKey), }; } // --------------------------------------------------------------------------- // Signing // --------------------------------------------------------------------------- /** * Sign the credential subject and produce a W3C VC proof. * * @param subject — canonical credential subject fields * @param privateKeyHex — 32-byte Ed25519 private key as hex (from env secret) * @param verificationMethod — DID key reference (e.g. "did:web:example.org#key-v1") */ // @ai-invariant: signCredential uses the shared signing core (RFC-0921). Private keys arrive // exclusively as hex env vars — never logged, written to disk, or returned. // The signed output is a W3C VC with Ed25519Signature2020 proof. export async function signCredential( subject: CredentialSubjectDigest, privateKeyHex: string, verificationMethod: string, ): Promise { const privateKeyBytes = fromHex(privateKeyHex); const message = credentialBytes(subject); const signatureBytes = await signingSignBytes(privateKeyBytes, message); return { type: "Ed25519Signature2020", created: subject.issuedAt, verificationMethod, proofPurpose: "assertionMethod", proofValue: toMultibase(signatureBytes), }; } // --------------------------------------------------------------------------- // Verification // --------------------------------------------------------------------------- /** * Verify a VC proof against the published public key. * * @param subject — credential subject fields from passport.json * @param proof — proof object from passport.json * @param publicKeyMultibase — multibase public key from cosmic-passport-key.json * @returns true if signature is valid */ export async function verifyCredential( subject: CredentialSubjectDigest, proof: VCProof, publicKeyMultibase: string, ): Promise { try { const publicKeyBytes = fromMultibase(publicKeyMultibase); const signatureBytes = fromMultibase(proof.proofValue); const message = credentialBytes(subject); return await signingVerifyBytes(publicKeyBytes, message, signatureBytes); } catch { return false; } } // --------------------------------------------------------------------------- // Raw detached signing (RFC-0135 amend provenance trail) // --------------------------------------------------------------------------- /** * Sign arbitrary bytes with an Ed25519 private key, returning a multibase * base58btc detached signature. Reused by the amend provenance trail * (RFC-0135) so provenance records carry the same signing facility as the * Cosmic Passport (RFC-0028). * * @param privateKeyHex — 32-byte Ed25519 private key as hex (from env secret) * @param message — canonical bytes to sign */ export async function signBytes(privateKeyHex: string, message: Uint8Array): Promise { const privateKeyBytes = fromHex(privateKeyHex); const signatureBytes = await signingSignBytes(privateKeyBytes, message); return toMultibase(signatureBytes); } /** * Verify a multibase detached signature produced by {@link signBytes} against * a multibase public key. Returns false on any decode/verify failure. */ export async function verifyBytes( publicKeyMultibase: string, message: Uint8Array, signatureMultibase: string, ): Promise { try { const publicKeyBytes = fromMultibase(publicKeyMultibase); const signatureBytes = fromMultibase(signatureMultibase); return await signingVerifyBytes(publicKeyBytes, message, signatureBytes); } catch { return false; } } // --------------------------------------------------------------------------- // Assemble full VC // --------------------------------------------------------------------------- export function assembleVerifiableCredential( appId: string, domain: string, subject: CredentialSubjectDigest, proof: VCProof, keyVersion: string, ): VerifiableCredential { return { "@context": ["https://www.w3.org/2018/credentials/v1"], type: ["VerifiableCredential", "CosmicPassportCredential"], issuer: `did:web:${domain}`, issuanceDate: subject.issuedAt, credentialSubject: { id: `urn:warpgogol:app:${appId}`, systemHash: subject.systemHash, commitSha: subject.commitSha, }, proof: { ...proof, verificationMethod: `did:web:${domain}#key-${keyVersion}`, }, }; }