From b396669f17a174ae3f0a48b4de4a75082d606ce5 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 22:20:58 -0300 Subject: [PATCH 01/18] docs: update intro page --- docs/intro.md | 93 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/docs/intro.md b/docs/intro.md index 854a104..9b404ad 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 a photo 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 photos +Use the iOS SDK to capture photos 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 photos + +Use the Python library to verify any SignedShot photo. + +```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 photos +- [Python Validation](/guides/python-validation) — Verify photos 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."* From 96edba622687af05f4881dbfe68322a96c2d4f3e Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 22:28:46 -0300 Subject: [PATCH 02/18] docs: add two-layer trust concept page --- docs/concepts/two-layer-trust.md | 125 +++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 docs/concepts/two-layer-trust.md diff --git a/docs/concepts/two-layer-trust.md b/docs/concepts/two-layer-trust.md new file mode 100644 index 0000000..5e577dc --- /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 a photo 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 taking a photo +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 photos programmatically +- [iOS Integration](/guides/ios-integration) — Capture signed photos From 48bd16e6ef605f545d5f22dc395b3cd14c1f952a Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 22:33:01 -0300 Subject: [PATCH 03/18] docs: remove placeholder pages that exist on marketing site --- docs/demo.md | 70 -------------------------------------------- docs/how-it-works.md | 37 ----------------------- 2 files changed, 107 deletions(-) delete mode 100644 docs/demo.md delete mode 100644 docs/how-it-works.md 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/how-it-works.md b/docs/how-it-works.md deleted file mode 100644 index 428b609..0000000 --- a/docs/how-it-works.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -sidebar_position: 2 ---- - -# 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. - -## Step 1: Capture - -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 - -- Lorem ipsum dolor sit amet -- Consectetur adipiscing elit -- Sed do eiusmod tempor incididunt -- Ut labore et dolore magna aliqua - -## Step 2: Sign - -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 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. - -### Security Features - -- End-to-end encryption -- Digital signatures -- Access tracking -- Expiration controls - -## Technical Details - -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 From 24116656a7a62ba2ea21ff59946d7ecc4cefa6d0 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 22:35:16 -0300 Subject: [PATCH 04/18] docs: add placeholder pages for linked content --- docs/api-reference/overview.md | 36 ++++++++++++++++++++++++ docs/concepts/sidecar-format.md | 29 ++++++++++++++++++++ docs/guides/ios-integration.md | 44 ++++++++++++++++++++++++++++++ docs/guides/python-validation.md | 47 ++++++++++++++++++++++++++++++++ docs/guides/quick-start.md | 28 +++++++++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 docs/api-reference/overview.md create mode 100644 docs/concepts/sidecar-format.md create mode 100644 docs/guides/ios-integration.md create mode 100644 docs/guides/python-validation.md create mode 100644 docs/guides/quick-start.md diff --git a/docs/api-reference/overview.md b/docs/api-reference/overview.md new file mode 100644 index 0000000..4aa3382 --- /dev/null +++ b/docs/api-reference/overview.md @@ -0,0 +1,36 @@ +--- +sidebar_position: 1 +--- + +# API Reference + +:::info Work in Progress +This page is under construction. +::: + +## Base URL + +``` +https://api.signedshot.io +``` + +## Authentication + +Most endpoints require a publisher token or device token in the `Authorization` header: + +``` +Authorization: Bearer +``` + +## Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/publishers` | Register a publisher | +| POST | `/devices` | Register a device | +| POST | `/capture/session` | Start a capture session | +| POST | `/capture/trust` | Exchange nonce for trust token | +| POST | `/validate` | Validate media + sidecar | +| GET | `/.well-known/jwks.json` | Public keys for JWT verification | + +Full endpoint documentation coming soon. diff --git a/docs/concepts/sidecar-format.md b/docs/concepts/sidecar-format.md new file mode 100644 index 0000000..80fc23e --- /dev/null +++ b/docs/concepts/sidecar-format.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 2 +--- + +# Sidecar Format + +:::info Work in Progress +This page is under construction. +::: + +The sidecar file contains both layers of cryptographic proof in a single JSON document. + +```json +{ + "version": "1.0", + "capture_trust": { + "jwt": "..." + }, + "media_integrity": { + "content_hash": "...", + "signature": "...", + "public_key": "...", + "capture_id": "...", + "captured_at": "..." + } +} +``` + +Full schema reference coming soon. diff --git a/docs/guides/ios-integration.md b/docs/guides/ios-integration.md new file mode 100644 index 0000000..79d0bff --- /dev/null +++ b/docs/guides/ios-integration.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 2 +--- + +# iOS Integration + +:::info Work in Progress +This page is under construction. +::: + +Integrate SignedShot into your iOS app to capture signed photos. + +## Installation + +Add the SignedShot SDK via Swift Package Manager: + +``` +https://github.com/SignedShot/signedshot-ios.git +``` + +## Basic Usage + +```swift +import SignedShotSDK + +let signedShot = SignedShotClient( + baseURL: "https://api.signedshot.io", + publisherToken: "your-publisher-token" +) + +// Register device (once) +let device = try await signedShot.registerDevice() + +// Start capture session +let session = try await signedShot.startSession() + +// After capturing photo... +let sidecar = try await signedShot.createSidecar( + imageData: jpegData, + session: session +) +``` + +Full guide coming soon. diff --git a/docs/guides/python-validation.md b/docs/guides/python-validation.md new file mode 100644 index 0000000..d73814e --- /dev/null +++ b/docs/guides/python-validation.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 3 +--- + +# Python Validation + +:::info Work in Progress +This page is under construction. +::: + +Verify SignedShot photos using the Python library. + +## Installation + +```bash +pip install signedshot +``` + +## Basic Usage + +```python +import signedshot + +# Validate from files +result = signedshot.validate_files("photo.sidecar.json", "photo.jpg") + +print(result.valid) # True/False +print(result.capture_trust) # JWT verification details +print(result.media_integrity) # Hash/signature verification details +``` + +## Validation Details + +```python +# Capture trust (JWT verification) +trust = result.capture_trust +print(trust["signature_valid"]) # JWT signature verified +print(trust["issuer"]) # API that issued the token +print(trust["method"]) # Attestation method + +# Media integrity (content verification) +integrity = result.media_integrity +print(integrity["content_hash_valid"]) # SHA-256 hash matches +print(integrity["signature_valid"]) # ECDSA signature verified +``` + +Full guide coming soon. diff --git a/docs/guides/quick-start.md b/docs/guides/quick-start.md new file mode 100644 index 0000000..7c501af --- /dev/null +++ b/docs/guides/quick-start.md @@ -0,0 +1,28 @@ +--- +sidebar_position: 1 +--- + +# Quick Start + +:::info Work in Progress +This page is under construction. +::: + +Get up and running with SignedShot in 5 minutes. + +## Verify a Photo + +```bash +pip install signedshot +``` + +```python +import signedshot + +result = signedshot.validate_files("photo.sidecar.json", "photo.jpg") +print(result.valid) +``` + +## Capture a Photo (iOS) + +See the [iOS Integration](/guides/ios-integration) guide. From 66f89794045aae5aadf99a121978bac3aa659454 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 22:37:28 -0300 Subject: [PATCH 05/18] docs: update sidebar structure and remove deleted pages --- sidebars.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/sidebars.ts b/sidebars.ts index 6bb8290..6638a9b 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -3,8 +3,30 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; const sidebars: SidebarsConfig = { mainSidebar: [ 'intro', - 'how-it-works', - 'demo', + { + type: 'category', + label: 'Concepts', + items: [ + 'concepts/two-layer-trust', + 'concepts/sidecar-format', + ], + }, + { + type: 'category', + label: 'Guides', + items: [ + 'guides/quick-start', + 'guides/ios-integration', + 'guides/python-validation', + ], + }, + { + type: 'category', + label: 'API Reference', + items: [ + 'api-reference/overview', + ], + }, ], }; From cdfc9fc339492f37fe8b8aa34bc29f3c11335ef6 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 22:40:14 -0300 Subject: [PATCH 06/18] docs: fix footer links to removed pages --- docusaurus.config.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 0213450..684b1d0 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -94,12 +94,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 +120,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', }, ], From 008390447865223d481c3d388acb43b6515721f2 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 22:47:53 -0300 Subject: [PATCH 07/18] docs: add sidecar format reference --- DOCS_STATUS.md | 71 +++++++++++++ docs/concepts/sidecar-format.md | 171 ++++++++++++++++++++++++++++++-- 2 files changed, 231 insertions(+), 11 deletions(-) create mode 100644 DOCS_STATUS.md diff --git a/DOCS_STATUS.md b/DOCS_STATUS.md new file mode 100644 index 0000000..c2d911b --- /dev/null +++ b/DOCS_STATUS.md @@ -0,0 +1,71 @@ +# Documentation Status + +This file tracks the progress of SignedShot documentation. + +--- + +## Complete (Real Content) + +| File | Status | +|------|--------| +| `intro.md` | ✅ Done | +| `concepts/two-layer-trust.md` | ✅ Done | + +--- + +## Placeholder (Work in Progress) + +| File | Status | +|------|--------| +| `concepts/sidecar-format.md` | ⏳ Placeholder | +| `guides/quick-start.md` | ⏳ Placeholder | +| `guides/ios-integration.md` | ⏳ Placeholder | +| `guides/python-validation.md` | ⏳ Placeholder | +| `api-reference/overview.md` | ⏳ Placeholder | + +--- + +## Still Missing + +| File | Description | +|------|-------------| +| `concepts/cryptographic-specs.md` | Algorithms, key formats | +| `api-reference/publishers.md` | POST /publishers | +| `api-reference/devices.md` | POST /devices | +| `api-reference/capture-session.md` | POST /capture/session | +| `api-reference/capture-trust.md` | POST /capture/trust | +| `api-reference/validate.md` | POST /validate | +| `api-reference/jwks.md` | GET /.well-known/jwks.json | +| `security/threat-model.md` | What SignedShot protects against | +| `security/limitations.md` | What it doesn't protect against | + +--- + +## Summary + +| Category | Done | Placeholder | Missing | +|----------|------|-------------|---------| +| Root | 1 | 0 | 0 | +| Concepts | 1 | 1 | 1 | +| Guides | 0 | 3 | 0 | +| API Reference | 0 | 1 | 6 | +| Security | 0 | 0 | 2 | +| **Total** | **2** | **5** | **9** | + +--- + +## Suggested Order + +1. `concepts/sidecar-format.md` — Fill in placeholder +2. `guides/quick-start.md` — Fill in placeholder +3. `guides/python-validation.md` — Fill in placeholder +4. `guides/ios-integration.md` — Fill in placeholder +5. `api-reference/overview.md` — Fill in placeholder +6. Remaining API reference pages +7. Security docs + +--- + +## How to Resume + +> "Read `DOCS_STATUS.md` and continue with the next placeholder doc." diff --git a/docs/concepts/sidecar-format.md b/docs/concepts/sidecar-format.md index 80fc23e..b639409 100644 --- a/docs/concepts/sidecar-format.md +++ b/docs/concepts/sidecar-format.md @@ -4,26 +4,175 @@ sidebar_position: 2 # Sidecar Format -:::info Work in Progress -This page is under construction. -::: +The sidecar file contains both layers of cryptographic proof in a single JSON document that travels alongside the media file. -The sidecar file contains both layers of cryptographic proof in a single JSON document. +## 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": "..." + "jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9..." }, "media_integrity": { - "content_hash": "...", - "signature": "...", - "public_key": "...", - "capture_id": "...", - "captured_at": "..." + "content_hash": "a1b2c3d4e5f6...", + "signature": "MEUCIQC...", + "public_key": "BHx5y...", + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "captured_at": "2025-01-15T10:30:00Z" } } ``` -Full schema reference coming soon. +## 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 From 69342afaf79f47ad44fe57a6425846316bfb39c5 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 22:50:05 -0300 Subject: [PATCH 08/18] fix: use dynamic baseUrl for Vercel previews --- docusaurus.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 684b1d0..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. From e3f57db7bb248ef1fcca22d2d094bf4873963a81 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 23:00:48 -0300 Subject: [PATCH 09/18] docs: fill in quick-start guide with real content --- DOCS_STATUS.md | 71 ----------------------- docs/guides/quick-start.md | 113 ++++++++++++++++++++++++++++++++++--- 2 files changed, 105 insertions(+), 79 deletions(-) delete mode 100644 DOCS_STATUS.md diff --git a/DOCS_STATUS.md b/DOCS_STATUS.md deleted file mode 100644 index c2d911b..0000000 --- a/DOCS_STATUS.md +++ /dev/null @@ -1,71 +0,0 @@ -# Documentation Status - -This file tracks the progress of SignedShot documentation. - ---- - -## Complete (Real Content) - -| File | Status | -|------|--------| -| `intro.md` | ✅ Done | -| `concepts/two-layer-trust.md` | ✅ Done | - ---- - -## Placeholder (Work in Progress) - -| File | Status | -|------|--------| -| `concepts/sidecar-format.md` | ⏳ Placeholder | -| `guides/quick-start.md` | ⏳ Placeholder | -| `guides/ios-integration.md` | ⏳ Placeholder | -| `guides/python-validation.md` | ⏳ Placeholder | -| `api-reference/overview.md` | ⏳ Placeholder | - ---- - -## Still Missing - -| File | Description | -|------|-------------| -| `concepts/cryptographic-specs.md` | Algorithms, key formats | -| `api-reference/publishers.md` | POST /publishers | -| `api-reference/devices.md` | POST /devices | -| `api-reference/capture-session.md` | POST /capture/session | -| `api-reference/capture-trust.md` | POST /capture/trust | -| `api-reference/validate.md` | POST /validate | -| `api-reference/jwks.md` | GET /.well-known/jwks.json | -| `security/threat-model.md` | What SignedShot protects against | -| `security/limitations.md` | What it doesn't protect against | - ---- - -## Summary - -| Category | Done | Placeholder | Missing | -|----------|------|-------------|---------| -| Root | 1 | 0 | 0 | -| Concepts | 1 | 1 | 1 | -| Guides | 0 | 3 | 0 | -| API Reference | 0 | 1 | 6 | -| Security | 0 | 0 | 2 | -| **Total** | **2** | **5** | **9** | - ---- - -## Suggested Order - -1. `concepts/sidecar-format.md` — Fill in placeholder -2. `guides/quick-start.md` — Fill in placeholder -3. `guides/python-validation.md` — Fill in placeholder -4. `guides/ios-integration.md` — Fill in placeholder -5. `api-reference/overview.md` — Fill in placeholder -6. Remaining API reference pages -7. Security docs - ---- - -## How to Resume - -> "Read `DOCS_STATUS.md` and continue with the next placeholder doc." diff --git a/docs/guides/quick-start.md b/docs/guides/quick-start.md index 7c501af..992f2f9 100644 --- a/docs/guides/quick-start.md +++ b/docs/guides/quick-start.md @@ -4,25 +4,122 @@ sidebar_position: 1 # Quick Start -:::info Work in Progress -This page is under construction. -::: +Get up and running with SignedShot in 5 minutes. This guide shows you how to verify a photo's authenticity using the Python library. -Get up and running with SignedShot in 5 minutes. +## Prerequisites -## Verify a Photo +- Python 3.12+ +- A photo with its sidecar file (e.g., `photo.jpg` and `photo.sidecar.json`) + +Don't have test files? Use the [interactive demo](https://signedshot.io/demo) to capture a photo and download both files. + +## Install the Library ```bash pip install signedshot ``` +## Verify a Photo + ```python import signedshot +# Validate a photo with its sidecar result = signedshot.validate_files("photo.sidecar.json", "photo.jpg") -print(result.valid) + +if result.valid: + print("✓ Photo 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) ``` -## Capture a Photo (iOS) +## 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 -See the [iOS Integration](/guides/ios-integration) guide. +- [iOS Integration](/guides/ios-integration) — Capture photos with cryptographic proof +- [Python Validation](/guides/python-validation) — Advanced validation scenarios +- [Sidecar Format](/concepts/sidecar-format) — Understand the proof structure From 7d92aee999e2c4eab3f7edfc6a03783c68cdef3e Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 23:04:34 -0300 Subject: [PATCH 10/18] docs: fill in python-validation guide with real content --- docs/guides/python-validation.md | 289 +++++++++++++++++++++++++++++-- 1 file changed, 270 insertions(+), 19 deletions(-) diff --git a/docs/guides/python-validation.md b/docs/guides/python-validation.md index d73814e..2ded4ba 100644 --- a/docs/guides/python-validation.md +++ b/docs/guides/python-validation.md @@ -4,11 +4,7 @@ sidebar_position: 3 # Python Validation -:::info Work in Progress -This page is under construction. -::: - -Verify SignedShot photos using the Python library. +This guide covers advanced validation scenarios using the SignedShot Python library. ## Installation @@ -16,32 +12,287 @@ Verify SignedShot photos using the Python library. pip install signedshot ``` -## Basic Usage +Requires Python 3.12+. The library is a compiled Rust extension for maximum performance. + +## Validation Methods + +### From Files + +The simplest approach for local files: ```python import signedshot -# Validate from files result = signedshot.validate_files("photo.sidecar.json", "photo.jpg") +``` + +### From Bytes -print(result.valid) # True/False -print(result.capture_trust) # JWT verification details -print(result.media_integrity) # Hash/signature verification details +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) ``` -## Validation Details +### 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 photos. + +## 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 -# Capture trust (JWT verification) trust = result.capture_trust -print(trust["signature_valid"]) # JWT signature verified -print(trust["issuer"]) # API that issued the token -print(trust["method"]) # Attestation method -# Media integrity (content verification) +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 -print(integrity["content_hash_valid"]) # SHA-256 hash matches -print(integrity["signature_valid"]) # ECDSA signature verified + +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 + +A photo 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("Photo is authentic") + else: + print(f"Validation failed: {result.error}") ``` -Full guide coming soon. +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 photos with cryptographic proof +- [Sidecar Format](/concepts/sidecar-format) — Understand the proof structure From a73c979b663cd7c95a4cb3bfb53bb594b2390e0e Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Thu, 5 Feb 2026 23:11:10 -0300 Subject: [PATCH 11/18] docs: fill in ios-integration guide with real content --- docs/guides/ios-integration.md | 329 +++++++++++++++++++++++++++++-- docs/guides/python-validation.md | 2 +- 2 files changed, 312 insertions(+), 19 deletions(-) diff --git a/docs/guides/ios-integration.md b/docs/guides/ios-integration.md index 79d0bff..419263b 100644 --- a/docs/guides/ios-integration.md +++ b/docs/guides/ios-integration.md @@ -4,41 +4,334 @@ sidebar_position: 2 # iOS Integration -:::info Work in Progress -This page is under construction. -::: +Integrate SignedShot into your iOS app to capture photos with cryptographic proof of authenticity. -Integrate SignedShot into your iOS app to capture signed photos. +## 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 SignedShot SDK via Swift Package Manager: +Add the SDK via Swift Package Manager. + +### In Xcode + +**File → Add Package Dependencies** → Enter: ``` https://github.com/SignedShot/signedshot-ios.git ``` -## Basic Usage +### 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 signedShot = SignedShotClient( - baseURL: "https://api.signedshot.io", - publisherToken: "your-publisher-token" +let config = SignedShotConfiguration( + baseURL: URL(string: "https://api.signedshot.io")!, + publisherId: "your-publisher-id" ) -// Register device (once) -let device = try await signedShot.registerDevice() +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): -// Start capture session -let session = try await signedShot.startSession() +```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)") +} +``` -// After capturing photo... -let sidecar = try await signedShot.createSidecar( - imageData: jpegData, - session: session +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 photo** — Take the photo (your camera code) +3. **Generate integrity** — Hash and sign the photo 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 Photo + +Use your existing camera implementation. The SDK doesn't handle camera capture—it only handles signing. + +```swift +// Your camera code produces JPEG data +let jpegData: Data = capturePhoto() +let capturedAt = Date() +``` + +### 3. Generate Media Integrity + +Sign the photo 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 ) ``` -Full guide coming soon. +This produces a `MediaIntegrity` object containing: +- `contentHash` — SHA-256 of the photo (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 photo 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 photos 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 index 2ded4ba..553109c 100644 --- a/docs/guides/python-validation.md +++ b/docs/guides/python-validation.md @@ -12,7 +12,7 @@ This guide covers advanced validation scenarios using the SignedShot Python libr pip install signedshot ``` -Requires Python 3.12+. The library is a compiled Rust extension for maximum performance. +Requires Python 3.12+. The library is a compiled Rust extension. ## Validation Methods From ce2181cad83333c717290b02f49879be0553c206 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Fri, 6 Feb 2026 09:29:41 -0300 Subject: [PATCH 12/18] docs: add API reference overview --- docs/api-reference/overview.md | 381 +++++++++++++++++++++++++++++++-- 1 file changed, 366 insertions(+), 15 deletions(-) diff --git a/docs/api-reference/overview.md b/docs/api-reference/overview.md index 4aa3382..5f153ba 100644 --- a/docs/api-reference/overview.md +++ b/docs/api-reference/overview.md @@ -4,33 +4,384 @@ sidebar_position: 1 # API Reference -:::info Work in Progress -This page is under construction. -::: +The SignedShot API provides cryptographic proof of authenticity for photos and videos. ## Base URL ``` -https://api.signedshot.io +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 a publisher token or device token in the `Authorization` header: +Most endpoints require authentication via the `Authorization` header: ``` Authorization: Bearer ``` -## Endpoints +| 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 -| Method | Endpoint | Description | +``` +POST /devices +``` + +**Headers:** + +| Header | Required | Description | |--------|----------|-------------| -| POST | `/publishers` | Register a publisher | -| POST | `/devices` | Register a device | -| POST | `/capture/session` | Start a capture session | -| POST | `/capture/trust` | Exchange nonce for trust token | -| POST | `/validate` | Validate media + sidecar | -| GET | `/.well-known/jwks.json` | Public keys for JWT verification | - -Full endpoint documentation coming soon. +| `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 photos with Python +- [iOS Integration](/guides/ios-integration) — Capture signed photos +- [Sidecar Format](/concepts/sidecar-format) — Proof structure reference From 4184ddc925c205df66a7909ec49246143185d4c2 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Fri, 6 Feb 2026 09:47:45 -0300 Subject: [PATCH 13/18] docs: add security section --- docs/security/limitations.md | 156 +++++++++++++++++++++++++++ docs/security/threat-model.md | 191 ++++++++++++++++++++++++++++++++++ sidebars.ts | 8 ++ 3 files changed, 355 insertions(+) create mode 100644 docs/security/limitations.md create mode 100644 docs/security/threat-model.md diff --git a/docs/security/limitations.md b/docs/security/limitations.md new file mode 100644 index 0000000..8f26bb1 --- /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. + +### Photo Only (Currently) + +Video support is in development. + +**Current state:** The protocol supports video (SHA-256 works on any byte stream), but the iOS SDK is optimized for photos. + +## 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 photo was captured on a verified device on [date] and has not been modified since." + +**Misleading:** +> "This photo is authentic and real." + +### Combine with Other Evidence + +SignedShot is one piece of evidence, not the complete picture: + +- 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..68a9583 --- /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 photo 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 photo 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/sidebars.ts b/sidebars.ts index 6638a9b..8d2fcda 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -27,6 +27,14 @@ const sidebars: SidebarsConfig = { 'api-reference/overview', ], }, + { + type: 'category', + label: 'Security', + items: [ + 'security/threat-model', + 'security/limitations', + ], + }, ], }; From b1b8edd55978eee7df838ffd4bb78c628471d8ba Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Fri, 6 Feb 2026 09:51:30 -0300 Subject: [PATCH 14/18] docs: use media-agnostic terminology (capture/media instead of photo) --- docs/api-reference/overview.md | 4 ++-- docs/concepts/two-layer-trust.md | 8 ++++---- docs/guides/ios-integration.md | 20 ++++++++++---------- docs/guides/python-validation.md | 8 ++++---- docs/guides/quick-start.md | 14 +++++++------- docs/intro.md | 14 +++++++------- docs/security/limitations.md | 10 +++++----- docs/security/threat-model.md | 4 ++-- 8 files changed, 41 insertions(+), 41 deletions(-) diff --git a/docs/api-reference/overview.md b/docs/api-reference/overview.md index 5f153ba..05345ab 100644 --- a/docs/api-reference/overview.md +++ b/docs/api-reference/overview.md @@ -382,6 +382,6 @@ The API currently has no rate limits. This may change in the future. ## Next Steps -- [Quick Start](/guides/quick-start) — Verify photos with Python -- [iOS Integration](/guides/ios-integration) — Capture signed photos +- [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/two-layer-trust.md b/docs/concepts/two-layer-trust.md index 5e577dc..3edb116 100644 --- a/docs/concepts/two-layer-trust.md +++ b/docs/concepts/two-layer-trust.md @@ -8,7 +8,7 @@ SignedShot uses two complementary layers of cryptographic proof to verify media ## Overview -When you capture a photo with SignedShot, two things happen: +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 @@ -22,7 +22,7 @@ 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 taking a photo +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 @@ -121,5 +121,5 @@ Verifiers can check the attestation method to make trust decisions. For example, ## Next Steps - [Sidecar Format](/concepts/sidecar-format) — Full JSON schema reference -- [Python Validation](/guides/python-validation) — Verify photos programmatically -- [iOS Integration](/guides/ios-integration) — Capture signed photos +- [Python Validation](/guides/python-validation) — Verify media programmatically +- [iOS Integration](/guides/ios-integration) — Capture signed media diff --git a/docs/guides/ios-integration.md b/docs/guides/ios-integration.md index 419263b..d4bbcef 100644 --- a/docs/guides/ios-integration.md +++ b/docs/guides/ios-integration.md @@ -4,7 +4,7 @@ sidebar_position: 2 # iOS Integration -Integrate SignedShot into your iOS app to capture photos with cryptographic proof of authenticity. +Integrate SignedShot into your iOS app to capture media with cryptographic proof of authenticity. ## Requirements @@ -89,8 +89,8 @@ Registration creates a device identity on the SignedShot backend and stores the The capture flow has four steps: 1. **Create session** — Get a capture ID and nonce from the backend -2. **Capture photo** — Take the photo (your camera code) -3. **Generate integrity** — Hash and sign the photo with Secure Enclave +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 @@ -103,19 +103,19 @@ let session = try await client.createCaptureSession() // session.expiresAt - Session expiration time ``` -### 2. Capture Photo +### 2. Capture Media Use your existing camera implementation. The SDK doesn't handle camera capture—it only handles signing. ```swift -// Your camera code produces JPEG data -let jpegData: Data = capturePhoto() +// Your camera code produces media data (JPEG, HEIC, etc.) +let mediaData: Data = captureMedia() let capturedAt = Date() ``` ### 3. Generate Media Integrity -Sign the photo with the device's Secure Enclave: +Sign the media with the device's Secure Enclave: ```swift let enclaveService = SecureEnclaveService() @@ -129,7 +129,7 @@ let integrity = try integrityService.generateIntegrity( ``` This produces a `MediaIntegrity` object containing: -- `contentHash` — SHA-256 of the photo (hex) +- `contentHash` — SHA-256 of the media (hex) - `signature` — ECDSA signature from Secure Enclave (base64) - `publicKey` — Device's public key (base64) - `captureId` — Matches the session @@ -160,7 +160,7 @@ let sidecarData = try generator.generate( ### 6. Save Files -Save the photo and sidecar together: +Save the media and sidecar together: ```swift let photoURL = documentsDirectory.appendingPathComponent("photo.jpg") @@ -331,7 +331,7 @@ Common errors: ## Next Steps -- [Quick Start](/guides/quick-start) — Verify photos with Python +- [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 index 553109c..459de64 100644 --- a/docs/guides/python-validation.md +++ b/docs/guides/python-validation.md @@ -54,7 +54,7 @@ jwks = requests.get("https://api.signedshot.io/.well-known/jwks.json").text result = signedshot.validate_with_jwks(sidecar_json, media_bytes, jwks) ``` -This is recommended for API services that validate many photos. +This is recommended for API services that validate many captures. ## Understanding Results @@ -101,7 +101,7 @@ integrity["captured_at"] # ISO8601 timestamp ### Validation Logic -A photo is valid when all these conditions are true: +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 @@ -123,7 +123,7 @@ except ValueError as e: print(f"Parse error: {e}") else: if result.valid: - print("Photo is authentic") + print("Media is authentic") else: print(f"Validation failed: {result.error}") ``` @@ -294,5 +294,5 @@ for item in validate_directory("./photos"): ## Next Steps - [Quick Start](/guides/quick-start) — Basic validation in 5 minutes -- [iOS Integration](/guides/ios-integration) — Capture photos with cryptographic proof +- [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 index 992f2f9..5ed2ea8 100644 --- a/docs/guides/quick-start.md +++ b/docs/guides/quick-start.md @@ -4,14 +4,14 @@ sidebar_position: 1 # Quick Start -Get up and running with SignedShot in 5 minutes. This guide shows you how to verify a photo's authenticity using the Python library. +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 photo with its sidecar file (e.g., `photo.jpg` and `photo.sidecar.json`) +- 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 a photo and download both files. +Don't have test files? Use the [interactive demo](https://signedshot.io/demo) to capture media and download both files. ## Install the Library @@ -19,16 +19,16 @@ Don't have test files? Use the [interactive demo](https://signedshot.io/demo) to pip install signedshot ``` -## Verify a Photo +## Verify Media ```python import signedshot -# Validate a photo with its sidecar +# Validate media with its sidecar result = signedshot.validate_files("photo.sidecar.json", "photo.jpg") if result.valid: - print("✓ Photo is authentic") + print("✓ Media is authentic") else: print(f"✗ Validation failed: {result.error}") ``` @@ -120,6 +120,6 @@ If any check fails, `result.valid` is `False` and `result.error` describes the i ## Next Steps -- [iOS Integration](/guides/ios-integration) — Capture photos with cryptographic proof +- [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/intro.md b/docs/intro.md index 9b404ad..4c47ca9 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -11,7 +11,7 @@ SignedShot is an open protocol for proving photos and videos haven't been altere ## The Problem -Digital manipulation is easy and getting easier. Anyone can alter a photo in seconds, and there's no built-in way to prove otherwise. +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." @@ -49,17 +49,17 @@ Together they prove: *"This exact content was captured on a verified device, in ## What You Can Do -### Capture signed photos -Use the iOS SDK to capture photos with embedded cryptographic proof. +### Capture signed media +Use the iOS SDK to capture media with embedded cryptographic proof. ```swift let session = try await signedShot.startSession() let sidecar = try await signedShot.createSidecar(imageData: jpegData, session: session) ``` -### Verify photos +### Verify media -Use the Python library to verify any SignedShot photo. +Use the Python library to verify any SignedShot capture. ```bash pip install signedshot @@ -78,8 +78,8 @@ Use the API directly to build custom integrations. - [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 photos -- [Python Validation](/guides/python-validation) — Verify photos programmatically +- [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 diff --git a/docs/security/limitations.md b/docs/security/limitations.md index 8f26bb1..7253cc3 100644 --- a/docs/security/limitations.md +++ b/docs/security/limitations.md @@ -100,11 +100,11 @@ The SDK currently supports iOS only. **Roadmap:** Android support is planned for post-launch. -### Photo Only (Currently) +### 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 is optimized for photos. +**Current state:** The protocol supports video (SHA-256 works on any byte stream), but the iOS SDK currently supports photos only. ## Comparison to Detection @@ -126,14 +126,14 @@ SignedShot takes a fundamentally different approach than AI detection: When displaying SignedShot verification results, be clear about what was verified: **Good:** -> "This photo was captured on a verified device on [date] and has not been modified since." +> "This media was captured on a verified device on [date] and has not been modified since." **Misleading:** -> "This photo is authentic and real." +> "This media is authentic and real." ### Combine with Other Evidence -SignedShot is one piece of evidence, not the complete picture: +SignedShot is one piece of evidence, not the whole story: - Cross-reference with other sources - Consider context and circumstances diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 68a9583..8c94975 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -18,7 +18,7 @@ SignedShot provides cryptographic proof that: 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 photo of a printed deepfake is still a valid SignedShot capture. +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 @@ -164,7 +164,7 @@ SignedShot's security relies on these assumptions: **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 photo is authentic; the content may not be. +**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 From 07d0efb656ba3d3741ecd9fc8fe52e471fc4fc87 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Fri, 6 Feb 2026 09:58:54 -0300 Subject: [PATCH 15/18] docs: add cryptographic specifications --- docs/concepts/cryptographic-specs.md | 355 +++++++++++++++++++++++++++ sidebars.ts | 1 + 2 files changed, 356 insertions(+) create mode 100644 docs/concepts/cryptographic-specs.md 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/sidebars.ts b/sidebars.ts index 8d2fcda..e18bade 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -9,6 +9,7 @@ const sidebars: SidebarsConfig = { items: [ 'concepts/two-layer-trust', 'concepts/sidecar-format', + 'concepts/cryptographic-specs', ], }, { From 041d824e10e693ccf7dde69cd41dfc07af89cf6d Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Fri, 6 Feb 2026 10:08:06 -0300 Subject: [PATCH 16/18] docs: add visual How It Works overview page --- docs/how-it-works.md | 259 +++++++++++++++++++++++++++++++++++++++++++ sidebars.ts | 1 + 2 files changed, 260 insertions(+) create mode 100644 docs/how-it-works.md diff --git a/docs/how-it-works.md b/docs/how-it-works.md new file mode 100644 index 0000000..adbb377 --- /dev/null +++ b/docs/how-it-works.md @@ -0,0 +1,259 @@ +--- +sidebar_position: 2 +--- + +# How It Works + +A visual overview of the SignedShot capture and verification flow. + +## The Big Picture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 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 │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 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 + +--- + +### 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 + +``` +┌─────────────────────────────────────────────────────────┐ +│ │ +│ 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." │ │ +│ │ │ │ +│ └─────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 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 ✗ | + +--- + +## Next Steps + +- [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/sidebars.ts b/sidebars.ts index e18bade..464dc54 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -3,6 +3,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; const sidebars: SidebarsConfig = { mainSidebar: [ 'intro', + 'how-it-works', { type: 'category', label: 'Concepts', From 97ed2662b9280d62ef040e7731efe15aebcb8c00 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Fri, 6 Feb 2026 10:14:37 -0300 Subject: [PATCH 17/18] docs: improve diagrams --- docs/how-it-works.md | 68 +++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/docs/how-it-works.md b/docs/how-it-works.md index adbb377..6587b41 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -8,19 +8,21 @@ A visual overview of the SignedShot capture and verification flow. ## The Big Picture +
+ ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ CAPTURE SIDE │ │ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Device │ │ Start │ │ Capture │ │ Sign │ │ -│ │ Register │ ──── │ Session │ ──── │ Media │ ──── │ & Save │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 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 │ +│ Get device Get nonce Take photo Sign with SE │ +│ token (once) + capture_id or video Get JWT │ +│ Save sidecar │ └─────────────────────────────────────────────────────────────────────────────┘ │ │ media + sidecar @@ -28,23 +30,27 @@ A visual overview of the SignedShot capture and verification flow. ┌─────────────────────────────────────────────────────────────────────────────┐ │ VERIFICATION SIDE │ │ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Parse │ │ Verify │ │ Verify │ │ Cross │ │ -│ │ Sidecar │ ──── │ JWT │ ──── │ Hash │ ──── │ Validate │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Parse │ │ Verify │ │ Verify │ │ Cross │ │ +│ │ Sidecar │ ──── │ JWT │ ──── │ Hash │ ──── │ Validate │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ -│ Read JSON Check sig Compare hash Match IDs │ -│ structure via JWKS + verify sig JWT ↔ media │ +│ Read JSON Check sig Compare hash Match IDs │ +│ structure via JWKS + verify sig JWT ↔ media │ └─────────────────────────────────────────────────────────────────────────────┘ ``` +
+ --- ## Capture Flow ### Step 1: Register Device (Once) +
+ ``` ┌─────────────┐ ┌─────────────┐ │ │ POST /devices │ │ @@ -57,6 +63,8 @@ A visual overview of the SignedShot capture and verification flow. └─────────────┘ └─────────────┘ ``` +
+ - 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) @@ -66,6 +74,8 @@ A visual overview of the SignedShot capture and verification flow. ### Step 2: Start Capture Session +
+ ``` ┌─────────────┐ ┌─────────────┐ │ │ POST /capture/session │ │ @@ -77,6 +87,8 @@ A visual overview of the SignedShot capture and verification flow. └─────────────┘ └─────────────┘ ``` +
+ - 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 @@ -85,6 +97,8 @@ A visual overview of the SignedShot capture and verification flow. ### Step 3: Capture Media +
+ ``` ┌─────────────────────────────────────────────────────────┐ │ │ @@ -97,6 +111,8 @@ A visual overview of the SignedShot capture and verification flow. └─────────────────────────────────────────────────────────┘ ``` +
+ - User captures media through the app - Media bytes are available before any disk write @@ -104,16 +120,18 @@ A visual overview of the SignedShot capture and verification flow. ### Step 4: Sign with Secure Enclave +
+ ``` ┌─────────────────────────────────────────────────────────┐ │ SECURE ENCLAVE │ │ ┌───────────────────────────────────────────────────┐ │ │ │ │ │ -│ │ media bytes ──► SHA-256 ──► hash │ │ +│ │ media bytes ──► SHA-256 ──► hash │ │ │ │ │ │ -│ │ hash + capture_id + timestamp ──► ECDSA sign │ │ +│ │ hash + capture_id + timestamp ──► ECDSA sign │ │ │ │ │ │ -│ │ Result: signature + public_key │ │ +│ │ Result: signature + public_key │ │ │ │ │ │ │ └───────────────────────────────────────────────────┘ │ │ │ @@ -121,6 +139,8 @@ A visual overview of the SignedShot capture and verification flow. └─────────────────────────────────────────────────────────┘ ``` +
+ - Compute SHA-256 hash of media bytes - Sign `{hash}:{capture_id}:{timestamp}` with Secure Enclave - Private key never leaves secure hardware @@ -129,6 +149,8 @@ A visual overview of the SignedShot capture and verification flow. ### Step 5: Exchange Nonce for JWT +
+ ``` ┌─────────────┐ ┌─────────────┐ │ │ POST /capture/trust │ │ @@ -140,6 +162,8 @@ A visual overview of the SignedShot capture and verification flow. └─────────────┘ └─────────────┘ ``` +
+ - 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 @@ -148,6 +172,8 @@ A visual overview of the SignedShot capture and verification flow. ### Step 6: Save Media + Sidecar +
+ ``` ┌─────────────────────────────────────────────────────────┐ │ │ @@ -169,6 +195,8 @@ A visual overview of the SignedShot capture and verification flow. └─────────────────────────────────────────────────────────┘ ``` +
+ - Save original media file (unchanged) - Save sidecar JSON with both trust layers - Files can be stored, shared, or uploaded together @@ -179,6 +207,8 @@ A visual overview of the SignedShot capture and verification flow. ### Anyone Can Verify +
+ ``` ┌─────────────────────────────────────────────────────────┐ │ │ @@ -205,10 +235,14 @@ A visual overview of the SignedShot capture and verification flow. └─────────────────────────────────────────────────────────┘ ``` +
+ --- ## What Each Layer Proves +
+ ``` ┌─────────────────────────────────────────────────────────┐ │ │ @@ -236,6 +270,8 @@ A visual overview of the SignedShot capture and verification flow. └─────────────────────────────────────────────────────────┘ ``` +
+ --- ## Quick Reference From 6476413fe2d040213bd16d2bc70f30613c39d4b5 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Fri, 6 Feb 2026 10:26:32 -0300 Subject: [PATCH 18/18] docs: fix layout and audit issues --- docs/how-it-works.md | 6 +- package-lock.json | 280 ++++++++++++++++++++++++++----------------- 2 files changed, 174 insertions(+), 112 deletions(-) diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 6587b41..d63754c 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -24,9 +24,9 @@ A visual overview of the SignedShot capture and verification flow. │ token (once) + capture_id or video Get JWT │ │ Save sidecar │ └─────────────────────────────────────────────────────────────────────────────┘ - │ - │ media + sidecar - ▼ + │ + │ media + sidecar + ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ VERIFICATION SIDE │ │ │ 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": {