Skip to content

feat(reports): Ed25519 snapshot signing + offline verification (A4a)#636

Merged
remyluslosius merged 1 commit into
mainfrom
feat/reports-a4a-signing
Jun 21, 2026
Merged

feat(reports): Ed25519 snapshot signing + offline verification (A4a)#636
remyluslosius merged 1 commit into
mainfrom
feat/reports-a4a-signing

Conversation

@remyluslosius

Copy link
Copy Markdown
Contributor

Reports A4 (backend) — Ed25519 signing for tamper-evidence. The signature/signing_key_id columns reserved in A3a get their writer.

What

  • A Signer signs a snapshot's content address with a domain-separated Ed25519 payload ("openwatch/report-snapshot/v1\n" + content_sha256). Generate stores the signature + a public key-id fingerprint; the Report wire exposes content_sha256 + signature (base64) + signing_key_id (null when no signer is wired).
  • Key custody (the decision you greenlit): a 32-byte raw Ed25519 seed loaded from [reports].signing_key_file (OPENWATCH_REPORTS_SIGNING_KEY_FILE), never in the DB — same model as the credential key. Unconfigured = an ephemeral per-boot key (dev) with a boot warning; production sets a durable key so signatures verify across restarts.
  • GET /api/v1/reports/signing-key (host:read) publishes the public key {key_id, algorithm, public_key, ephemeral} for offline verification; 503 when no signer.
  • The json export face is now canonical — re-marshaled so sha256(json face) == content_sha256 (the stored jsonb is Postgres-normalized), making the content↔hash link independently checkable. So a verifier can: re-hash the json face to confirm content, then Ed25519-verify the signature over that hash with the published key.
  • PDF footer shows the signed key-id instead of "not signed (MVP)".

No new dependency

Signing is stdlib crypto/ed25519go mod tidy is a no-op. (No supply-chain change, unlike A3b's fpdf.)

SDD

api-reports v1.5.0 — C-11 (signing + signing-key endpoint + offline verification), canonical-face note on C-09/C-10; AC-14 (signer sign/verify), AC-15 (generate stores a verifying signature), AC-16 (end-to-end over the API: generate → publish key → verify the signature offline, anon rejected).

Validation

gofmt/vet/build clean; go mod tidy no-op; specter check 111 structural; api-reports 16/16, 100%; report suite + full server suite (411s) green; frontend schema.d.ts regenerated.

Follow-up A4b: frontend Signed badge + Verify action.

Reports A4 backend (docs/engineering/reports_design.md). Signs report
snapshots for tamper-evidence; the signature/signing_key_id columns
reserved in A3a get their writer.

- A Signer (internal/report/signing.go) signs a snapshot's content
  address with a domain-separated Ed25519 payload
  ("openwatch/report-snapshot/v1\n" + content_sha256). Generate stores
  the signature + a public key-id fingerprint; the Report wire exposes
  content_sha256 + signature (base64) + signing_key_id (null when no
  signer is wired).
- Key custody: a 32-byte raw Ed25519 seed loaded from
  [reports].signing_key_file (OPENWATCH_REPORTS_SIGNING_KEY_FILE), never
  in the DB - same model as the credential key. Unconfigured = an
  EPHEMERAL per-boot key (dev) with a boot warning; production sets a
  durable key so signatures verify across restarts.
- GET /api/v1/reports/signing-key (host:read) publishes the public key
  {key_id, algorithm, public_key, ephemeral} for OFFLINE verification;
  503 when no signer. VerifySignature reconstructs the payload from
  content_sha256.
- The json export face is now CANONICAL: re-marshaled so
  sha256(json face) == content_sha256 (the stored jsonb is
  Postgres-normalized), making content<->hash independently checkable.
- PDF footer shows the signed key-id instead of "not signed (MVP)".

No new dependency (stdlib crypto/ed25519; go mod tidy no-op). SDD:
api-reports v1.5.0 (C-11 signing + canonical-face note on C-09/C-10;
AC-14 signer, AC-15 generate-signed, AC-16 end-to-end sign+publish+verify
over the API). gofmt/vet/build clean; specter 111 (api-reports 16/16,
100%); report + full server suites green.

Follow-up A4b: frontend Signed badge + Verify action + PDF-footer note.
@remyluslosius remyluslosius merged commit 8262354 into main Jun 21, 2026
13 checks passed
@remyluslosius remyluslosius deleted the feat/reports-a4a-signing branch June 21, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant