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-*.jsonmetadata. - Confirmation exports are delivered as ZIP packages that contain
confirmation-data-*.jsonplus 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.tsworkers/data-worker/src/signature-utils.tsworkers/data-worker/src/data-worker.tsapp/utils/SHA256.tsapp/utils/confirmation-signature.tsapp/utils/signature-utils.tsapp/utils/export-verification.tsapp/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-PSSandSHA-256 - Salt length:
32bytes
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.jsoncworkers/data-worker/src/signature-utils.ts
Verification Keys (App)
App config supports key rotation and fallback:
manifest_signing_public_keysmap bykeyId(preferred)manifest_signing_public_key+manifest_signing_key_idfallback
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.jsonapp/config-example/config.jsonapp/utils/signature-utils.ts
Manifest Signing
Manifest Canonical Payload Rules
createManifestSigningPayload builds deterministic JSON for signing:
- Include
manifestVersion(currently3.0) - Lowercase
dataHash - Sort
imageHashesby filename and lowercase each hash - Lowercase
manifestHash - Preserve
totalFilesandcreatedAt
Reference:
- Worker canonicalizer:
workers/data-worker/src/signing-payload-utils.ts - App canonicalizer/verifier:
app/utils/SHA256.ts
Export-Time Flow
- Export code builds secure manifest via
generateForensicManifestSecure. - App requests signature via
signForensicManifest. - Data Worker validates payload and signs canonical payload.
- Export ZIP stores signed manifest as
FORENSIC_MANIFEST.jsonwith:- manifest data
manifestVersionsignature
- Export ZIP also stores the active public signing key PEM file for independent verification.
Reference:
app/components/actions/case-export/download-handlers.tsapp/utils/data-operations.tsworkers/data-worker/src/data-worker.ts
Manifest Import-Time Verification
Case import is fail-closed:
- Require
FORENSIC_MANIFEST.json - Verify manifest schema/data extraction
- Resolve verification key source:
- Use ZIP-contained PEM when present
- Otherwise resolve configured key by
keyId
- Verify signature (
verifyForensicManifestSignature) - Verify comprehensive integrity (data hash + image hashes + manifest hash)
- Reject import on any failure
Reference:
app/components/actions/case-import/orchestrator.tsapp/components/actions/case-import/zip-processing.tsapp/utils/SHA256.ts
Confirmation Signing
Hash-Then-Sign Rule
Confirmation exports are hashed before signature metadata is attached:
- Build unsigned export object (
metadata+confirmations) - Compute SHA-256 of
JSON.stringify(exportData, null, 2) - Store hash as uppercase in
metadata.hash - Sign canonical payload derived from this hash-bearing unsigned object
- Attach
metadata.signatureVersionandmetadata.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.tsapp/utils/data-operations.ts
Confirmation Canonical Payload Rules
createConfirmationSigningPayload builds deterministic JSON for signing:
- Include
signatureVersion(currently3.0) - Include fixed metadata fields in stable order
- Normalize
metadata.hashto uppercase - Include
originalExportCreatedAtonly when present - Include
originalCaseOwnerUidonly when present - Sort confirmation map keys (image IDs)
- 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:
- Parse confirmation import package (
extractConfirmationImportPackage). The importer accepts an encrypted confirmation ZIP containing exactly oneconfirmation-data-*.json, requiresENCRYPTION_MANIFEST.json, and captures a ZIP-contained PEM when present for signature verification. - Hash validation (
validateConfirmationHash). The importer removesmetadata.hash,metadata.signature, andmetadata.signatureVersion, then recomputes SHA-256 and compares case-insensitively. - Signature validation (
validateConfirmationSignatureFile). The importer uses a ZIP-contained PEM when present; otherwise it resolves the configured key bykeyId. - Exporter UID validation and self-import block.
- Original case owner validation. When
originalCaseOwnerUidis present in metadata, the importing user's UID must match. Package is rejected if it was not exported for the importing user's case. - 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.tsapp/components/actions/case-import/confirmation-package.tsapp/components/actions/case-import/validation.tsapp/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:
403invalid/missing auth header400invalid payload or unsupported version500signing or parsing failures
Reference:
workers/data-worker/src/data-worker.ts
Verification Internals
verifySignaturePayload enforces:
- Expected algorithm match
- Signature key ID and signature value presence
- Public key resolution for key ID
- 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:
- Accepts encrypted confirmation ZIP and case export ZIP inputs.
- For ZIP inputs, tests confirmation package structure first; if that fails, runs case ZIP verification.
- Uses the supplied PEM key for signature validation and combines this with integrity checks.
- 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.tsand worker signing helpers - Confirmation in
app/utils/confirmation-signature.tsand worker signing helpers
- Manifest in
- 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.