diff --git a/docs/api-reference/overview.md b/docs/api-reference/overview.md new file mode 100644 index 0000000..05345ab --- /dev/null +++ b/docs/api-reference/overview.md @@ -0,0 +1,387 @@ +--- +sidebar_position: 1 +--- + +# API Reference + +The SignedShot API provides cryptographic proof of authenticity for photos and videos. + +## Base URL + +``` +https://dev-api.signedshot.io +``` + +:::note Staging Environment +During the MVP launch, use `dev-api.signedshot.io` for development and testing. + +Production API (`api.signedshot.io`) will be available post-launch with proper account registration and authentication. +::: + +## Authentication + +Most endpoints require authentication via the `Authorization` header: + +``` +Authorization: Bearer +``` + +| Token Type | Used For | Obtained From | +|------------|----------|---------------| +| Device Token | Capture endpoints | `POST /devices` response | + +## Endpoints Overview + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| POST | `/publishers` | Create a publisher | None | +| GET | `/publishers/{id}` | Get publisher details | None | +| PATCH | `/publishers/{id}` | Update publisher | None | +| POST | `/devices` | Register a device | Header | +| POST | `/capture/session` | Start capture session | Bearer | +| POST | `/capture/trust` | Exchange nonce for JWT | Bearer | +| POST | `/validate` | Validate media + sidecar | None | +| GET | `/.well-known/jwks.json` | Public keys for JWT verification | None | + +--- + +## Publishers + +Publishers represent apps or organizations that capture signed media. + +### Create Publisher + +``` +POST /publishers +``` + +**Request Body:** + +```json +{ + "name": "My Camera App", + "sandbox": true, + "attestation_provider": "NONE", + "firebase_project_id": null, + "attestation_bundle_id": null, + "track_devices": false +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Display name (1-255 chars) | +| `sandbox` | boolean | No | Sandbox mode (default: `true`) | +| `attestation_provider` | string | No | `"NONE"` or `"FIREBASE_APP_CHECK"` | +| `firebase_project_id` | string | No | Firebase project ID (required for App Check) | +| `attestation_bundle_id` | string | No | App bundle ID for attestation | +| `track_devices` | boolean | No | Enable device tracking (default: `false`) | + +**Response (201):** + +```json +{ + "publisher_id": "9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c", + "name": "My Camera App", + "sandbox": true, + "attestation_provider": "NONE", + "firebase_project_id": null, + "attestation_bundle_id": null, + "track_devices": false, + "created_at": "2025-01-15T10:30:00Z" +} +``` + +### Get Publisher + +``` +GET /publishers/{publisher_id} +``` + +**Response (200):** Same as create response. + +**Errors:** +- `404` — Publisher not found + +### Update Publisher + +``` +PATCH /publishers/{publisher_id} +``` + +Only provided fields are updated. + +**Request Body:** + +```json +{ + "sandbox": false, + "attestation_provider": "FIREBASE_APP_CHECK", + "firebase_project_id": "my-project-123", + "attestation_bundle_id": "io.signedshot.capture" +} +``` + +**Response (200):** Updated publisher object. + +**Errors:** +- `404` — Publisher not found + +--- + +## Devices + +Devices are registered once per app installation and receive a token for authentication. + +### Register Device + +``` +POST /devices +``` + +**Headers:** + +| Header | Required | Description | +|--------|----------|-------------| +| `X-Publisher-ID` | Yes | Publisher UUID | +| `X-Attestation-Token` | Conditional | Firebase App Check token (required if publisher has attestation enabled) | + +**Request Body:** + +```json +{ + "external_id": "device-abc-123" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `external_id` | string | Yes | Unique device identifier (1-255 chars) | + +**Response (201):** + +```json +{ + "device_id": "ea5c9bfe-6bbc-4ee2-b82d-0bcfcc185ef1", + "publisher_id": "9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c", + "external_id": "device-abc-123", + "device_token": "eyJhbGciOiJIUzI1NiIs...", + "created_at": "2025-01-15T10:30:00Z" +} +``` + +**Important:** Store `device_token` securely. It's only returned once. + +**Errors:** +- `400` — Invalid publisher ID format +- `401` — Attestation verification failed +- `404` — Publisher not found +- `409` — Device already registered +- `500` — Attestation not configured for non-sandbox publisher + +--- + +## Capture + +The capture flow creates a session before capturing and exchanges a nonce for a trust token after. + +### Create Session + +``` +POST /capture/session +``` + +**Headers:** + +``` +Authorization: Bearer +``` + +**Response (201):** + +```json +{ + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "nonce": "a1b2c3d4e5f6...", + "expires_at": "2025-01-15T10:35:00Z" +} +``` + +| Field | Description | +|-------|-------------| +| `capture_id` | UUID for this capture (include in sidecar) | +| `nonce` | One-time token to exchange for trust token | +| `expires_at` | Session expiration (complete capture before this) | + +**Errors:** +- `401` — Invalid or missing device token + +### Exchange Trust Token + +``` +POST /capture/trust +``` + +**Headers:** + +``` +Authorization: Bearer +``` + +**Request Body:** + +```json +{ + "nonce": "a1b2c3d4e5f6..." +} +``` + +**Response (200):** + +```json +{ + "trust_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0xIn0..." +} +``` + +The `trust_token` is an ES256-signed JWT containing: + +```json +{ + "iss": "https://dev-api.signedshot.io", + "aud": "signedshot", + "sub": "capture-service", + "iat": 1705312200, + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "publisher_id": "9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c", + "device_id": "ea5c9bfe-6bbc-4ee2-b82d-0bcfcc185ef1", + "attestation": { + "method": "app_check", + "app_id": "io.signedshot.capture" + } +} +``` + +**Errors:** +- `400` — Invalid or expired nonce +- `401` — Invalid device token + +--- + +## Validate + +Verify a media file against its sidecar. + +### Validate Media + +``` +POST /validate +Content-Type: multipart/form-data +``` + +**Form Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `media` | file | The media file (photo/video) | +| `sidecar` | file | The sidecar JSON file | + +**Response (200):** + +```json +{ + "valid": true, + "version": "1.0", + "capture_trust": { + "signature_valid": true, + "issuer": "https://dev-api.signedshot.io", + "publisher_id": "9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c", + "device_id": "ea5c9bfe-6bbc-4ee2-b82d-0bcfcc185ef1", + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "method": "app_check", + "app_id": "io.signedshot.capture", + "issued_at": 1705312200, + "key_id": "key-1" + }, + "media_integrity": { + "content_hash_valid": true, + "signature_valid": true, + "capture_id_match": true, + "content_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "captured_at": "2025-01-15T10:30:00Z" + }, + "error": null +} +``` + +**Errors:** +- `400` — Invalid sidecar format or validation error + +--- + +## JWKS + +Public keys for verifying JWT signatures. + +### Get JWKS + +``` +GET /.well-known/jwks.json +``` + +**Response (200):** + +```json +{ + "keys": [ + { + "kty": "EC", + "crv": "P-256", + "kid": "key-1", + "x": "...", + "y": "...", + "use": "sig", + "alg": "ES256" + } + ] +} +``` + +Use the `kid` from the JWT header to find the matching key. + +--- + +## Error Responses + +All errors follow this format: + +```json +{ + "detail": "Error message describing the issue" +} +``` + +### Common Status Codes + +| Code | Meaning | +|------|---------| +| `400` | Bad request (invalid input) | +| `401` | Unauthorized (missing/invalid token) | +| `404` | Resource not found | +| `409` | Conflict (duplicate resource) | +| `500` | Server error | + +--- + +## Rate Limits + +The API currently has no rate limits. This may change in the future. + +--- + +## Next Steps + +- [Quick Start](/guides/quick-start) — Verify media with Python +- [iOS Integration](/guides/ios-integration) — Capture signed media +- [Sidecar Format](/concepts/sidecar-format) — Proof structure reference diff --git a/docs/concepts/cryptographic-specs.md b/docs/concepts/cryptographic-specs.md new file mode 100644 index 0000000..08cef61 --- /dev/null +++ b/docs/concepts/cryptographic-specs.md @@ -0,0 +1,355 @@ +--- +sidebar_position: 3 +--- + +# Cryptographic Specifications + +This document provides detailed specifications for the cryptographic algorithms, key formats, and encoding standards used by SignedShot. + +## Overview + +SignedShot uses industry-standard cryptographic primitives: + +| Component | Algorithm | Standard | +|-----------|-----------|----------| +| Content hashing | SHA-256 | FIPS 180-4 | +| Device signatures | ECDSA P-256 | FIPS 186-4, SEC 2 | +| JWT signatures | ES256 | RFC 7518 | +| Key storage | Secure Enclave | Apple Platform Security | + +## Hash Algorithm + +### SHA-256 + +All content hashing uses SHA-256 (Secure Hash Algorithm 256-bit). + +**Properties:** +- Output: 256 bits (32 bytes) +- Collision resistant +- Preimage resistant + +**Encoding:** Lowercase hexadecimal (64 characters) + +**Example:** +``` +Input: [binary media file] +Output: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 +``` + +**Standard:** FIPS 180-4 + +## Signature Algorithms + +### Device Signatures (ECDSA P-256) + +Device signatures use ECDSA (Elliptic Curve Digital Signature Algorithm) with the P-256 curve. + +**Parameters:** +- Curve: NIST P-256 (secp256r1, prime256v1) +- Key size: 256 bits +- Signature format: DER-encoded (X9.62) + +**Key Generation:** +- Generated in the device's Secure Enclave +- Private key never leaves secure hardware +- One key pair per device + +**Signed Message Format:** +``` +{content_hash}:{capture_id}:{captured_at} +``` + +**Example message:** +``` +9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08:550e8400-e29b-41d4-a716-446655440000:2025-01-15T10:30:00Z +``` + +**Signature encoding:** Base64 + +**Standards:** FIPS 186-4, SEC 2, ANSI X9.62 + +### JWT Signatures (ES256) + +JWTs are signed using ES256 (ECDSA with P-256 and SHA-256). + +**Parameters:** +- Algorithm: ES256 +- Curve: P-256 +- Hash: SHA-256 + +**Key management:** +- Server-side key pair +- Public keys published via JWKS endpoint +- Key ID (`kid`) in JWT header for key selection + +**Standard:** RFC 7518 (JSON Web Algorithms) + +## Key Formats + +### Device Public Key + +The device's public key is stored in the sidecar as an uncompressed EC point. + +**Format:** Uncompressed point (X9.62) +- Prefix: `0x04` +- X coordinate: 32 bytes +- Y coordinate: 32 bytes +- Total: 65 bytes + +**Encoding:** Base64 + +**Example:** +``` +BHx5yK3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3M= +``` + +**Decoding steps:** +1. Base64 decode → 65 bytes +2. Verify prefix is `0x04` +3. Extract X (bytes 1-32) and Y (bytes 33-64) + +### JWKS Public Keys + +Server public keys are published in JWK (JSON Web Key) format. + +**Endpoint:** `/.well-known/jwks.json` + +**Example:** +```json +{ + "keys": [ + { + "kty": "EC", + "crv": "P-256", + "kid": "key-1", + "use": "sig", + "alg": "ES256", + "x": "base64url-encoded-x-coordinate", + "y": "base64url-encoded-y-coordinate" + } + ] +} +``` + +**Fields:** +| Field | Value | Description | +|-------|-------|-------------| +| `kty` | `"EC"` | Key type (Elliptic Curve) | +| `crv` | `"P-256"` | Curve name | +| `kid` | string | Key identifier | +| `use` | `"sig"` | Key usage (signature) | +| `alg` | `"ES256"` | Algorithm | +| `x` | base64url | X coordinate | +| `y` | base64url | Y coordinate | + +**Standard:** RFC 7517 (JSON Web Key) + +## Encoding Standards + +### Base64 + +Used for: +- Device signatures +- Device public keys + +**Alphabet:** Standard Base64 (A-Z, a-z, 0-9, +, /) +**Padding:** With `=` padding + +### Base64URL + +Used for: +- JWT components +- JWKS coordinates + +**Alphabet:** URL-safe Base64 (A-Z, a-z, 0-9, -, _) +**Padding:** Without padding + +### Hexadecimal + +Used for: +- Content hashes + +**Format:** Lowercase, no prefix +**Length:** 64 characters for SHA-256 + +## JWT Structure + +### Header + +```json +{ + "alg": "ES256", + "typ": "JWT", + "kid": "key-1" +} +``` + +| Field | Description | +|-------|-------------| +| `alg` | Signature algorithm (always `ES256`) | +| `typ` | Token type (always `JWT`) | +| `kid` | Key ID for JWKS lookup | + +### Payload + +```json +{ + "iss": "https://dev-api.signedshot.io", + "aud": "signedshot", + "sub": "capture-service", + "iat": 1705312200, + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "publisher_id": "9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c", + "device_id": "ea5c9bfe-6bbc-4ee2-b82d-0bcfcc185ef1", + "attestation": { + "method": "app_check", + "app_id": "io.signedshot.capture" + } +} +``` + +### Signature + +The signature is computed over: +``` +base64url(header) + "." + base64url(payload) +``` + +Using ES256 (ECDSA P-256 with SHA-256). + +**Standard:** RFC 7519 (JSON Web Token) + +## Timestamp Formats + +### ISO 8601 (Media Integrity) + +Used for `captured_at` in media integrity. + +**Format:** `YYYY-MM-DDTHH:mm:ssZ` +**Timezone:** UTC (indicated by `Z` suffix) +**Precision:** Seconds (no fractional seconds) + +**Example:** `2025-01-15T10:30:00Z` + +### Unix Timestamp (JWT) + +Used for `iat` (issued at) in JWT payload. + +**Format:** Integer seconds since Unix epoch (1970-01-01T00:00:00Z) + +**Example:** `1705312200` + +## UUID Format + +All identifiers use UUID v4. + +**Format:** `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` +- `x`: hexadecimal digit +- `4`: version (always 4) +- `y`: variant (8, 9, a, or b) + +**Example:** `550e8400-e29b-41d4-a716-446655440000` + +**Standard:** RFC 4122 + +## Verification Pseudocode + +### Verify Media Integrity + +```python +def verify_media_integrity(media_bytes, sidecar): + # 1. Compute hash + computed_hash = sha256(media_bytes).hex() + + # 2. Compare hashes + if computed_hash != sidecar.media_integrity.content_hash: + return False, "Hash mismatch" + + # 3. Reconstruct signed message + message = f"{sidecar.media_integrity.content_hash}:{sidecar.media_integrity.capture_id}:{sidecar.media_integrity.captured_at}" + + # 4. Decode public key (base64 → 65 bytes uncompressed point) + public_key = base64_decode(sidecar.media_integrity.public_key) + + # 5. Decode signature (base64 → DER-encoded ECDSA signature) + signature = base64_decode(sidecar.media_integrity.signature) + + # 6. Verify ECDSA signature + if not ecdsa_verify(public_key, message.encode('utf-8'), signature): + return False, "Invalid signature" + + return True, None +``` + +### Verify Capture Trust + +```python +def verify_capture_trust(sidecar, jwks): + jwt = sidecar.capture_trust.jwt + + # 1. Decode JWT header (without verification) + header = jwt_decode_header(jwt) + kid = header['kid'] + + # 2. Find key in JWKS + key = find_key_by_kid(jwks, kid) + if key is None: + return False, "Key not found" + + # 3. Verify JWT signature + if not jwt_verify(jwt, key, algorithm='ES256'): + return False, "Invalid JWT signature" + + # 4. Decode payload + payload = jwt_decode_payload(jwt) + + return True, payload +``` + +### Cross-Validate + +```python +def cross_validate(sidecar, jwt_payload): + # Capture IDs must match + if sidecar.media_integrity.capture_id != jwt_payload['capture_id']: + return False, "Capture ID mismatch" + + return True, None +``` + +## Security Considerations + +### Key Security + +- Device private keys are generated in and never leave the Secure Enclave +- Server signing keys should be stored in HSM or secure key management +- Key rotation is supported via JWKS `kid` field + +### Signature Security + +- ECDSA P-256 provides ~128 bits of security +- SHA-256 is collision-resistant with 256-bit output +- Both meet current security standards (NIST, FIPS) + +### Timing Attacks + +- Signature verification should use constant-time comparison +- Hash comparison should use constant-time comparison + +## References + +| Standard | Document | +|----------|----------| +| SHA-256 | [FIPS 180-4](https://csrc.nist.gov/publications/detail/fips/180/4/final) | +| ECDSA | [FIPS 186-4](https://csrc.nist.gov/publications/detail/fips/186/4/final) | +| P-256 | [SEC 2](https://www.secg.org/sec2-v2.pdf) | +| JWT | [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) | +| JWK | [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517) | +| JWA | [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518) | +| UUID | [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) | + +## Next Steps + +- [Sidecar Format](/concepts/sidecar-format) — Full JSON schema reference +- [Two-Layer Trust](/concepts/two-layer-trust) — Understand the trust model +- [Threat Model](/security/threat-model) — Security analysis diff --git a/docs/concepts/sidecar-format.md b/docs/concepts/sidecar-format.md new file mode 100644 index 0000000..b639409 --- /dev/null +++ b/docs/concepts/sidecar-format.md @@ -0,0 +1,178 @@ +--- +sidebar_position: 2 +--- + +# Sidecar Format + +The sidecar file contains both layers of cryptographic proof in a single JSON document that travels alongside the media file. + +## Overview + +A sidecar file (e.g., `photo.sidecar.json`) accompanies each media file (e.g., `photo.jpg`). This separation keeps the original media untouched while providing all verification data. + +## Why JSON? + +The proof format is JSON for three reasons: + +- **Human-readable** — Developers can inspect proofs without special tools +- **Flexible storage** — Store as a file, in a database, on a blockchain, or transmit via API +- **Easy integration** — Every platform has JSON libraries + +## Structure + +```json +{ + "version": "1.0", + "capture_trust": { + "jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9..." + }, + "media_integrity": { + "content_hash": "a1b2c3d4e5f6...", + "signature": "MEUCIQC...", + "public_key": "BHx5y...", + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "captured_at": "2025-01-15T10:30:00Z" + } +} +``` + +## Fields + +### Root + +| Field | Type | Description | +|-------|------|-------------| +| `version` | string | Schema version (currently `"1.0"`) | +| `capture_trust` | object | Server-issued trust token | +| `media_integrity` | object | Device-generated integrity proof | + +### capture_trust + +| Field | Type | Description | +|-------|------|-------------| +| `jwt` | string | ES256-signed JWT from the SignedShot API | + +### media_integrity + +| Field | Type | Description | +|-------|------|-------------| +| `content_hash` | string | SHA-256 hash of the media file (hex, 64 characters) | +| `signature` | string | ECDSA signature over the signed message (base64) | +| `public_key` | string | Device's public key (base64, uncompressed EC point, 65 bytes) | +| `capture_id` | string | UUID of the capture session (must match JWT) | +| `captured_at` | string | ISO8601 UTC timestamp of capture | + +## JWT Payload + +The `capture_trust.jwt` is a standard JWT. When decoded, it contains: + +```json +{ + "iss": "https://api.signedshot.io", + "aud": "signedshot", + "sub": "capture-service", + "iat": 1705312200, + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "publisher_id": "9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c", + "device_id": "ea5c9bfe-6bbc-4ee2-b82d-0bcfcc185ef1", + "attestation": { + "method": "app_check", + "app_id": "io.signedshot.capture" + } +} +``` + +### JWT Fields + +| Field | Type | Description | +|-------|------|-------------| +| `iss` | string | Issuer (API URL) | +| `aud` | string | Audience | +| `sub` | string | Subject (always `"capture-service"`) | +| `iat` | number | Issued at (Unix timestamp) | +| `capture_id` | string | Unique capture session ID | +| `publisher_id` | string | Publisher UUID | +| `device_id` | string | Device UUID | +| `attestation` | object | Attestation details | + +### attestation Object + +| Field | Type | Description | +|-------|------|-------------| +| `method` | string | `"sandbox"`, `"app_check"`, or `"app_attest"` | +| `app_id` | string | App bundle ID (only present when attested) | + +## Signature Format + +### Media Integrity Signature + +The `media_integrity.signature` signs a message constructed from: + +``` +{content_hash}:{capture_id}:{captured_at} +``` + +For example: +``` +a1b2c3d4e5f6...:550e8400-e29b-41d4-a716-446655440000:2025-01-15T10:30:00Z +``` + +The signature is: +- Algorithm: ECDSA with P-256 curve +- Generated by the device's Secure Enclave +- Encoded as base64 + +### JWT Signature + +The JWT uses: +- Algorithm: ES256 (ECDSA with P-256) +- Key ID (`kid`) in header for JWKS lookup +- Verification via `/.well-known/jwks.json` + +## File Naming Convention + +| Media File | Sidecar File | +|------------|--------------| +| `photo.jpg` | `photo.sidecar.json` | +| `video.mp4` | `video.sidecar.json` | +| `IMG_1234.HEIC` | `IMG_1234.sidecar.json` | + +## Example: Complete Sidecar + +```json +{ + "version": "1.0", + "capture_trust": { + "jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0xIn0.eyJpc3MiOiJodHRwczovL2FwaS5zaWduZWRzaG90LmlvIiwiYXVkIjoic2lnbmVkc2hvdCIsInN1YiI6ImNhcHR1cmUtc2VydmljZSIsImlhdCI6MTcwNTMxMjIwMCwiY2FwdHVyZV9pZCI6IjU1MGU4NDAwLWUyOWItNDFkNC1hNzE2LTQ0NjY1NTQ0MDAwMCIsInB1Ymxpc2hlcl9pZCI6IjlhNWIxMDYyLWE4ZmUtNDg3MS1iZGMxLWZlNTRlOTZjYmYxYyIsImRldmljZV9pZCI6ImVhNWM5YmZlLTZiYmMtNGVlMi1iODJkLTBiY2ZjYzE4NWVmMSIsImF0dGVzdGF0aW9uIjp7Im1ldGhvZCI6ImFwcF9jaGVjayIsImFwcF9pZCI6ImlvLnNpZ25lZHNob3QuY2FwdHVyZSJ9fQ.signature" + }, + "media_integrity": { + "content_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + "signature": "MEUCIQDKZokqnCjrRtw+3S0P2mjJH+E8zRqgaG6R4bG6V7oONwIgF3lQsGV1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3M=", + "public_key": "BHx5yK3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3K1K3M=", + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "captured_at": "2025-01-15T10:30:00Z" + } +} +``` + +## Verification Steps + +To verify a sidecar: + +1. **Parse the sidecar JSON** +2. **Verify Capture Trust (JWT)** + - Fetch JWKS from issuer's `/.well-known/jwks.json` + - Find key by `kid` in JWT header + - Verify ES256 signature +3. **Verify Media Integrity** + - Compute SHA-256 of media file + - Compare with `content_hash` + - Reconstruct signed message: `{hash}:{capture_id}:{captured_at}` + - Verify ECDSA signature using `public_key` +4. **Cross-validate** + - Confirm `capture_id` matches in both JWT and media_integrity + +## Next Steps + +- [Two-Layer Trust Model](/concepts/two-layer-trust) — Understand why both layers are needed +- [Python Validation](/guides/python-validation) — Verify sidecars programmatically diff --git a/docs/concepts/two-layer-trust.md b/docs/concepts/two-layer-trust.md new file mode 100644 index 0000000..3edb116 --- /dev/null +++ b/docs/concepts/two-layer-trust.md @@ -0,0 +1,125 @@ +--- +sidebar_position: 1 +--- + +# Two-Layer Trust Model + +SignedShot uses two complementary layers of cryptographic proof to verify media authenticity. + +## Overview + +When you capture media with SignedShot, two things happen: + +1. **Capture Trust** — The server issues a signed token proving a verified device started a capture session +2. **Media Integrity** — The device signs the content hash using its Secure Enclave + +Together, these prove: *"This exact content was captured on a verified device, in an authorized session, and hasn't been modified since."* + +## Layer 1: Capture Trust + +Capture Trust answers: **"Was this captured by a legitimate device?"** + +### How it works + +1. Device registers with the SignedShot API (once) +2. Device requests a capture session before capturing +3. Server verifies the device via attestation (Firebase App Check or App Attest) +4. Server issues a signed JWT containing session details + +### What's in the JWT + +| Field | Description | +|-------|-------------| +| `issuer` | API that issued the token (e.g., `https://api.signedshot.io`) | +| `publisher_id` | Publisher who owns the app | +| `device_id` | Unique device identifier | +| `capture_id` | Unique session identifier | +| `method` | Attestation method: `sandbox`, `app_check`, or `app_attest` | +| `app_id` | App bundle ID (when attested) | +| `issued_at` | Unix timestamp | + +### Verification + +The JWT is signed with ES256 (P-256 ECDSA). Anyone can verify it using the public keys from: + +``` +https://api.signedshot.io/.well-known/jwks.json +``` + +## Layer 2: Media Integrity + +Media Integrity answers: **"Has this content been modified?"** + +### How it works + +1. Device computes SHA-256 hash of the media bytes +2. Device signs the hash + metadata using its Secure Enclave private key +3. Signature is stored in the sidecar alongside the content hash + +### What's in Media Integrity + +| Field | Description | +|-------|-------------| +| `content_hash` | SHA-256 hash of the media (hex, 64 characters) | +| `signature` | ECDSA signature (base64) | +| `public_key` | Device's public key (base64, uncompressed EC point) | +| `capture_id` | Must match the JWT's capture_id | +| `captured_at` | ISO8601 timestamp | + +### Verification + +1. Compute SHA-256 of the media file +2. Compare with `content_hash` in sidecar +3. Verify ECDSA signature using the `public_key` +4. Confirm `capture_id` matches the JWT + +## Why Two Layers? + +Neither layer alone is sufficient: + +| Layer | What it proves | What it doesn't prove | +|-------|---------------|----------------------| +| **Capture Trust only** | Device is legitimate | Content wasn't modified after capture | +| **Media Integrity only** | Content wasn't modified | Device was legitimate | + +Together, they create a complete chain of trust from device verification to content integrity. + +## The Sidecar File + +Both layers are stored in a JSON sidecar file that travels with the media: + +```json +{ + "version": "1.0", + "capture_trust": { + "jwt": "eyJhbGciOiJFUzI1NiIs..." + }, + "media_integrity": { + "content_hash": "a1b2c3d4...", + "signature": "MEUCIQC...", + "public_key": "BHx5y...", + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "captured_at": "2025-01-15T10:30:00Z" + } +} +``` + +The sidecar is a separate file (e.g., `photo.sidecar.json`) that accompanies the media file (`photo.jpg`). This keeps the original media untouched. + +## Attestation Methods + +The `method` field in the JWT indicates how the device was verified: + +| Method | Description | +|--------|-------------| +| `sandbox` | No attestation (development/testing only) | +| `app_check` | Firebase App Check verification | +| `app_attest` | Apple App Attest directly (future) | + +Verifiers can check the attestation method to make trust decisions. For example, a news organization might reject `sandbox` captures while accepting `app_check` or `app_attest`. + +## Next Steps + +- [Sidecar Format](/concepts/sidecar-format) — Full JSON schema reference +- [Python Validation](/guides/python-validation) — Verify media programmatically +- [iOS Integration](/guides/ios-integration) — Capture signed media diff --git a/docs/demo.md b/docs/demo.md deleted file mode 100644 index 0213e94..0000000 --- a/docs/demo.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Demo - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - -## Live Demo - -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - -:::tip Try It Out - -Visit [https://signedshot.io](https://signedshot.io) to try SignedShot for yourself! - -::: - -## Example Screenshots - -### Basic Usage - -Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. - -:::note -Screenshots will be added here once available. -::: - -### Advanced Features - -Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. - -:::note -Advanced feature screenshots coming soon. -::: - -## Video Walkthrough - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. - -:::info Coming Soon - -Video demonstration will be available soon! - -::: - -## Interactive Examples - -### Example 1: Simple Screenshot - -```bash -# Take a screenshot -signedshot capture --region -``` - -### Example 2: Secure Share - -```bash -# Share with expiration -signedshot share screenshot.png --expires 24h -``` - -## Use Cases - -- **Documentation**: Create secure documentation with screenshots -- **Bug Reports**: Share sensitive error information safely -- **Design Reviews**: Collaborate on UI/UX designs securely -- **Customer Support**: Help customers while protecting their privacy - -At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident. \ No newline at end of file diff --git a/docs/guides/ios-integration.md b/docs/guides/ios-integration.md new file mode 100644 index 0000000..d4bbcef --- /dev/null +++ b/docs/guides/ios-integration.md @@ -0,0 +1,337 @@ +--- +sidebar_position: 2 +--- + +# iOS Integration + +Integrate SignedShot into your iOS app to capture media with cryptographic proof of authenticity. + +## Requirements + +- iOS 16.0+ +- Xcode 15.0+ +- Swift 5.9+ +- Device with Secure Enclave (iPhone 5s or later) + +The SDK uses the Secure Enclave for hardware-backed key storage. Simulator builds work but use software keys. + +## Installation + +Add the SDK via Swift Package Manager. + +### In Xcode + +**File → Add Package Dependencies** → Enter: + +``` +https://github.com/SignedShot/signedshot-ios.git +``` + +### In Package.swift + +```swift +dependencies: [ + .package(url: "https://github.com/SignedShot/signedshot-ios.git", from: "0.1.0") +] +``` + +## Configuration + +Initialize the client with your publisher ID: + +```swift +import SignedShotSDK + +let config = SignedShotConfiguration( + baseURL: URL(string: "https://api.signedshot.io")!, + publisherId: "your-publisher-id" +) + +let client = SignedShotClient(configuration: config) +``` + +Or using the string convenience initializer: + +```swift +let config = SignedShotConfiguration( + baseURLString: "https://api.signedshot.io", + publisherId: "your-publisher-id" +)! + +let client = SignedShotClient(configuration: config) +``` + +## Device Registration + +Register the device once (credentials are stored in Keychain): + +```swift +// Check if already registered +if !client.isDeviceRegistered { + do { + let response = try await client.registerDevice() + print("Device registered: \(response.deviceId)") + } catch { + print("Registration failed: \(error)") + } +} + +// Access stored device ID +if let deviceId = client.deviceId { + print("Using device: \(deviceId)") +} +``` + +Registration creates a device identity on the SignedShot backend and stores the device token securely in Keychain. + +## Capture Workflow + +The capture flow has four steps: + +1. **Create session** — Get a capture ID and nonce from the backend +2. **Capture media** — Take the photo or video (your camera code) +3. **Generate integrity** — Hash and sign the media with Secure Enclave +4. **Exchange trust token** — Swap nonce for a signed JWT +5. **Generate sidecar** — Combine trust token and integrity proof + +### 1. Create Session + +```swift +let session = try await client.createCaptureSession() +// session.captureId - UUID for this capture +// session.nonce - Cryptographic nonce (use once) +// session.expiresAt - Session expiration time +``` + +### 2. Capture Media + +Use your existing camera implementation. The SDK doesn't handle camera capture—it only handles signing. + +```swift +// Your camera code produces media data (JPEG, HEIC, etc.) +let mediaData: Data = captureMedia() +let capturedAt = Date() +``` + +### 3. Generate Media Integrity + +Sign the media with the device's Secure Enclave: + +```swift +let enclaveService = SecureEnclaveService() +let integrityService = MediaIntegrityService(enclaveService: enclaveService) + +let integrity = try integrityService.generateIntegrity( + for: jpegData, + captureId: session.captureId, + capturedAt: capturedAt +) +``` + +This produces a `MediaIntegrity` object containing: +- `contentHash` — SHA-256 of the media (hex) +- `signature` — ECDSA signature from Secure Enclave (base64) +- `publicKey` — Device's public key (base64) +- `captureId` — Matches the session +- `capturedAt` — ISO8601 timestamp + +### 4. Exchange Trust Token + +Swap the nonce for a signed JWT: + +```swift +let trustResponse = try await client.exchangeTrustToken(nonce: session.nonce) +let jwt = trustResponse.trustToken +``` + +The JWT contains claims about the publisher, device, and attestation method, signed by the SignedShot API. + +### 5. Generate Sidecar + +Combine everything into a sidecar file: + +```swift +let generator = SidecarGenerator() +let sidecarData = try generator.generate( + jwt: jwt, + mediaIntegrity: integrity +) +``` + +### 6. Save Files + +Save the media and sidecar together: + +```swift +let photoURL = documentsDirectory.appendingPathComponent("photo.jpg") +let sidecarURL = documentsDirectory.appendingPathComponent("photo.sidecar.json") + +try jpegData.write(to: photoURL) +try sidecarData.write(to: sidecarURL) +``` + +## Complete Example + +```swift +import SignedShotSDK + +class CaptureManager { + private let client: SignedShotClient + private let integrityService: MediaIntegrityService + + init(publisherId: String) { + let config = SignedShotConfiguration( + baseURLString: "https://api.signedshot.io", + publisherId: publisherId + )! + + self.client = SignedShotClient(configuration: config) + self.integrityService = MediaIntegrityService() + } + + func captureSignedPhoto(jpegData: Data) async throws -> (photo: Data, sidecar: Data) { + // Ensure device is registered + if !client.isDeviceRegistered { + try await client.registerDevice() + } + + // Create capture session + let session = try await client.createCaptureSession() + let capturedAt = Date() + + // Generate media integrity (Secure Enclave signs the hash) + let integrity = try integrityService.generateIntegrity( + for: jpegData, + captureId: session.captureId, + capturedAt: capturedAt + ) + + // Exchange nonce for trust token + let trustResponse = try await client.exchangeTrustToken(nonce: session.nonce) + + // Generate sidecar + let generator = SidecarGenerator() + let sidecarData = try generator.generate( + jwt: trustResponse.trustToken, + mediaIntegrity: integrity + ) + + return (jpegData, sidecarData) + } +} +``` + +## Firebase App Check + +For production apps, enable device attestation with Firebase App Check. This proves the app is running on a genuine device. + +### 1. Set Up Firebase + +Add Firebase to your project and enable App Check in the Firebase Console. + +### 2. Configure Provider + +```swift +import FirebaseCore +import FirebaseAppCheck + +class AppDelegate: NSObject, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Set up App Check before Firebase.configure() + let providerFactory = SignedShotAppCheckProviderFactory() + AppCheck.setAppCheckProviderFactory(providerFactory) + + FirebaseApp.configure() + return true + } +} + +class SignedShotAppCheckProviderFactory: NSObject, AppCheckProviderFactory { + func createProvider(with app: FirebaseApp) -> AppCheckProvider? { + #if targetEnvironment(simulator) + return AppCheckDebugProvider(app: app) + #else + return AppAttestProvider(app: app) + #endif + } +} +``` + +### 3. Register with Attestation + +```swift +import FirebaseAppCheck + +// Get App Check token +let appCheckToken = try await AppCheck.appCheck().token(forcingRefresh: false) + +// Register device with attestation +let response = try await client.registerDevice(attestationToken: appCheckToken.token) +``` + +### 4. Configure Publisher (Backend) + +Your publisher must be configured for attestation on the backend: + +```bash +curl -X PATCH https://api.signedshot.io/publishers/YOUR_PUBLISHER_ID \ + -H "Content-Type: application/json" \ + -d '{ + "sandbox": false, + "attestation_provider": "firebase_app_check", + "attestation_bundle_id": "com.yourcompany.yourapp" + }' +``` + +## Error Handling + +The SDK throws `SignedShotAPIError` for API failures: + +```swift +do { + let session = try await client.createCaptureSession() +} catch SignedShotAPIError.deviceNotRegistered { + // Need to register first + try await client.registerDevice() +} catch SignedShotAPIError.unauthorized { + // Token expired or invalid + try client.clearStoredCredentials() + try await client.registerDevice() +} catch SignedShotAPIError.sessionExpired { + // Nonce was already used or expired + // Create a new session +} catch SignedShotAPIError.networkError(let error) { + // Network issue + print("Network error: \(error)") +} catch SignedShotAPIError.httpError(let statusCode, let message) { + // Other HTTP error + print("HTTP \(statusCode): \(message ?? "unknown")") +} +``` + +Common errors: + +| Error | Cause | Solution | +|-------|-------|----------| +| `deviceNotRegistered` | No device credentials | Call `registerDevice()` | +| `unauthorized` | Invalid or expired token | Clear credentials and re-register | +| `sessionExpired` | Nonce already used | Create new session | +| `invalidNonce` | Nonce format invalid | Use nonce from `createCaptureSession()` | +| `invalidPublisherId` | Publisher ID not found | Check configuration | + +## Security Notes + +- **Private keys never leave the device** — Generated and stored in Secure Enclave +- **Keys are hardware-bound** — Cannot be extracted or copied +- **Content hashed before any disk write** — Sign immediately after capture +- **Each capture session is single-use** — Nonces prevent replay attacks + +## Next Steps + +- [Quick Start](/guides/quick-start) — Verify media with Python +- [Python Validation](/guides/python-validation) — Advanced validation scenarios +- [Two-Layer Trust](/concepts/two-layer-trust) — Understand the trust model +- [Sidecar Format](/concepts/sidecar-format) — Proof structure reference diff --git a/docs/guides/python-validation.md b/docs/guides/python-validation.md new file mode 100644 index 0000000..459de64 --- /dev/null +++ b/docs/guides/python-validation.md @@ -0,0 +1,298 @@ +--- +sidebar_position: 3 +--- + +# Python Validation + +This guide covers advanced validation scenarios using the SignedShot Python library. + +## Installation + +```bash +pip install signedshot +``` + +Requires Python 3.12+. The library is a compiled Rust extension. + +## Validation Methods + +### From Files + +The simplest approach for local files: + +```python +import signedshot + +result = signedshot.validate_files("photo.sidecar.json", "photo.jpg") +``` + +### From Bytes + +For in-memory data (web uploads, API services): + +```python +with open("photo.sidecar.json") as f: + sidecar_json = f.read() +with open("photo.jpg", "rb") as f: + media_bytes = f.read() + +result = signedshot.validate(sidecar_json, media_bytes) +``` + +### With Pre-loaded JWKS + +By default, the library fetches JWKS from the issuer's `/.well-known/jwks.json` endpoint. For high-throughput services, cache the JWKS to avoid HTTP calls: + +```python +import requests +import signedshot + +# Fetch once and cache (refresh periodically) +jwks = requests.get("https://api.signedshot.io/.well-known/jwks.json").text + +# Validate without HTTP call +result = signedshot.validate_with_jwks(sidecar_json, media_bytes, jwks) +``` + +This is recommended for API services that validate many captures. + +## Understanding Results + +### Overall Status + +```python +result.valid # True if all checks pass +result.version # Sidecar format version (e.g., "1.0") +result.error # Error message if validation failed, None otherwise +``` + +### Capture Trust + +The server-issued JWT verification: + +```python +trust = result.capture_trust + +trust["signature_valid"] # JWT signature verified against JWKS +trust["issuer"] # API that issued the token +trust["publisher_id"] # Publisher UUID +trust["device_id"] # Device UUID +trust["capture_id"] # Capture session UUID +trust["method"] # Attestation: "sandbox", "app_check", or "app_attest" +trust["app_id"] # App bundle ID (present when attested) +trust["issued_at"] # Unix timestamp when JWT was issued +trust["key_id"] # JWKS key ID used for signing +``` + +### Media Integrity + +The device-generated proof verification: + +```python +integrity = result.media_integrity + +integrity["content_hash_valid"] # SHA-256 of file matches sidecar +integrity["signature_valid"] # ECDSA signature verified +integrity["capture_id_match"] # Capture ID matches JWT +integrity["content_hash"] # SHA-256 hash (hex, 64 chars) +integrity["capture_id"] # Capture session UUID +integrity["captured_at"] # ISO8601 timestamp +``` + +### Validation Logic + +Media is valid when all these conditions are true: + +1. `capture_trust["signature_valid"]` — JWT signature verifies +2. `media_integrity["content_hash_valid"]` — File hash matches +3. `media_integrity["signature_valid"]` — Device signature verifies +4. `media_integrity["capture_id_match"]` — Capture IDs match + +If any check fails, `result.valid` is `False` and `result.error` describes the failure. + +## Error Handling + +```python +import signedshot + +try: + result = signedshot.validate_files("photo.sidecar.json", "photo.jpg") +except FileNotFoundError as e: + print(f"File not found: {e}") +except ValueError as e: + print(f"Parse error: {e}") +else: + if result.valid: + print("Media is authentic") + else: + print(f"Validation failed: {result.error}") +``` + +Common error scenarios: + +| Error | Cause | +|-------|-------| +| `FileNotFoundError` | Sidecar or media file doesn't exist | +| `ValueError` | Malformed JSON or invalid sidecar structure | +| `result.error = "Content hash mismatch"` | File was modified after capture | +| `result.error = "Signature verification failed"` | Invalid device signature | +| `result.error = "JWT verification failed"` | Invalid or expired JWT | + +## Output Formats + +Export results as dictionary or JSON: + +```python +# Dictionary (for further processing) +data = result.to_dict() + +# JSON string (compact) +json_str = result.to_json() + +# JSON string (formatted) +json_pretty = result.to_json_pretty() +``` + +Example JSON output: + +```json +{ + "valid": true, + "version": "1.0", + "error": null, + "capture_trust": { + "signature_valid": true, + "issuer": "https://api.signedshot.io", + "publisher_id": "9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c", + "device_id": "ea5c9bfe-6bbc-4ee2-b82d-0bcfcc185ef1", + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "method": "app_check", + "app_id": "io.signedshot.capture", + "issued_at": 1705312200 + }, + "media_integrity": { + "content_hash_valid": true, + "signature_valid": true, + "capture_id_match": true, + "content_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "captured_at": "2025-01-15T10:30:00Z" + } +} +``` + +## Web Service Integration + +### FastAPI Example + +```python +from fastapi import FastAPI, UploadFile, HTTPException +import signedshot +import requests + +app = FastAPI() + +# Cache JWKS at startup +JWKS = requests.get("https://api.signedshot.io/.well-known/jwks.json").text + +@app.post("/validate") +async def validate_photo(media: UploadFile, sidecar: UploadFile): + media_bytes = await media.read() + sidecar_json = (await sidecar.read()).decode("utf-8") + + try: + result = signedshot.validate_with_jwks(sidecar_json, media_bytes, JWKS) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + if not result.valid: + raise HTTPException(status_code=422, detail=result.error) + + return { + "valid": True, + "publisher_id": result.capture_trust["publisher_id"], + "captured_at": result.media_integrity["captured_at"], + "attestation": result.capture_trust["method"], + } +``` + +### Flask Example + +```python +from flask import Flask, request, jsonify +import signedshot +import requests + +app = Flask(__name__) + +# Cache JWKS at startup +JWKS = requests.get("https://api.signedshot.io/.well-known/jwks.json").text + +@app.post("/validate") +def validate_photo(): + media = request.files["media"].read() + sidecar = request.files["sidecar"].read().decode("utf-8") + + try: + result = signedshot.validate_with_jwks(sidecar, media, JWKS) + except ValueError as e: + return jsonify({"error": str(e)}), 400 + + if not result.valid: + return jsonify({"error": result.error}), 422 + + return jsonify({ + "valid": True, + "publisher_id": result.capture_trust["publisher_id"], + "captured_at": result.media_integrity["captured_at"], + "attestation": result.capture_trust["method"], + }) +``` + +## Batch Validation + +For validating multiple files: + +```python +from pathlib import Path +import signedshot + +def validate_directory(directory: str): + results = [] + + for sidecar_path in Path(directory).glob("*.sidecar.json"): + # Derive media path (photo.sidecar.json → photo.jpg or photo.heic) + stem = sidecar_path.stem.replace(".sidecar", "") + media_path = None + + for ext in [".jpg", ".jpeg", ".heic", ".png"]: + candidate = sidecar_path.parent / f"{stem}{ext}" + if candidate.exists(): + media_path = candidate + break + + if not media_path: + results.append({"file": str(sidecar_path), "error": "Media file not found"}) + continue + + result = signedshot.validate_files(str(sidecar_path), str(media_path)) + results.append({ + "file": str(media_path), + "valid": result.valid, + "error": result.error, + "captured_at": result.media_integrity.get("captured_at") if result.valid else None, + }) + + return results + +# Usage +for item in validate_directory("./photos"): + status = "✓" if item["valid"] else "✗" + print(f"{status} {item['file']}") +``` + +## Next Steps + +- [Quick Start](/guides/quick-start) — Basic validation in 5 minutes +- [iOS Integration](/guides/ios-integration) — Capture media with cryptographic proof +- [Sidecar Format](/concepts/sidecar-format) — Understand the proof structure diff --git a/docs/guides/quick-start.md b/docs/guides/quick-start.md new file mode 100644 index 0000000..5ed2ea8 --- /dev/null +++ b/docs/guides/quick-start.md @@ -0,0 +1,125 @@ +--- +sidebar_position: 1 +--- + +# Quick Start + +Get up and running with SignedShot in 5 minutes. This guide shows you how to verify media authenticity using the Python library. + +## Prerequisites + +- Python 3.12+ +- A media file with its sidecar (e.g., `photo.jpg` and `photo.sidecar.json`) + +Don't have test files? Use the [interactive demo](https://signedshot.io/demo) to capture media and download both files. + +## Install the Library + +```bash +pip install signedshot +``` + +## Verify Media + +```python +import signedshot + +# Validate media with its sidecar +result = signedshot.validate_files("photo.sidecar.json", "photo.jpg") + +if result.valid: + print("✓ Media is authentic") +else: + print(f"✗ Validation failed: {result.error}") +``` + +## Understanding the Result + +The validation result contains detailed information about both trust layers: + +```python +# Basic result +print(result.valid) # True/False +print(result.version) # Sidecar format version +print(result.error) # Error message if validation failed + +# Capture trust (server-issued JWT) +trust = result.capture_trust +print(trust["signature_valid"]) # JWT signature verified +print(trust["issuer"]) # API that issued the token +print(trust["publisher_id"]) # Publisher ID +print(trust["device_id"]) # Device ID +print(trust["capture_id"]) # Capture session ID +print(trust["method"]) # Attestation: "sandbox", "app_check", or "app_attest" +print(trust["app_id"]) # App bundle ID (if attested) + +# Media integrity (device signature) +integrity = result.media_integrity +print(integrity["content_hash_valid"]) # SHA-256 hash matches +print(integrity["signature_valid"]) # ECDSA signature verified +print(integrity["capture_id_match"]) # Capture IDs match +print(integrity["content_hash"]) # SHA-256 of media file +print(integrity["captured_at"]) # ISO8601 timestamp +``` + +## Validate from Bytes + +For in-memory validation (useful in web services): + +```python +with open("photo.sidecar.json") as f: + sidecar_json = f.read() +with open("photo.jpg", "rb") as f: + media_bytes = f.read() + +result = signedshot.validate(sidecar_json, media_bytes) +``` + +## CLI Usage + +The library also installs a command-line tool: + +```bash +# Basic validation +signedshot validate photo.sidecar.json photo.jpg + +# Output as JSON (for scripting) +signedshot validate photo.sidecar.json photo.jpg --json +``` + +Example output: + +``` +Validating sidecar: photo.sidecar.json +Media file: photo.jpg +[OK] Sidecar parsed +[OK] JWT decoded + Issuer: https://api.signedshot.io + Publisher: 9a5b1062-a8fe-4871-bdc1-fe54e96cbf1c + Device: ea5c9bfe-6bbc-4ee2-b82d-0bcfcc185ef1 + Capture: ac85dbd2-d8a8-4d0b-9e39-2feef5f7b19f + Method: app_check + App ID: io.signedshot.capture +[OK] JWT signature verified +[OK] Content hash matches +[OK] Media signature verified +[OK] Capture IDs match + +✓ VALID - Media authenticity verified +``` + +## What Gets Validated + +The validator performs these checks: + +1. **Capture Trust (JWT)** — Fetches JWKS from the issuer and verifies the ES256 signature +2. **Media Integrity** — Computes SHA-256 of the file and verifies the ECDSA signature +3. **Cross-Validation** — Confirms capture IDs match between JWT and media integrity + +If any check fails, `result.valid` is `False` and `result.error` describes the issue. + +## Next Steps + +- [iOS Integration](/guides/ios-integration) — Capture media with cryptographic proof +- [Python Validation](/guides/python-validation) — Advanced validation scenarios +- [Sidecar Format](/concepts/sidecar-format) — Understand the proof structure diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 428b609..d63754c 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -2,36 +2,294 @@ sidebar_position: 2 --- -# How it Works +# How It Works -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +A visual overview of the SignedShot capture and verification flow. -## Step 1: Capture +## The Big Picture -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +
-### Features +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ CAPTURE SIDE │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Device │ │ Start │ │ Capture │ │ Sign │ │ +│ │ Register │ ──── │ Session │ ──── │ Media │ ──── │ & Save │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ Get device Get nonce Take photo Sign with SE │ +│ token (once) + capture_id or video Get JWT │ +│ Save sidecar │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + │ media + sidecar + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ VERIFICATION SIDE │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Parse │ │ Verify │ │ Verify │ │ Cross │ │ +│ │ Sidecar │ ──── │ JWT │ ──── │ Hash │ ──── │ Validate │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ Read JSON Check sig Compare hash Match IDs │ +│ structure via JWKS + verify sig JWT ↔ media │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` -- Lorem ipsum dolor sit amet -- Consectetur adipiscing elit -- Sed do eiusmod tempor incididunt -- Ut labore et dolore magna aliqua +
-## Step 2: Sign +--- + +## Capture Flow + +### Step 1: Register Device (Once) + +
+ +``` +┌─────────────┐ ┌─────────────┐ +│ │ POST /devices │ │ +│ iOS App │ ─────────────────────────► │ Server │ +│ │ X-Publisher-ID │ │ +│ │ X-Attestation-Token │ │ +│ │ │ │ +│ │ ◄───────────────────────── │ │ +│ │ device_token │ │ +└─────────────┘ └─────────────┘ +``` + +
+ +- App sends publisher ID and attestation token +- Server verifies device via Firebase App Check +- Server returns `device_token` (store securely, use for all future requests) +- **This only happens once per device** + +--- + +### Step 2: Start Capture Session + +
+ +``` +┌─────────────┐ ┌─────────────┐ +│ │ POST /capture/session │ │ +│ iOS App │ ─────────────────────────► │ Server │ +│ │ Authorization: Bearer │ │ +│ │ │ │ +│ │ ◄───────────────────────── │ │ +│ │ capture_id, nonce │ │ +└─────────────┘ └─────────────┘ +``` + +
+ +- App requests a new capture session +- Server returns `capture_id` (unique ID for this capture) and `nonce` (one-time token) +- Session expires after a short window + +--- + +### Step 3: Capture Media + +
+ +``` +┌─────────────────────────────────────────────────────────┐ +│ │ +│ 📷 CAPTURE │ +│ │ +│ User takes photo or video using the app's camera │ +│ │ +│ The raw bytes are immediately available for signing │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +
+ +- User captures media through the app +- Media bytes are available before any disk write + +--- + +### Step 4: Sign with Secure Enclave + +
+ +``` +┌─────────────────────────────────────────────────────────┐ +│ SECURE ENCLAVE │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ media bytes ──► SHA-256 ──► hash │ │ +│ │ │ │ +│ │ hash + capture_id + timestamp ──► ECDSA sign │ │ +│ │ │ │ +│ │ Result: signature + public_key │ │ +│ │ │ │ +│ └───────────────────────────────────────────────────┘ │ +│ │ +│ Private key NEVER leaves the Secure Enclave │ +└─────────────────────────────────────────────────────────┘ +``` + +
+ +- Compute SHA-256 hash of media bytes +- Sign `{hash}:{capture_id}:{timestamp}` with Secure Enclave +- Private key never leaves secure hardware -Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. +--- + +### Step 5: Exchange Nonce for JWT + +
+ +``` +┌─────────────┐ ┌─────────────┐ +│ │ POST /capture/trust │ │ +│ iOS App │ ─────────────────────────► │ Server │ +│ │ { nonce } │ │ +│ │ │ │ +│ │ ◄───────────────────────── │ │ +│ │ trust_token (JWT) │ │ +└─────────────┘ └─────────────┘ +``` + +
+ +- App sends the nonce received in Step 2 +- Server validates nonce (one-time use) and issues signed JWT +- JWT contains: publisher_id, device_id, capture_id, attestation method + +--- + +### Step 6: Save Media + Sidecar + +
+ +``` +┌─────────────────────────────────────────────────────────┐ +│ │ +│ photo.jpg photo.sidecar.json │ +│ ┌─────────────┐ ┌─────────────────────────┐ │ +│ │ │ │ { │ │ +│ │ [image │ │ "version": "1.0", │ │ +│ │ bytes] │ │ "capture_trust": { │ │ +│ │ │ │ "jwt": "eyJ..." │ │ +│ │ │ │ }, │ │ +│ │ │ │ "media_integrity": { │ │ +│ │ │ │ "content_hash":..., │ │ +│ └─────────────┘ │ "signature": ... │ │ +│ │ } │ │ +│ │ } │ │ +│ └─────────────────────────┘ │ +│ │ +│ Both files travel together │ +└─────────────────────────────────────────────────────────┘ +``` + +
+ +- Save original media file (unchanged) +- Save sidecar JSON with both trust layers +- Files can be stored, shared, or uploaded together + +--- + +## Verification Flow + +### Anyone Can Verify + +
+ +``` +┌─────────────────────────────────────────────────────────┐ +│ │ +│ INPUT: media file + sidecar.json │ +│ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ 1. Parse sidecar JSON │ │ +│ │ ↓ │ │ +│ │ 2. Verify JWT signature (fetch JWKS) │ │ +│ │ ↓ │ │ +│ │ 3. Compute SHA-256 of media │ │ +│ │ ↓ │ │ +│ │ 4. Compare with content_hash │ │ +│ │ ↓ │ │ +│ │ 5. Verify ECDSA signature │ │ +│ │ ↓ │ │ +│ │ 6. Confirm capture_id matches │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ +│ OUTPUT: ✓ VALID or ✗ INVALID + reason │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +
+ +--- + +## What Each Layer Proves -## Step 3: Share +
-Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet. +``` +┌─────────────────────────────────────────────────────────┐ +│ │ +│ CAPTURE TRUST (JWT) MEDIA INTEGRITY │ +│ ───────────────── ──────────────── │ +│ │ +│ ✓ Verified device ✓ Content unchanged │ +│ ✓ Authorized app ✓ Signed by device │ +│ ✓ Valid session ✓ Timestamp bound │ +│ ✓ Attestation method ✓ Capture ID linked │ +│ │ +│ ╲ ╱ │ +│ ╲ ╱ │ +│ ╲ ╱ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────┐ │ +│ │ │ │ +│ │ "This exact content was │ │ +│ │ captured on a verified │ │ +│ │ device and hasn't been │ │ +│ │ modified since." │ │ +│ │ │ │ +│ └─────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` -### Security Features +
+ +--- -- End-to-end encryption -- Digital signatures -- Access tracking -- Expiration controls +## Quick Reference + +| Step | Action | Result | +|------|--------|--------| +| 1 | Register device | `device_token` (one-time) | +| 2 | Start session | `capture_id` + `nonce` | +| 3 | Capture media | Raw bytes | +| 4 | Sign in Secure Enclave | `signature` + `public_key` | +| 5 | Exchange nonce | `trust_token` (JWT) | +| 6 | Save files | `media` + `sidecar.json` | +| 7 | Verify | ✓ or ✗ | + +--- -## Technical Details +## Next Steps -At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident. \ No newline at end of file +- [Two-Layer Trust](/concepts/two-layer-trust) — Deep dive into the trust model +- [Quick Start](/guides/quick-start) — Verify media in 5 minutes +- [iOS Integration](/guides/ios-integration) — Capture signed media diff --git a/docs/intro.md b/docs/intro.md index 854a104..4c47ca9 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -3,28 +3,93 @@ sidebar_position: 1 slug: / --- -# SignedShot — Cryptographic proof at capture +# SignedShot -**SignedShot** is an open-source protocol that provides cryptographic proof of media authenticity at the moment of capture. The goal is to embed verifiable signatures into photos and videos, so technical professionals can cryptographically verify they're unaltered. +**Signed at capture. Verified anywhere.** + +SignedShot is an open protocol for proving photos and videos haven't been altered since capture—cryptographically, not by guessing. ## The Problem -Today, anyone can edit photos or videos in seconds — making it impossible to know what's authentic. Digital media is trivially manipulated; cryptographic verification is needed. +Digital manipulation is easy and getting easier. Anyone can alter an image in seconds, and there's no built-in way to prove otherwise. + +This leads to an erosion of trust in visual media—and no way to prove "I didn't edit this." + +SignedShot solves this by embedding cryptographic proof at the moment of capture. + +## A Different Approach + +SignedShot doesn't detect fakes. It proves authenticity at the moment of capture. + +> "Detection is an arms race. SignedShot focuses on authenticity at capture: cryptographic proof of where content came from, at the moment it was created." + +The result: **"This device captured this content at this time."** + +Anyone can verify the proof independently. Open source, no vendor lock-in. + +## How It Works: Two Layers + +SignedShot uses two complementary layers of cryptographic proof: + +| Layer | What it proves | How | +|-------|---------------|-----| +| **Media Integrity** | Content hasn't been modified | Device signs the content hash using Secure Enclave (P-256) | +| **Capture Trust** | A legitimate device captured it | Server verifies the device before capture via attestation | + +Together they prove: *"This exact content was captured on a verified device, in an authorized session, and hasn't been modified since."* -## Our Solution +## Detection vs Provenance -SignedShot solves this by using **Secure Enclave**, **cryptographic signatures**, and **device-based attestation** to embed mathematically verifiable proof of authenticity into every photo or video you capture. +| Detection (others) | Provenance (SignedShot) | +|-------------------|------------------------| +| Analyzes after the fact | Proves at capture time | +| Statistical guessing | Cryptographic certainty | +| Arms race with AI | Doesn't matter how good AI gets | +| Requires expert analysis | Anyone can verify | -Whether you're a security engineer, cryptographer, or developer building trust systems, SignedShot provides the cryptographic foundations to **verify what you capture**. +## What You Can Do -## Core Features +### Capture signed media +Use the iOS SDK to capture media with embedded cryptographic proof. -- **Direct proof at capture**: Authenticity is cryptographically embedded at recording time, not retroactively guessed -- **Open-source protocol**: Anyone can inspect, verify, or contribute — transparency first -- **Two-layer model**: Clear separation of media integrity and capture trust, making the protocol flexible and evolvable -- **Secure Enclave P-256 signing**: Hardware-backed cryptographic signatures +```swift +let session = try await signedShot.startSession() +let sidecar = try await signedShot.createSidecar(imageData: jpegData, session: session) +``` -Ready to learn more? Check out our documentation: +### Verify media + +Use the Python library to verify any SignedShot capture. + +```bash +pip install signedshot +``` + +```python +import signedshot +result = signedshot.validate_files("photo.sidecar.json", "photo.jpg") +print(result.valid) # True or False +``` + +### Integrate with your app +Use the API directly to build custom integrations. + +## Get Started + +- [Two-Layer Trust Model](/concepts/two-layer-trust) — Understand how SignedShot works +- [Quick Start](/guides/quick-start) — Get up and running in 5 minutes +- [iOS Integration](/guides/ios-integration) — Capture signed media +- [Python Validation](/guides/python-validation) — Verify media programmatically +- [API Reference](/api-reference/overview) — Direct API integration + +## Open Source + +SignedShot is fully open source. Inspect, verify, or contribute: + +- [signedshot-api](https://github.com/SignedShot/signedshot-api) — Backend API +- [signedshot-ios](https://github.com/SignedShot/signedshot-ios) — iOS SDK +- [signedshot-validator](https://github.com/SignedShot/signedshot-validator) — Verification CLI + Python library + +--- -- [How it Works](/how-it-works) - Technical implementation details -- [Demo](/demo) - See SignedShot in action \ No newline at end of file +*"I don't believe any single company should be the arbiter of digital truth."* diff --git a/docs/security/limitations.md b/docs/security/limitations.md new file mode 100644 index 0000000..7253cc3 --- /dev/null +++ b/docs/security/limitations.md @@ -0,0 +1,156 @@ +--- +sidebar_position: 2 +--- + +# Limitations + +SignedShot provides strong guarantees about capture authenticity, but it's important to understand what it doesn't protect against. + +## What SignedShot Does NOT Prove + +### Content Truthfulness + +SignedShot proves a device captured specific content — not that the content depicts reality. + +**Example:** Photographing a printed deepfake, a TV screen showing manipulated video, or a staged scene produces a valid SignedShot capture. The capture is authentic; the subject may not be. + +**Implication:** SignedShot is about provenance (where did this come from?), not truth (is this real?). + +### Scene Authenticity + +SignedShot doesn't verify what the camera is pointed at. + +**Example:** An attacker could: +- Display AI-generated content on a monitor and photograph it +- Print a fake document and capture it +- Stage a scene with actors + +All would produce valid captures. + +### Pre-Capture Manipulation + +If content is manipulated before being photographed, SignedShot cannot detect it. + +**Example:** A document could be forged, printed, then photographed with SignedShot. The capture is authentic; the document is not. + +### Identity of Photographer + +SignedShot identifies the device, not the person using it. + +**Example:** Anyone with physical access to a registered device can create valid captures. There's no biometric or identity verification. + +**Implication:** "This device captured this" ≠ "This person captured this" + +## Technical Limitations + +### Compromised Devices + +**Rooted/Jailbroken Devices:** +- Secure Enclave still protects keys on most rooted devices +- However, sophisticated attacks may be possible +- Attestation (`app_check`) may fail on some rooted devices + +**Physical Attacks:** +- Sophisticated hardware attacks on Secure Enclave are theoretically possible +- Requires physical access and specialized equipment +- Not practical for most threat scenarios + +### Sandbox Mode + +**`sandbox` mode provides NO security guarantees:** +- No attestation is performed +- Any device can register +- Signatures are still valid, but trust is minimal + +**Use only for:** +- Development and testing +- Demos and proof-of-concept +- Non-critical applications + +### Network Dependency + +SignedShot requires network connectivity for: +- Device registration (once) +- Creating capture sessions +- Exchanging nonces for trust tokens + +**Implication:** Fully offline capture is not currently supported. The media integrity layer (hash + signature) works offline, but the capture trust layer (JWT) requires server communication. + +### Key Loss + +If a device is lost, wiped, or replaced: +- The Secure Enclave keys are lost +- Previous captures remain verifiable (public key is in sidecar) +- New captures require re-registration on the new device + +### Server Dependency + +SignedShot's capture trust layer depends on the API server: +- Server issues JWTs +- Server manages sessions and nonces +- JWKS provides verification keys + +**Implication:** If the server is unavailable, new captures can't get trust tokens. Existing captures remain verifiable as long as JWKS is cached or accessible. + +## Platform Limitations + +### iOS Only (Currently) + +The SDK currently supports iOS only. + +**Roadmap:** Android support is planned for post-launch. + +### Photos Only (Currently) + +Video support is in development. + +**Current state:** The protocol supports video (SHA-256 works on any byte stream), but the iOS SDK currently supports photos only. + +## Comparison to Detection + +SignedShot takes a fundamentally different approach than AI detection: + +| Approach | What it does | Limitations | +|----------|--------------|-------------| +| **AI Detection** | Analyzes content for signs of manipulation | Arms race with generators; false positives/negatives | +| **SignedShot** | Proves content came from a verified source | Doesn't detect pre-capture manipulation | + +**SignedShot's advantage:** No arms race. Cryptographic proofs don't become less secure as AI improves. + +**SignedShot's limitation:** Only works for content captured through the SignedShot flow. Doesn't help with existing content. + +## Responsible Use + +### What Verifiers Should Communicate + +When displaying SignedShot verification results, be clear about what was verified: + +**Good:** +> "This media was captured on a verified device on [date] and has not been modified since." + +**Misleading:** +> "This media is authentic and real." + +### Combine with Other Evidence + +SignedShot is one piece of evidence, not the whole story: + +- Cross-reference with other sources +- Consider context and circumstances +- Apply editorial or investigative judgment +- Use SignedShot as part of a verification workflow + +## Summary + +| SignedShot Proves | SignedShot Does NOT Prove | +|-------------------|---------------------------| +| Content hasn't been modified | Content depicts reality | +| Captured on a specific device | Identity of photographer | +| Captured at a specific time | Scene wasn't staged | +| App passed attestation | Device wasn't compromised | + +## Next Steps + +- [Threat Model](/security/threat-model) — What SignedShot protects against +- [Two-Layer Trust](/concepts/two-layer-trust) — Understand the trust model +- [Quick Start](/guides/quick-start) — Start verifying captures diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md new file mode 100644 index 0000000..8c94975 --- /dev/null +++ b/docs/security/threat-model.md @@ -0,0 +1,191 @@ +--- +sidebar_position: 1 +--- + +# Threat Model + +This document describes what SignedShot protects against, the assumptions it makes, and how the security model works. + +## What SignedShot Proves + +SignedShot provides cryptographic proof that: + +1. **This exact content** was captured (hash verification) +2. **On this device** (Secure Enclave signature) +3. **At this time** (timestamp in signed message) +4. **By a verified app** (device attestation via JWT) + +It establishes a verified chain of custody from capture to verification. + +:::info Key Insight +SignedShot proves "this device captured this content at this time" — not "this content depicts reality." A capture of a printed deepfake is still a valid SignedShot capture. +::: + +## Threats Mitigated + +### Post-Capture Tampering + +**Threat:** An attacker modifies the image after capture (cropping, editing, AI manipulation). + +**Mitigation:** The SHA-256 hash is computed at capture time and signed by the device's Secure Enclave. Any modification — even a single pixel — changes the hash and invalidates the signature. + +**Verification:** Compare `content_hash` in the sidecar with the computed hash of the file. + +### Content Substitution + +**Threat:** An attacker claims a different image was captured during a session. + +**Mitigation:** The `capture_id` links the media integrity signature to a specific capture session. The signed message includes the content hash, so a signature cannot be transferred to different content. + +**Verification:** Confirm `capture_id` matches in both the JWT and media integrity sections. + +### Signature Forgery + +**Threat:** An attacker creates a valid signature without access to the device. + +**Mitigation:** Private keys are generated and stored in the device's Secure Enclave (iOS) or KeyStore (Android). Keys never leave the secure hardware and cannot be exported. + +**Verification:** Verify the ECDSA signature using the public key in the sidecar. + +### Replay Attacks + +**Threat:** An attacker reuses a valid signature or JWT from a previous capture. + +**Mitigation:** Each capture session has a unique `capture_id` and `nonce`. The nonce is single-use and expires after a short window. The server rejects attempts to reuse a nonce. + +**Verification:** The JWT `capture_id` must match the media integrity `capture_id`. + +### Unauthorized Capture Claims + +**Threat:** An attacker claims media was captured through the SignedShot flow when it wasn't. + +**Mitigation:** The JWT is signed by the SignedShot API with ES256. Only the API can issue valid JWTs. Validators fetch the JWKS to verify the signature. + +**Verification:** Verify JWT signature against `/.well-known/jwks.json`. + +### Device Spoofing (with attestation) + +**Threat:** An attacker registers a fake device to obtain capture tokens. + +**Mitigation:** When attestation is enabled (`app_check` or `app_attest` method), the device must pass Firebase App Check verification. This proves the request comes from a genuine app on a real device. + +**Verification:** Check the `method` field in the JWT. Reject `sandbox` for high-trust use cases. + +### Metadata Forgery + +**Threat:** An attacker manipulates EXIF timestamps, GPS coordinates, or other metadata. + +**Mitigation:** SignedShot doesn't rely on EXIF metadata. The `captured_at` timestamp is part of the signed message, making it tamper-evident. The JWT `iat` (issued at) provides server-side timestamp. + +**Verification:** Use `captured_at` from the signed media integrity, not EXIF data. + +## Security Layers + +### Layer 1: Media Integrity + +| Component | Security Property | +|-----------|-------------------| +| SHA-256 hash | Collision-resistant content binding | +| ECDSA P-256 signature | Unforgeable without private key | +| Secure Enclave storage | Hardware-protected key material | +| Signed message format | Binds hash, capture_id, and timestamp | + +### Layer 2: Capture Trust + +| Component | Security Property | +|-----------|-------------------| +| Session nonce | Single-use, prevents replay | +| Device token | Authenticates registered devices | +| Firebase App Check | Verifies genuine app on real device | +| ES256 JWT | Server-signed, publicly verifiable | +| JWKS endpoint | Key rotation, standard verification | + +## Trust Levels + +The `method` field in the JWT indicates the attestation level: + +| Method | Trust Level | Use Case | +|--------|-------------|----------| +| `sandbox` | Low | Development and testing only | +| `app_check` | Medium-High | Production apps with Firebase | +| `app_attest` | High | Future: direct Apple attestation | + +**Recommendation:** Verifiers should reject `sandbox` captures for any use case requiring trust. + +## Cryptographic Specifications + +| Algorithm | Purpose | Standard | +|-----------|---------|----------| +| SHA-256 | Content hashing | FIPS 180-4 | +| ECDSA P-256 | Device signatures | FIPS 186-4, SEC 2 | +| ES256 | JWT signing | RFC 7518 | +| Secure Enclave | Key storage | Apple Platform Security | + +### Signature Format + +The media integrity signature signs the message: + +``` +{content_hash}:{capture_id}:{captured_at} +``` + +Example: +``` +9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08:550e8400-e29b-41d4-a716-446655440000:2025-01-15T10:30:00Z +``` + +The signature uses ECDSA with the P-256 curve, encoded as base64. + +## Assumptions + +SignedShot's security relies on these assumptions: + +1. **Secure Enclave integrity** — The device's secure hardware is not compromised +2. **App integrity** — The SignedShot SDK is running in an unmodified app +3. **Network security** — TLS protects API communication +4. **Server security** — The SignedShot API's signing keys are protected +5. **Time accuracy** — Device and server clocks are reasonably synchronized + +## Attack Scenarios + +### Scenario: Modified App + +**Attack:** An attacker modifies the app to sign arbitrary content. + +**Result:** If attestation is enabled (`app_check`), Firebase App Check will reject the modified app. With `sandbox` mode, this attack succeeds — which is why `sandbox` should not be trusted. + +### Scenario: Rooted Device + +**Attack:** An attacker uses a rooted/jailbroken device to extract keys. + +**Result:** Modern Secure Enclaves resist software-based extraction even on rooted devices. However, attestation may fail on some rooted devices, preventing registration with `app_check` method. + +### Scenario: Screenshot of Deepfake + +**Attack:** An attacker photographs a deepfake displayed on a screen. + +**Result:** SignedShot will produce a valid capture. This is by design — SignedShot proves what was captured, not whether the subject is real. The capture is authentic; the content may not be. + +### Scenario: Compromised Server + +**Attack:** An attacker gains access to the SignedShot API signing keys. + +**Result:** The attacker could issue fraudulent JWTs. Mitigation: key rotation via JWKS, server security hardening, monitoring for anomalous JWT issuance. + +## Verification Checklist + +For maximum security, verify all of the following: + +- [ ] JWT signature valid against JWKS +- [ ] JWT `iss` matches expected issuer +- [ ] JWT `method` is acceptable (not `sandbox` for production) +- [ ] Content hash matches file +- [ ] Media integrity signature valid +- [ ] `capture_id` matches in JWT and media integrity +- [ ] `captured_at` is reasonable (not in future, not too old) + +## Next Steps + +- [Limitations](/security/limitations) — What SignedShot doesn't protect against +- [Two-Layer Trust](/concepts/two-layer-trust) — Understand the trust model +- [Sidecar Format](/concepts/sidecar-format) — Proof structure reference diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 0213450..ce66ce1 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -2,6 +2,9 @@ import {themes as prismThemes} from 'prism-react-renderer'; import type {Config} from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; +// Use '/' for Vercel previews, '/docs/' for production +const baseUrl = process.env.VERCEL_ENV === 'preview' ? '/' : '/docs/'; + const config: Config = { title: 'SignedShot Documentation', tagline: 'Secure screenshot sharing made simple', @@ -13,7 +16,7 @@ const config: Config = { }, url: 'https://signedshot.io', - baseUrl: '/docs/', + baseUrl, // GitHub pages deployment config. organizationName: 'SignedShot', // Usually your GitHub org/user name. @@ -94,12 +97,12 @@ const config: Config = { title: 'Docs', items: [ { - label: 'How it Works', - to: '/how-it-works', + label: 'Getting Started', + to: '/', }, { - label: 'Demo', - to: '/demo', + label: 'Concepts', + to: '/concepts/two-layer-trust', }, ], }, @@ -120,7 +123,15 @@ const config: Config = { href: 'https://signedshot.io', }, { - label: 'GitHub Organization', + label: 'How it Works', + href: 'https://signedshot.io/how-it-works', + }, + { + label: 'Demo', + href: 'https://signedshot.io/demo', + }, + { + label: 'GitHub', href: 'https://github.com/SignedShot', }, ], diff --git a/package-lock.json b/package-lock.json index a1a7958..a07b078 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5492,9 +5492,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz", - "integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==", + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -5528,23 +5528,23 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -5569,12 +5569,41 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bonjour-service": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", @@ -5636,9 +5665,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz", - "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -5655,11 +5684,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.2", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -5801,9 +5830,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001741", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", - "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "funding": [ { "type": "opencollective", @@ -7285,9 +7314,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.218", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", - "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==", + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -7331,13 +7360,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -7383,9 +7412,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "license": "MIT" }, "node_modules/es-object-atoms": { @@ -7711,39 +7740,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -8426,9 +8455,9 @@ } }, "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -9543,9 +9572,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9688,12 +9717,16 @@ "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -9726,9 +9759,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -10180,9 +10213,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -12279,18 +12312,18 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/normalize-path": { @@ -14480,12 +14513,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -14545,15 +14578,15 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -14568,6 +14601,35 @@ "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -15402,9 +15464,9 @@ "license": "Apache-2.0" }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -16246,9 +16308,9 @@ } }, "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "license": "MIT", "engines": { "node": ">=6" @@ -16277,9 +16339,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", @@ -16686,9 +16748,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -16992,9 +17054,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -17024,9 +17086,9 @@ } }, "node_modules/webpack": { - "version": "5.101.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", - "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", @@ -17037,22 +17099,22 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { diff --git a/sidebars.ts b/sidebars.ts index 6bb8290..464dc54 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -4,7 +4,39 @@ const sidebars: SidebarsConfig = { mainSidebar: [ 'intro', 'how-it-works', - 'demo', + { + type: 'category', + label: 'Concepts', + items: [ + 'concepts/two-layer-trust', + 'concepts/sidecar-format', + 'concepts/cryptographic-specs', + ], + }, + { + type: 'category', + label: 'Guides', + items: [ + 'guides/quick-start', + 'guides/ios-integration', + 'guides/python-validation', + ], + }, + { + type: 'category', + label: 'API Reference', + items: [ + 'api-reference/overview', + ], + }, + { + type: 'category', + label: 'Security', + items: [ + 'security/threat-model', + 'security/limitations', + ], + }, ], };