ADR-010: Partner JWS Integrity Verification
On this page
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.
-
Officer visits CRAIG’s key registration page
-
WebCrypto generates ECDSA P-256 keypair in the officer’s browser
-
Public key is POSTed to CRAIG (
POST /public/v1/keys) -
Private key is downloaded as a
.jwkfile (never leaves the officer’s device) -
CRAIG admin approves the key registration (prevents impersonation)
-
Officer provides
key_idand.jwkfile to their IT department for vendor software configuration
Key states: pending → approved → revoked
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).