From 275fad8674f30674884aa12f8c7f9fde90ceac5a Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Tue, 17 Feb 2026 22:29:06 -0300 Subject: [PATCH] docs: add cross-layer binding to sidecar format, crypto specs, trust model, and threat model --- docs/concepts/cryptographic-specs.md | 7 +++++++ docs/concepts/sidecar-format.md | 3 +++ docs/concepts/two-layer-trust.md | 7 +++++++ docs/security/threat-model.md | 9 +++++++++ 4 files changed, 26 insertions(+) diff --git a/docs/concepts/cryptographic-specs.md b/docs/concepts/cryptographic-specs.md index 08cef61..a309faf 100644 --- a/docs/concepts/cryptographic-specs.md +++ b/docs/concepts/cryptographic-specs.md @@ -201,6 +201,7 @@ Used for: "capture_id": "550e8400-e29b-41d4-a716-446655440000", "publisher_id": "9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c", "device_id": "ea5c9bfe-6bbc-4ee2-b82d-0bcfcc185ef1", + "device_public_key_fingerprint": "4ca63447117ea5c99614bcbe433eb393a1f8b2e14c7b3f5d8e9a0b1c2d3e4f56", "attestation": { "method": "app_check", "app_id": "io.signedshot.capture" @@ -314,6 +315,12 @@ def cross_validate(sidecar, jwt_payload): if sidecar.media_integrity.capture_id != jwt_payload['capture_id']: return False, "Capture ID mismatch" + # Device public key fingerprint must match (cross-layer binding) + public_key_bytes = base64_decode(sidecar.media_integrity.public_key) + fingerprint = sha256(public_key_bytes).hex() + if fingerprint != jwt_payload['device_public_key_fingerprint']: + return False, "Device public key fingerprint mismatch" + return True, None ``` diff --git a/docs/concepts/sidecar-format.md b/docs/concepts/sidecar-format.md index b639409..c99790d 100644 --- a/docs/concepts/sidecar-format.md +++ b/docs/concepts/sidecar-format.md @@ -75,6 +75,7 @@ The `capture_trust.jwt` is a standard JWT. When decoded, it contains: "capture_id": "550e8400-e29b-41d4-a716-446655440000", "publisher_id": "9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c", "device_id": "ea5c9bfe-6bbc-4ee2-b82d-0bcfcc185ef1", + "device_public_key_fingerprint": "4ca63447117ea5c99614bcbe433eb393...", "attestation": { "method": "app_check", "app_id": "io.signedshot.capture" @@ -93,6 +94,7 @@ The `capture_trust.jwt` is a standard JWT. When decoded, it contains: | `capture_id` | string | Unique capture session ID | | `publisher_id` | string | Publisher UUID | | `device_id` | string | Device UUID | +| `device_public_key_fingerprint` | string | SHA-256 of the device's content-signing public key (hex) | | `attestation` | object | Attestation details | ### attestation Object @@ -171,6 +173,7 @@ To verify a sidecar: - Verify ECDSA signature using `public_key` 4. **Cross-validate** - Confirm `capture_id` matches in both JWT and media_integrity + - Confirm `SHA-256(public_key)` matches `device_public_key_fingerprint` in the JWT ## Next Steps diff --git a/docs/concepts/two-layer-trust.md b/docs/concepts/two-layer-trust.md index 3edb116..7489d0c 100644 --- a/docs/concepts/two-layer-trust.md +++ b/docs/concepts/two-layer-trust.md @@ -36,6 +36,7 @@ Capture Trust answers: **"Was this captured by a legitimate device?"** | `capture_id` | Unique session identifier | | `method` | Attestation method: `sandbox`, `app_check`, or `app_attest` | | `app_id` | App bundle ID (when attested) | +| `device_public_key_fingerprint` | SHA-256 of the device's content-signing public key | | `issued_at` | Unix timestamp | ### Verification @@ -84,6 +85,12 @@ Neither layer alone is sufficient: Together, they create a complete chain of trust from device verification to content integrity. +### Cross-Layer Binding + +The two layers are cryptographically bound through the `device_public_key_fingerprint` — a SHA-256 hash of the device's content-signing public key. This fingerprint is computed at device registration and included in every JWT. + +During verification, the validator computes `SHA-256(public_key)` from the media integrity layer and checks it matches the `device_public_key_fingerprint` in the JWT. This prevents an attacker from combining a valid JWT with a media integrity proof signed by a different key. + ## The Sidecar File Both layers are stored in a JSON sidecar file that travels with the media: diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 8c94975..65bc107 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -71,6 +71,14 @@ SignedShot proves "this device captured this content at this time" — not "this **Verification:** Check the `method` field in the JWT. Reject `sandbox` for high-trust use cases. +### Cross-Layer Substitution + +**Threat:** An attacker performs a legitimate capture to obtain a valid JWT, then generates a new key pair and signs different content with it, combining the valid JWT with the forged media integrity proof. + +**Mitigation:** The JWT contains `device_public_key_fingerprint` — the SHA-256 hash of the device's content-signing public key, computed at registration. The validator checks that `SHA-256(public_key from media integrity)` matches this fingerprint, binding the two layers cryptographically. + +**Verification:** Confirm `SHA-256(public_key)` matches `device_public_key_fingerprint` in the JWT. + ### Metadata Forgery **Threat:** An attacker manipulates EXIF timestamps, GPS coordinates, or other metadata. @@ -182,6 +190,7 @@ For maximum security, verify all of the following: - [ ] Content hash matches file - [ ] Media integrity signature valid - [ ] `capture_id` matches in JWT and media integrity +- [ ] `device_public_key_fingerprint` matches `SHA-256(public_key)` from media integrity - [ ] `captured_at` is reasonable (not in future, not too old) ## Next Steps