Title: Signing-Key Publishing, Pinning, and Rotation Policy
Version: 0.1.0
Status: Phase 1 — example/demo key in repo; production KMS key pending KmsEd25519Signer (ADR-0002, skeleton today)
Owner: Security Lead
Last Reviewed: 2026-05-07
Next Review: 2026-08-07
Supersedes: none

---

# Why this document exists

The customer-facing offline verifier (`genomics-verify`, see
[`../customer/LOI_ONEPAGER.md`](../customer/LOI_ONEPAGER.md)) re-canonicalizes
a provenance manifest under JCS and verifies the detached Ed25519
signature against a **public verification key** that the lab fetches
out of band.

For that flow to be defensible inside a lab's QMS, three things must
be unambiguous:

1. **Where** the lab obtains the canonical public key.
2. **How** the lab confirms the key it pulled is the one we published
   (fingerprint pinning).
3. **When** the key rotates and how old manifests stay verifiable
   after rotation.

This file is the single source of truth for all three.

---

# 1. Trust model

```text
              published                  out-of-band                       in-pipeline
public key ─────────────► fingerprint ──────────► pinned in pilot agreement ──► verifies
(PEM file)               (sha256 hex)            and shipped to lab                signature
```

The public key file alone is **not** the trust anchor. The trust
anchor is the **fingerprint** — `SHA-256(PEM-bytes)` — which we
deliver to the lab through a separate, signed channel (the executed
pilot agreement, plus a confirmation email from the security lead
sent from the company's primary corporate domain).

A lab that only has the PEM file but no out-of-band fingerprint is
performing TOFU (trust-on-first-use) and **must not** rely on the
result for clinical sign-off. Ask for the pinned fingerprint.

---

# 2. Where to fetch the current public key

| Channel | URL pattern | Use when |
|---|---|---|
| **Repo (canonical)** | `https://github.com/NVIDIA-AI-Blueprints/genomics-analysis/raw/main/keys/genomics-public.pem` | Default; commit-pinned; verify the commit SHA matches the one cited in the pilot agreement. |
| **Customer doc bundle** | `customer-bundle-<pilot_id>.zip → keys/genomics-public.pem` | When the lab took delivery of a pilot bundle; the bundle's manifest pins the fingerprint. |
| **In-pipeline** | The `signature.json` for any signed manifest carries `public_key_fingerprint` — the lab reconciles that against its pinned fingerprint, never against a key the same payload claims to use. | Always, as a defense-in-depth check. |

**Canonical filename**: `genomics-public.pem`. PEM-encoded Ed25519,
SubjectPublicKeyInfo wrapper, PKCS#8 — the format
`cryptography.hazmat.primitives.serialization.load_pem_public_key`
parses by default.

> **Phase 1 note.** As of v0.1.0 of this policy, the **example key**
> shipped at [`../../../keys/genomics-public.pem.example`](../../../keys/genomics-public.pem.example)
> is the only key in the repo. The example fingerprint is documented
> in §3 below. The production KMS-rooted key lands when
> `KmsEd25519Signer` (ADR-0002) flips from skeleton to GA, at which
> point this file is updated and a new section added documenting the
> retirement of the example key.

---

# 3. Current keys

## 3.1 Active: example/demo key (Phase 1)

| Field | Value |
|---|---|
| Key ID | `example-2026-05-07` |
| Algorithm | Ed25519 |
| PEM path (in repo) | `keys/genomics-public.pem.example` |
| Fingerprint (SHA-256 of PEM bytes) | `c45fed5f205aea057efa7314515ec3688109aa4f072aa71bd4a7fd4c48db102d` |
| First used (signing) | 2026-05-07 (earliest) |
| Status | **Active for demos only.** Real pilot signatures must be produced by the production KMS key once KmsEd25519Signer is implemented. |

**This key is example material.** It is checked into the repo so the
demo script in `customer/DEMO_SCRIPT.md` runs end-to-end without
network access. Any signature the lab receives that verifies under
this key is by definition a demo signature, **not** a clinical-pilot
signature.

## 3.2 Pending: production key (Phase 1 → Phase 2)

| Field | Value |
|---|---|
| Key ID | (assigned at provisioning time, e.g. `prod-2026-Q3`) |
| Algorithm | Ed25519 |
| Storage | Google Cloud KMS, asymmetric key, `EC_SIGN_ED25519`, HSM-backed |
| Resource path | `projects/<gcp_project>/locations/<region>/keyRings/<ring>/cryptoKeys/<key>/cryptoKeyVersions/<v>` |
| Public key URL | `https://github.com/NVIDIA-AI-Blueprints/genomics-analysis/raw/<release_tag>/keys/genomics-public.pem` |
| Fingerprint | populated at first GA release; communicated by signed email + pilot agreement appendix |
| First used | TBD (gated on KmsEd25519Signer landing) |
| Status | Pending — substrate is ready, KMS hookup is tracked in `genomics/CLAUDE.md` §"Substrate-hook reality" item 3. |

---

# 4. Lab-side verification checklist

A lab that just received `manifest.json` + `signature.json` does this
on every signed sample, in order:

1. **Pin the fingerprint** in advance, from the executed pilot
   agreement (Appendix A: Signing-Key Fingerprints), as a hex string.
   Treat the pilot agreement as the trust anchor.
2. **Pull the PEM** from the canonical URL in §2 (or read it from the
   delivery bundle).
3. **Compute** `sha256(<the-PEM-bytes-as-stored-on-disk>)` and confirm
   it equals the pinned fingerprint. Reject otherwise.
   ```bash
   sha256sum genomics-public.pem
   # expect: c45fed5f205aea057efa7314515ec3688109aa4f072aa71bd4a7fd4c48db102d  (example key)
   ```
4. **Confirm the signature block** has a `public_key_fingerprint`
   field equal to the pinned fingerprint. Reject otherwise.
   ```bash
   jq -r .public_key_fingerprint signature.json
   ```
5. **Run the verifier** (`genomics-verify`):
   ```bash
   genomics-verify manifest.json signature.json --public-key genomics-public.pem
   ```
6. **Record** the verifier's exit code (0 / 1) plus the
   `manifest_sha256` and the run timestamp into the lab's QMS records
   alongside the run.

Steps 1, 3, and 4 are non-optional. Steps 5 and 6 are the action;
steps 1, 3, and 4 are what makes the action mean something.

---

# 5. Rotation policy

## 5.1 Triggers

A signing key rotates on any of:

- **Scheduled**: every 12 months.
- **Personnel**: any departure of an individual who held shell or KMS
  access to the platform (reactive; new key issued within 7 days).
- **Suspected exposure**: any incident where the private key, KMS
  IAM grants, or the signing-pipeline VM may have been accessed by an
  untrusted party. Treat as confirmed exposure. Issue new key
  immediately and revoke the prior key.
- **Algorithm or HSM migration** (e.g. Ed25519 → Ed448, or KMS
  region change).

## 5.2 Process

1. **T-7 days** — Notice posted to all active customers via the
   primary contact in the executed pilot agreement, by signed email
   from the company's corporate domain. Notice includes:
   - Reason for rotation (scheduled / incident / personnel — minimum
     truthful disclosure consistent with any open security review).
   - New key ID, fingerprint, canonical PEM URL.
   - Cutover date.
   - Whether the prior key is being **deprecated** (still verifiable
     for old manifests) or **revoked** (no manifest signed by it
     should be trusted any more — only used in confirmed-exposure
     rotation).
2. **T-0** — New key activates. **Both keys remain valid for
   signature verification on prior manifests** for the deprecation
   window (default 18 months) so historical artifacts stay
   re-verifiable.
3. **T+18 months** — Deprecated key moved to the archive section of
   this document (§6). Manifests signed by the deprecated key remain
   verifiable for the lab's local records, but the platform stops
   issuing new signatures under that key on T-0 day one.

In a confirmed-exposure rotation, step 2's overlap is **omitted**
and the prior key is moved straight to revoked status.

## 5.3 What labs should do on a rotation notice

1. Read the notice; capture new key ID + fingerprint into local QMS
   records.
2. Update the pinned fingerprint(s) in the lab's verification
   pipeline (e.g. config secret in their LIMS or CI).
3. Re-pull the PEM from the canonical URL; confirm fingerprint match
   per §4.
4. Continue verifying old manifests under the old fingerprint(s)
   until they age out of retention; verify new signatures under the
   new fingerprint.
5. If the rotation is a **revoke** (not a deprecate), flag for the
   QMS owner: any manifest signed under the revoked fingerprint
   inside the exposure window may need re-signing or remediation.

---

# 6. Key archive (rotation history)

Past keys we have signed under, ordered most recent first.

| Key ID | Algorithm | Fingerprint | Status | Active range | Reason for rotation |
|---|---|---|---|---|---|
| `example-2026-05-07` | Ed25519 | `c45fed5f205aea057efa7314515ec3688109aa4f072aa71bd4a7fd4c48db102d` | Active (demo only; not for pilots) | 2026-05-07 — present | n/a — first key in repo, example use |

Add new rows on rotation. Never delete rows; deprecation and revoke
are status changes, not deletions.

---

# 7. References

- [`../intended-use/INTENDED_USE.md`](../intended-use/INTENDED_USE.md) — what we sign for
- [`../technical/PROVENANCE_SCHEMA.md`](../technical/PROVENANCE_SCHEMA.md) §"Signature block" — manifest field semantics
- [`../technical/AUDIT_LOG_SPEC.md`](../technical/AUDIT_LOG_SPEC.md) — checkpoint signatures use the same key
- [`../decisions/0002-detached-jcs-signatures.md`](../decisions/0002-detached-jcs-signatures.md) — why JCS + detached Ed25519
- [`../customer/LOI_ONEPAGER.md`](../customer/LOI_ONEPAGER.md) §"Per-sample deliverable" — pilot-side commitment
- [`../customer/DEMO_SCRIPT.md`](../customer/DEMO_SCRIPT.md) §"Verify the manifest signature offline" — concrete walkthrough
- [`../../../compute_layer/cli/genomics_verify.py`](../../../compute_layer/cli/genomics_verify.py) — the verifier itself
- [`../../../keys/README.md`](../../../keys/README.md) — repo-side directory README
