ADR-010: Partner JWS Integrity Verification

On this page

Status

Accepted (2026-03-16)

Context

Third-party vendors (police departments, hospitals, schools) submit child abuse reports to CRAIG’s intake service via the partner API (POST /partner/v1/reports) using API key authentication. API keys prove which vendor organization sent a request, but cannot prove that the individual reporter (e.g., a police officer) authored the content unaltered.

In child welfare, report integrity has legal and evidentiary significance. Courts may need to verify that a report submitted by an officer through a vendor’s system was not altered by the vendor before reaching CRAIG. This requires cryptographic non-repudiation at the individual reporter level.

The trust chain problem: if the vendor holds the only signing key, the vendor signs its own output — proving the vendor sent it, but not that the vendor didn’t change what the officer typed. The signing key must be in a place the vendor cannot access.

Decision

Adopt ECDSA P-256 (ES256) detached JWS with a CRAIG-hosted public key registry for partner report integrity verification.

Algorithm: ECDSA P-256 (ES256)

Chosen over alternatives:

  • RS256 (RSA-2048): Larger keys (2048 bits vs 256 bits), slower signing. No benefit for this use case.

  • HS256 (HMAC): Symmetric — CRAIG and the reporter share a secret. CRAIG could forge signatures, defeating non-repudiation.

  • Ed25519: Not supported by WebCrypto API in all browsers.

ES256 provides: compact keys (64-byte public key), fast signing/verification, native WebCrypto support, broad library support (p256 Rust crate, jose JS, joserfc Python).

Signature Format: Detached JWS Compact Serialization

The JWS compact format is header..signature (empty payload segment). The payload travels in the HTTP request body, not inside the JWS. This avoids payload duplication and integrates naturally with existing REST endpoints.

The JWS header contains {"alg": "ES256", "kid": "<key_id>"} where kid identifies the signer’s registered public key.

Key Registry: CRAIG-Hosted

Public keys are registered directly with CRAIG through a public web page (/report/keys). This ensures the vendor cannot control the key-to-identity mapping.

Key lifecycle
  1. Officer visits CRAIG’s key registration page

  2. WebCrypto generates ECDSA P-256 keypair in the officer’s browser

  3. Public key is POSTed to CRAIG (POST /public/v1/keys)

  4. Private key is downloaded as a .jwk file (never leaves the officer’s device)

  5. CRAIG admin approves the key registration (prevents impersonation)

  6. Officer provides key_id and .jwk file to their IT department for vendor software configuration

Key states: pendingapprovedrevoked

Canonicalization

Before signing, the JSON payload is canonicalized: all object keys sorted lexicographically at every nesting level, serialized with no extra whitespace. All signing implementations (Rust, TypeScript, Python, browser JS) must produce identical output for the same input. A shared test vector file ensures cross-language consistency.

Mandatory JWS for Partner API

JWS is required on the partner endpoint (POST /partner/v1/reports). Requests without the X-JWS-Signature header are rejected with a 400 error. This ensures that every partner-submitted report has cryptographic proof of authorship from the moment the feature is deployed.

Public (web form) submissions do not use JWS — they go through CAPTCHA-protected endpoints and are not subject to the same non-repudiation requirements.

Storage

The JWS signature string, signer key ID, and a SHA-256 hash of the canonical payload are stored on the report record. This enables:

  • Audit: any authorized user can re-verify the signature at any time

  • Tamper detection: if the stored report body has been modified since submission, re-verification fails

  • Non-repudiation: the stored JWS proves who signed the report

Consequences

Positive

  • Cryptographic proof that a specific individual authored a report, independent of the vendor system

  • Vendor cannot alter a signed report without breaking the signature

  • CRAIG cannot alter a signed report without breaking the signature (mutual integrity)

  • Ensures every partner-submitted report has cryptographic proof of authorship from day one

  • WebCrypto-based key generation requires no software installation for officers

Negative

  • Key management burden: officers must safeguard private key files, CRAIG admins must approve registrations

  • Canonicalization is a cross-language contract — any divergence breaks verification

  • Officers using the vendor’s server-side web app need client-side signing integration (browser JS library or vendor frontend changes)

Risks

  • If the vendor’s web app does not integrate client-side signing, the JWS feature provides no benefit (the vendor could still alter the content before signing on the server)

  • Private key loss requires re-registration and re-approval

  • WebCrypto API requires HTTPS (standard for production, but development environments need consideration)

Alternatives Considered

Department-Hosted JWKS

Each department hosts a JWKS endpoint with officer public keys. CRAIG fetches keys from the department’s URL. Rejected because: adds dependency on department infrastructure, introduces availability concerns, and departments may not have JWKS hosting capability.

Certificate Chain (PKI)

Officers get certificates signed by a department CA. JWS includes the certificate chain (x5c header). Rejected for initial implementation due to PKI infrastructure complexity. Noted as a potential future tier for agencies with existing PKI (e.g., PIV/CAC cards in law enforcement).

Vendor-Level Signing Only

The vendor signs with its organizational key. Rejected because: proves the vendor sent it, but cannot prove the vendor didn’t alter the individual reporter’s content.

Edit this page · latest