# L3 chain-signature migration plan — HMAC-SHA256 → Ed25519

> **Status (cycle 624)**: planned, not yet shipped. This doc commits publicly to the migration design so the audit P1-7 finding has a documented forward path even before the code lands. Cross-references: `[[verification-recipe]]` §L3 (Roadmap P1-7), `app/lib/attestation-chain.ts:108-112` (existing roadmap note), `/tmp/regulatory-compliance-findings-cycle600.md` RC-600-P1-7, `[[wedge]]`.

## What L3 is today

`computeAttestationChainSignature` (app/lib/attestation-chain.ts:114) signs over the canonical bytes of:

```
{
  artifactPath,
  artifactSignature,
  evalSignatures: <sorted hex array>,
  evalSummary,
  issuedAt,
  issuedTo: { tag, requestId }
}
```

with **HMAC-SHA256** using `SIGNING_KEY` (the platform-internal `PROVENANCE_SIGNING_KEY` env). The result is the `chainSignature` field on the composed attestation envelope returned by `/api/knowledge/artifact/attestation/{path}` and re-verified by `POST /api/knowledge/attestation/verify`.

**Why this layer exists**: bundle-composition tamper-detection. An attacker who reads the persistence layer but cannot write to the platform's signing key cannot reorder evals, swap an eval for a different one, or trim the bundle without breaking `chainSignature`. The L1 (W3C VC) signatures on individual judgments stay valid under that attack (they sign the inner credentials, not the bundle composition); L3 catches the bundle-level tamper.

**Why it isn't enough today**: HMAC is symmetric. Anyone with `PROVENANCE_SIGNING_KEY` (the platform itself, anyone with the env in dev, anyone with write access to the deployment) can forge a chain signature. Relying parties outside the platform have no way to independently verify the bundle composition. The platform's own marketing claim "cryptographic chain-of-custody from data to deployment" is therefore L1+L2-honest but L3-platform-attested — the bundle composition is the last link in that chain that isn't yet asymmetric.

## Target design

Add a **second** signature field alongside `chainSignature`:

```
{
  artifact: { ... },
  evals: [ ... ],
  evalSummary,
  issuedAt,
  issuedTo,
  chainSignature: "<hmac-sha256 hex>",               // legacy
  chainSigningKeyId: "attestation-key-1",            // legacy
  chainSignatureEd25519: "<base64url ed25519 sig>",  // NEW
  chainSigningKeyIdEd25519: "attestation-ed25519-1", // NEW
  chainCanonicalPayloadShape: "rfc-8785-jcs"         // NEW (pins recipe)
}
```

The new Ed25519 signature is computed over **RFC 8785 JCS canonical bytes** of the same payload shape (artifactPath, artifactSignature, sorted evalSignatures, evalSummary, issuedAt, issuedTo). RFC 8785 JCS is the canonical form already in use for L1 + L2 — so consumers can reuse the same canonicalizer they use for the W3C VC layer.

The Ed25519 key is **separate** from the existing platform domain key (the one tombstone-registry uses):
- key-id: `attestation-ed25519-1` (versioned; rotates via the same retired-keys runbook used for judges)
- JWKS entry: published at `/.well-known/jwks.json` alongside the domain + judge keys
- Verification recipe: `verify(ed25519, jwks[keyId].publicKey, jcs(payload), chainSignatureEd25519)` — same shape as L2 judge verification

## Migration phases

1. **Phase A (cycle ~625, planned)** — Mint the Ed25519 key. Add to `/.well-known/jwks.json`. Surface `attestationChainKeyMode` on `/api/health` (mirror the `domainSigningKeyMode` pattern from cycle 610). Add a new `computeAttestationChainSignatureEd25519()` helper. No behaviour change yet — key exists but not signing.

2. **Phase B (cycle ~626, planned)** — Parallel-issue: every emitted attestation envelope carries BOTH `chainSignature` (HMAC) and `chainSignatureEd25519` (Ed25519). `verifyAttestationChain()` accepts either; relying parties can choose. Verification recipe doc updated.

