Manifest and Confirmation Signing - striae-org/striae GitHub Wiki

Overview

Striae uses server-issued digital signatures to protect exported forensic artifacts from tamper-by-rehash attacks.

  • Case package integrity is signed via FORENSIC_MANIFEST.json.
  • Case ZIP exports include the active public signing key PEM file for independent verification.
  • Confirmation export integrity is signed via confirmation-data-*.json metadata.
  • Confirmation exports are delivered as ZIP packages that contain confirmation-data-*.json plus the active public signing key PEM file.
  • Signing happens only in the Data Worker (private key secret).
  • Verification happens in the app using this precedence:
    • Use a PEM bundled in the selected import ZIP when available.
    • Otherwise resolve keys from app config.

This page covers signature payload rules, envelope structure, endpoint contracts, and verification behavior. For encrypted package structure, ENCRYPTION_MANIFEST.json, and import-time decryption, see Export Encryption.

This page documents the current implementation based on code in:

  • workers/data-worker/src/signing-payload-utils.ts
  • workers/data-worker/src/signature-utils.ts
  • workers/data-worker/src/data-worker.ts
  • app/utils/SHA256.ts
  • app/utils/confirmation-signature.ts
  • app/utils/signature-utils.ts
  • app/utils/export-verification.ts
  • app/utils/data-operations.ts

Signature Envelope

Both manifest and confirmation signatures use the same envelope shape:

{
  "algorithm": "RSASSA-PSS-SHA-256",
  "keyId": "forensic-signing-key-v1",
  "signedAt": "2026-03-12T00:00:00.000Z",
  "value": "base64url-signature"
}

value is base64url-encoded in the worker and base64url-decoded during verification.

Crypto and Key Management

Algorithm

  • Constant: RSASSA-PSS-SHA-256
  • Implemented with Web Crypto RSA-PSS and SHA-256
  • Salt length: 32 bytes

Signing Secrets (Worker)

