# Tabletop: "Signing key leaked" (20.10.2026)

**Type:** desk walk-through, not a live drill. Run by the documentation engineer against the code as it stands on 20.10.2026. No second person participated, and nothing was changed in production. The live drill is planned for 15.01.2027 (see TRUST-PACK.md G-list).

**Scenario:** on 20.10.2026 at 10:00 UTC, the `ATTEST_SIGNING_KEY` for kid `13fa3d18a369e6c7` is found in a public log. The compromise window is unknown. The earliest possible start is 03.10.2026, when the 03.10 audit found leaked tokens on the workstation.

## Walk-through

### Step 1: detect
- Source: an external report to hello@predge.io, or a secret scanner alert.
- Finding D-1: there is no monitoring of signing operations, so no internal alert would fire. Detection depends on people.

### Step 2: contain
- Unset `ATTEST_SIGNING_KEY`, redeploy. Code confirms signing routes return 503 when the variable is empty.
- Finding D-2: the variable is read from the Railway environment. Anyone with Railway project access can read or change it. Project access and MFA are not verified.

### Step 3: publish the new registry
- Generate a new key, set the variable, redeploy.
- Finding D-3: the registry emits only the current key. The old key disappears from the document at redeploy. The rotation text promises the old key stays listed as `active: false`. The code does not do that.
- Finding D-4: the registry has no `revoked`, `valid_from` or `valid_to` field. A verifier cannot tell that a key was revoked.

### Step 4: what verifiers see for old-key signatures
- `GET /verify` with the old key returns `valid: true` if the signature checks, and `key_published: false` once the key is gone.
- Finding D-5: a verifier that reads only `valid` will accept forged records signed after the compromise. The response shape does not tell them to reject.
- `GET /verify?id=` checks only call records, not verdicts. Verdict existence is available at `GET /v1/verdicts/:id`.

### Step 5: bound the compromise window
- Signed payloads carry `issued_at`, set by the signer.
- Finding D-6: `issued_at` is self-asserted. An attacker with the key can write any time. There is no timestamp authority. The window can only be bounded by the earliest evidence of compromise, which is human judgement.

### Step 6: list affected records
- Records issued by Predge are in the database. The verdict record includes `public_key`. The signed-call table has a `pubkey` column.
- Finding D-7: there is no index on the verdict record's public key, and no runbook query has been written or tested. The query is possible with the owner role but has not been run.
- Finding D-8: forged records never reach the database, so the list covers only legitimate records. This is a limit, not a bug. The mitigation is step 6 of INCIDENT-RESPONSE.md, existence checks.

### Step 7: operational access
- Finding D-9: one person holds all keys and roles. If that person is unavailable, nobody can rotate. No break-glass contact exists.

### Step 8: communications
- Channels: X, email, predge.io/trust. The status section is not yet live (Finding D-10).
- The trust page is not yet built or deployed, so there is no page to publish to during an incident.

### Step 9: contract side
- The attestation key is separate from the contract owner key. A leak of the attestation key does not give control of contracts.
- Finding D-11: the cachet-oracle key (`kid a122cc095c0f7fe5`) is held on an operator machine. It is not in the rotation procedure. Its leak case is not walked through here.
- The contract owner is a single externally owned account (review finding M-01). A leak of that key gives full control of the canonical contracts, including setValidator, setOperator, setParams and withdraw on the settlement contract. This is a separate tabletop item.

### Step 10: secrets at rest
- Finding D-12: Desk webhook secrets are stored in plaintext in Postgres. A database read gives every customer's HMAC secret.

## Findings summary

| ID | Finding | Severity | Owner | Fix | Target |
|---|---|---|---|---|---|
| D-1 | No signing-operation monitoring | High | Amir | Count and alert signatures per kid per hour | 13.11.2026 (proposed) |
| D-2 | Signing key readable by anyone with Railway project access; MFA unverified | High | Amir | MFA check; restrict project access; move to KMS in v2 | MFA 24.10.2026; KMS 09.11.2026 |
| D-3 | Rotation policy promised in registry text is not implemented | High | Amir | Keep prior keys listed with status | 06.11.2026 (proposed) |
| D-4 | No revocation or validity fields in registry | High | Amir | Add status, revoked_at, valid_from/valid_to | 06.11.2026 (proposed) |
| D-5 | `/verify` returns valid for keys no longer published | High | Amir | Return invalid for keys with a revoked status; add key_status to response | 06.11.2026 (proposed) |
| D-6 | Signing time is self-asserted | Medium | Amir | Document the limit now; RFC 3161 timestamp in v2 | Document 27.10.2026; timestamp 09.11.2026 |
| D-7 | No tested affected-records query | Medium | Amir | Write the query, run it on a staging copy, store it in the runbook | 30.10.2026 (proposed) |
| D-8 | Forged records cannot be enumerated | Accepted | n/a | Existence checks in the verdict endpoint, documented | 27.10.2026 (proposed) |
| D-9 | Single operator, no break-glass contact | High | Amir | Name a second trusted person with sealed credentials | 30.11.2026 (proposed) |
| D-10 | No status section on the trust page | Medium | Amir | Build status section | 27.10.2026 (proposed) |
| D-11 | Cachet-oracle key not in rotation procedure | Medium | Amir | Add to runbook; separate tabletop | 30.10.2026 (proposed) |
| D-12 | Desk webhook secrets in plaintext | Medium | Amir | Encrypt at rest | 13.11.2026 (G8) |

Plus, for the contract side: owner EOA (M-01), H-01 to H-03 (see TRUST-PACK.md section 6). These are not part of this scenario but affect the same trust story.

## Decisions taken in this walk-through

- No changes to production were made.
- The trust page will say "signing key held in a platform environment variable, rotation procedure in progress" until D-3 to D-5 are fixed.
- The next exercise is a live staging drill after D-3 to D-5 ship, by 15.01.2027.

## Sign-off

Facilitator: documentation engineer (desk walk-through, 20.10.2026)
Owner review: ______________________ Date: __________