3. **Phase C — ENTERED cycle 687** (was planned ~635; deferred while the 5-audit closure thread consumed the parallel-agent's cycles + this thread's own non-L3 work). Six-cycle deprecation window after Phase B (cycle 627) lands. Verification recipe doc (`/docs/verification-recipe.md` §L3 — cycle 687) declares L3 HMAC "deprecated; emitted for back-compat only." External callers migrate to `chainSignatureEd25519`. The platform's own `/api/knowledge/attestation/verify` endpoint accepts either signature (cycle 629 Phase B.2 wiring); per-form outcomes surfaced so a Phase-C consumer can detect partial-tamper signals during the transition. **Phase C exit criteria**: ≥6 cycles elapsed since Phase B (cycle 627 → exits ~cycle 720+); no external-partner breakage reports on the Ed25519 path; no partial-tamper signals at a rate suggesting Ed25519-side issuance bugs.

4. **Phase D (cycle ~640+, planned)** — Stop emitting `chainSignature` (HMAC) on new envelopes. `verifyAttestationChain()` continues to accept HMAC on historical envelopes (until they age out of consumer caches), refuses HMAC-only on freshly-issued envelopes. Marketing claim hedge removed from `app/page.tsx`.

5. **Phase E (cycle ~700+, planned)** — Remove HMAC verification entirely once consumer caches have aged past max-age-86400 + 7-day stale-while-revalidate window across two cycles' worth of deployments. Delete `chainSignature` field from envelope; rename `chainSignatureEd25519` → `chainSignature`. Major-version bump on the attestation envelope shape.

## What ships in cycle 624

Just the **plan and the marketing-claim hedge**:
- This doc.
- `app/page.tsx` "Honest disclosure" paragraph naming L3 as HMAC pending Ed25519 migration, cross-linking this doc + the verification-recipe section.
- Smoke pin asserting both the disclosure and this doc stay present until Phase D removes the hedge.

No code change to `attestation-chain.ts`. No new key. No new wire field. This is the documentation layer that closes the audit's "honest non-conformance" gap — the cryptographic-chain-of-custody marketing claim and the verification-recipe reality are now consistent at the wire surface.

## Rejected alternatives

**A1: Replace HMAC with Ed25519 in-place** — breaks all existing consumers that verify `chainSignature`. Multi-phase parallel-issue is the only safe path for an external-facing contract.

**A2: Sign with the existing domain key** (the one tombstone-registry uses) — conflates two security domains. The domain key signs tombstone receipts (one-time data-subject erasure records); the chain key signs many bundle compositions per second. Rotation cadences differ. A compromise of one shouldn't blast-radius the other.

**A3: Move to a JWS Compact / JWS JSON serialization** — too big a shape change. The current envelope's flat `chainSignature` + `chainSigningKeyId` fields work; we keep them and add Ed25519-equivalents alongside.

**A4: Defer the migration indefinitely** — leaves the marketing claim non-conformant with the verification recipe. The audit (RC-600-P1-7) explicitly cites this as a credibility hit in any due-diligence review even if it isn't an enforcement target on its own. The phased plan resolves the credibility gap in 5-6 cycles of incremental work.

## Smoke regression contract

`scripts/smoke-test.mjs` step 737 pins:
- `app/page.tsx` carries the "Honest disclosure" paragraph
- The paragraph cross-references both `/docs/verification-recipe.md` §L3 and `/docs/l3-chain-migration-plan.md`
- This doc carries the 5-phase plan (Phase A through Phase E)
- This doc names `chainSignatureEd25519` as the new wire-field name

A future cycle that lands Phase B amends the smoke pin to also assert the new field is emitted; Phase D removes the disclosure paragraph and the corresponding smoke check; the migration plan stays as historical record.

## Cross-references

- `[[verification-recipe]]` §L3 — the current honest-non-conformance disclosure
- `[[wedge]]` — the 5-tier chain-of-custody framing the L3 layer composes into
- `app/lib/attestation-chain.ts:108-112` — the existing roadmap note that points at this plan
- `/.well-known/control-mapping.json` — cycle-620 NIST AI RMF mapping; MEASURE-2.6 binding cites L1+L2 explicitly and is silent on L3, consistent with the hedge here
- `/tmp/regulatory-compliance-findings-cycle600.md` RC-600-P1-7 — parent audit finding
- `/tmp/supply-chain-auditor-findings-cycle579.md` — earlier supply-chain audit thread that landed Phase-B judge VC + retired-judge-keys (cycles 581-583) and queued L3 migration as the remaining structural break