Data Worker secrets:

  • MANIFEST_SIGNING_PRIVATE_KEY (PKCS#8 private key)
  • MANIFEST_SIGNING_KEY_ID (identifier embedded in signatures)

Reference:

  • workers/data-worker/wrangler.jsonc
  • workers/data-worker/src/signature-utils.ts

Verification Keys (App)

App config supports key rotation and fallback:

  • manifest_signing_public_keys map by keyId (preferred)
  • manifest_signing_public_key + manifest_signing_key_id fallback

Import-time key selection precedence:

  • Case and confirmation imports first look for a PEM file inside the selected ZIP package.
  • If multiple PEM files exist, Striae prefers striae-public-signing-key*.pem; otherwise it uses the first PEM in sorted order.
  • If no PEM is present in the ZIP, verification falls back to configured key resolution.

Reference:

  • app/config/config.json
  • app/config-example/config.json
  • app/utils/signature-utils.ts

Manifest Signing

Manifest Canonical Payload Rules

createManifestSigningPayload builds deterministic JSON for signing:

  1. Include manifestVersion (currently 3.0)
  2. Lowercase dataHash
  3. Sort imageHashes by filename and lowercase each hash
  4. Lowercase manifestHash
  5. Preserve totalFiles and createdAt

Reference:

  • Worker canonicalizer: workers/data-worker/src/signing-payload-utils.ts
  • App canonicalizer/verifier: app/utils/SHA256.ts

Export-Time Flow

  1. Export code builds secure manifest via generateForensicManifestSecure.
  2. App requests signature via signForensicManifest.
  3. Data Worker validates payload and signs canonical payload.
  4. Export ZIP stores signed manifest as FORENSIC_MANIFEST.json with:
    • manifest data
    • manifestVersion
    • signature
  5. Export ZIP also stores the active public signing key PEM file for independent verification.

Reference:

  • app/components/actions/case-export/download-handlers.ts
  • app/utils/data-operations.ts
  • workers/data-worker/src/data-worker.ts

Manifest Import-Time Verification

Case import is fail-closed:

  1. Require FORENSIC_MANIFEST.json
  2. Verify manifest schema/data extraction
  3. Resolve verification key source:
    • Use ZIP-contained PEM when present
    • Otherwise resolve configured key by keyId
  4. Verify signature (verifyForensicManifestSignature)
  5. Verify comprehensive integrity (data hash + image hashes + manifest hash)
  6. Reject import on any failure

Reference:

  • app/components/actions/case-import/orchestrator.ts
  • app/components/actions/case-import/zip-processing.ts
  • app/utils/SHA256.ts

Confirmation Signing

Hash-Then-Sign Rule

Confirmation exports are hashed before signature metadata is attached:

  1. Build unsigned export object (metadata + confirmations)
  2. Compute SHA-256 of JSON.stringify(exportData, null, 2)
  3. Store hash as uppercase in metadata.hash
  4. Sign canonical payload derived from this hash-bearing unsigned object
  5. Attach metadata.signatureVersion and metadata.signature

This ordering is required for deterministic verification on import.

After signing, confirmation export download packages are written as ZIP archives that include:

  • confirmation-data-*.json (signed confirmation payload)
  • striae-public-signing-key*.pem (public verification key)

Reference:

  • app/components/actions/confirm-export.ts
  • app/utils/data-operations.ts

Confirmation Canonical Payload Rules

createConfirmationSigningPayload builds deterministic JSON for signing:

  1. Include signatureVersion (currently 3.0)
  2. Include fixed metadata fields in stable order
  3. Normalize metadata.hash to uppercase
  4. Include originalExportCreatedAt only when present
  5. Include originalCaseOwnerUid only when present
  6. Sort confirmation map keys (image IDs)
  7. Sort entries per image by key:
    • confirmationId|confirmedAt|confirmedBy

Reference:

  • Worker canonicalizer: workers/data-worker/src/signing-payload-utils.ts
  • App canonicalizer/verifier: app/utils/confirmation-signature.ts

Confirmation Import-Time Verification

Confirmation import validates in this order:

  1. Parse confirmation import package (extractConfirmationImportPackage). The importer accepts an encrypted confirmation ZIP containing exactly one confirmation-data-*.json, requires ENCRYPTION_MANIFEST.json, and captures a ZIP-contained PEM when present for signature verification.
  2. Hash validation (validateConfirmationHash). The importer removes metadata.hash, metadata.signature, and metadata.signatureVersion, then recomputes SHA-256 and compares case-insensitively.
  3. Signature validation (validateConfirmationSignatureFile). The importer uses a ZIP-contained PEM when present; otherwise it resolves the configured key by keyId.
  4. Exporter UID validation and self-import block.
  5. Original case owner validation. When originalCaseOwnerUid is present in metadata, the importing user's UID must match. Package is rejected if it was not exported for the importing user's case.
  6. Case ownership checks and per-image update constraints.

If signature is missing/invalid or version is unsupported, import fails.

Reference:

  • app/components/actions/case-import/confirmation-import.ts
  • app/components/actions/case-import/confirmation-package.ts
  • app/components/actions/case-import/validation.ts
  • app/utils/confirmation-signature.ts

Data Worker Signing Endpoints

All signing endpoints are routed via the env.DATA_WORKER service binding from the Pages data proxy and return JSON.

POST /api/forensic/sign-manifest

Accepts either:

  • { "manifest": { ...manifestFields } }
  • direct manifest fields at root

Response:

{
  "success": true,
  "manifestVersion": "3.0",
  "signature": {
    "algorithm": "RSASSA-PSS-SHA-256",
    "keyId": "...",
    "signedAt": "...",
    "value": "..."
  }
}

POST /api/forensic/sign-confirmation

Accepts either:

  • { "confirmationData": { ... }, "signatureVersion": "3.0" }
  • direct confirmation payload at root (version defaults to 3.0)

Response:

{
  "success": true,
  "signatureVersion": "3.0",
  "signature": {
    "algorithm": "RSASSA-PSS-SHA-256",
    "keyId": "...",
    "signedAt": "...",
    "value": "..."
  }
}

Common errors:

  • 403 invalid/missing auth header
  • 400 invalid payload or unsupported version
  • 500 signing or parsing failures

Reference:

  • workers/data-worker/src/data-worker.ts

Verification Internals

verifySignaturePayload enforces:

  1. Expected algorithm match
  2. Signature key ID and signature value presence
  3. Public key resolution for key ID
  4. Web Crypto verify against canonical payload bytes

Failure reasons are returned as structured messages and surfaced by import workflows.

Reference:

  • app/utils/signature-utils.ts

Shared Verification Helpers

verifyExportFile in app/utils/export-verification.ts remains the shared helper for manual and workflow-level export validation logic.

Behavior:

  1. Accepts encrypted confirmation ZIP and case export ZIP inputs.
  2. For ZIP inputs, tests confirmation package structure first; if that fails, runs case ZIP verification.
  3. Uses the supplied PEM key for signature validation and combines this with integrity checks.
  4. Returns pass/fail plus user-facing messages without exposing expected/actual hash values.

The old standalone public-key verification modal has been removed from the current UI. Validation behavior now lives in shared workflow utilities and import/export code paths.

Reference:

  • app/utils/export-verification.ts

Implementation Notes

  • Canonicalization is implemented separately per domain payload to avoid drift:
    • Manifest in app/utils/SHA256.ts and worker signing helpers
    • Confirmation in app/utils/confirmation-signature.ts and worker signing helpers
  • The shared verifier (app/utils/signature-utils.ts) and shared worker signer (workers/data-worker/src/signature-utils.ts) should remain generic.
  • Keep payload field ordering and normalization rules stable; changing them invalidates existing signatures.

Related Documentation