# Evidence Envelope v1 — specyfikacja formatu certyfikatu

Status: obowiązujący (Blok 8, 2026-07-28). Zmiany wyłącznie addytywne.

## 1. Cel

Evidence Envelope to wersjonowany, rozszerzalny kontener dowodowy dołączany do
każdego proofa. Zawiera wszystkie sygnały zebrane w chwili certyfikacji, jest
kanonizowany, hashowany (sha256 → `envelope_hash`) i podpisywany serwerowo
(Ed25519). `envelope_hash` jest wartością, która otrzymuje znacznik czasu
RFC 3161 i kotwicę blockchain (OpenTimestamps).

## 2. Struktura

```jsonc
{
  "envelope_version": "1.0",
  "created_at": "2026-07-28T12:00:00.000Z",     // ISO 8601, czas serwera
  "capture": {
    "session_id": "cuid sesji",
    "step_index": 0,
    "nonce": "nonce challenge-response",         // patrz §6
    "capture_source": "live_stream",             // patrz §7
    "device": { "user_agent": "…", "webgl_vendor": "…", "webgl_renderer": "…" }
  },
  "photo": {
    "sha256": "hex",                             // hash pliku zdjęcia
    "sha3": "hex",                               // SHA-3 (Keccak-256)
    "byte_size": 123456,
    "mime_type": "image/jpeg"
  },
  "signals": [
    {
      "name": "location",                        // rejestr nazw — §4
      "status": "present",                       // §3
      "value": { /* dowolny JSON */ },
      "summary": "krótki opis dla człowieka",
      "reason": "powód statusu not_available/failed",
      "collected_at": "ISO 8601"
    }
  ]
}
```

## 3. Statusy sygnałów

| status          | znaczenie                                                     |
|-----------------|---------------------------------------------------------------|
| `present`       | sygnał zebrany poprawnie, `value` zawiera dane                |
| `not_available` | sygnału nie dało się zebrać; `reason` mówi dlaczego (np. `user_declined`, `unsupported`) |
| `failed`        | sygnał zebrany, ale wskazuje na problem (np. duplikat)        |
| `pending`       | sygnał w trakcie pozyskiwania (np. timestamp TSA w retry)     |

## 4. Rejestr sygnałów (v1)

| nazwa             | zawartość                                                        |
|-------------------|------------------------------------------------------------------|
| `location`        | GPS: lat/lng/accuracy, liczba odczytów, confidence anti-spoofing |
| `sensors`         | podsumowanie DeviceMotion/Orientation + serie (wariancja, próbki)|
| `environment`     | typ połączenia, bateria, ciśnienie, fingerprint canvas/WebGL     |
| `time`            | blok czasu: czas urządzenia, EXIF, czas serwera, delty           |
| `forensics`       | raport forensyczny per-check (EXIF, kwantyzacja JPEG, thumbnail) |
| `duplicate_check` | wynik porównania pHash/dHash z rejestrem odcisków                |
| `score`           | breakdown credibility score v2 (czynniki, wagi, kontrybucje)     |
| `liveness`        | wynik heurystyki żywości (mikro-ruch ręki vs statyw/emulator)    |

**Zasada ewolucji addytywnej:** weryfikator MUSI ignorować sygnały o nieznanych
nazwach. Nowe sygnały nie unieważniają starych certyfikatów — stare certyfikaty
pozostają ważne na zawsze. Usuwanie i zmiana znaczenia istniejących nazw jest
zabroniona; zmiany łamiące wymagają `envelope_version: "2.0"`.

## 5. Kanonizacja, hash i podpis

1. **Kanonizacja** (w duchu RFC 8785): klucze obiektów sortowane
   leksykograficznie (rekurencyjnie), zero białych znaków, `undefined`
   pomijane w obiektach i zabronione w tablicach, NaN/Infinity zabronione.
   Kolejność `signals[]` jest normalizowana (sort po `name`) przed podpisem.
2. **Hash**: `envelope_hash = sha256(canonicalize(envelope))` (hex).
3. **Podpis**: Ed25519 nad bajtami `envelope_hash` (hex-decoded). Klucz z env
   `ENVELOPE_SIGNING_KEY` (32-bajtowy seed). Obok podpisu przechowujemy
   `key_id` (16 hex sha256 klucza publicznego) i sam klucz publiczny (base64),
   więc certyfikat jest samowystarczalny do weryfikacji.
4. **Atestacje** `envelope_hash` (token RFC 3161, proof OpenTimestamps) żyją
   POZA podpisanym rdzeniem — nie mogą być częścią hasha, który atestują.
   W widoku weryfikacji prezentowane są jako sygnały `timestamp` i `anchor`
   ze statusem `pending`/`present`.

## 6. Nonce challenge-response

Serwer wystawia jednorazowy, podpisany nonce z TTL (domyślnie 30 min,
env `CAPTURE_NONCE_TTL_MINUTES`) powiązany z sesją. Klient wysyła nonce z
każdym uploadem; serwer weryfikuje podpis, TTL, jednorazowość (persystencja)
i powiązanie z sesją, po czym zapisuje nonce w envelope.

**Cel:** nonce dowodzi, że zdjęcie powstało wewnątrz okna czasowego tej sesji —
blokuje na poziomie protokołu wgrywanie zdjęć zrobionych wcześniej / z galerii.
Odrzucenie (wygaśnięcie/ponowne użycie) daje czytelny błąd dla UI:
„sesja wygasła — poproś o nowy link".

### 6.1 Okno czasowe a realne, długie sesje (Blok 9)

Semantyka okien czasowych — trzy NIEZALEŻNE zegary:

1. **TTL nonce'a (30 min)** ogranicza JEDNO zdjęcie (od pobrania nonce'a do
   uploadu), NIE łączną długość sesji. Klient pobiera świeży nonce przed
   **każdą próbą** uploadu (od Bloku 9; wcześniej: raz na zdjęcie, wspólny dla
   retry — utrata odpowiedzi w trakcie próby oznaczała `NONCE_REUSED` przy
   ponowieniu, bo nonce był już skonsumowany na początku pipeline'u).
   Świeży nonce per próba zachowuje pełną gwarancję anti-replay: każdy nonce
   pozostaje jednorazowy (atomowy consume), podpisany HMAC i związany z sesją;
   skraca też realne okno issue→upload do sekund. Retry z tym samym nonce
   nigdy nie jest poprawny — i nigdy nie jest już wysyłany.
2. **Sesje E2E nie wygasają** — serwerowy keypair ECDH jest derywowany
   bezstanowo z `ENCRYPTION_KEY + sessionId` (patrz `routes/e2e.ts`), więc
   długość sesji nie wpływa na deszyfrowanie.
3. **`Session.expiresAt`** (godziny, ustawiane przy tworzeniu sesji) to jedyny
   limit łącznej długości. UI pokazuje licznik pozostałego czasu, a po
   wygaśnięciu jawny stan „Sesja wygasła" — dotychczasowe proofy pozostają
   zapisane i certyfikowane; wznowienie = nowy link (nowa sesja) lub ponowne
   otwarcie linku przed wygaśnięciem (klient odtwarza postęp z
   `stepPhotoCounts`).

Wniosek projektowy: NIE wydłużamy TTL nonce'a dla sesji szablonowych —
per-próba świeży nonce czyni TTL nieistotnym dla długości sesji, a krótkie
okno issue→upload jest mocniejszym dowodem świeżości.

## 7. Gwarancja live-stream

Strona capture NIE zawiera fallbacku `<input type="file">`. Klatki pochodzą
wyłącznie ze streamu `getUserMedia` (kamera na żywo). Envelope zapisuje
`capture_source: "live_stream"`. Każde inne źródło (np. przyszłe ścieżki
importu dokumentów) musi być jawnie oznaczone innym `capture_source` i jest
oceniane inaczej przez score.

## 8. Kompatybilność wsteczna

Proofy sprzed Bloku 8 mają `envelopeVersion = NULL` w DB i są weryfikowane
wg starych zasad (hash pliku + metadane). Odpowiedź API certyfikatu zawiera
wtedy `proof.envelope = null`. Weryfikator nie może odrzucić certyfikatu z
powodu braku envelope.

## 9. Weryfikacja certyfikatu z envelope

Kroki weryfikatora (`GET /api/certificates/:publicId` robi to serwerowo,
ale każdy może powtórzyć niezależnie):

1. Pobierz `envelope`, `envelopeHash`, `signature`, `publicKey`.
2. Sprawdź `sha256(canonicalize(envelope)) == envelopeHash`.
3. Sprawdź podpis Ed25519 nad bajtami `envelopeHash`.
4. Sprawdź `photo.sha256` względem pliku (jeśli dostępny).
5. Zweryfikuj token RFC 3161 dla `envelopeHash` (jeśli `present`).
6. Zweryfikuj proof OpenTimestamps dla `envelopeHash` (jeśli `confirmed`).
7. Ignoruj sygnały o nieznanych nazwach (§4).

## 10. C2PA / Content Credentials — derywat dystrybucyjny (Blok 10)

**Reguła architektoniczna: oryginalne bajty dowodu NIGDY nie są
modyfikowane.** `sha256` oryginału (`photo.sha256`) pozostaje kotwicą
dowodową — to jego dotyczą podpis envelope, znacznik RFC 3161 i kotwica
OpenTimestamps.

Plik z osadzonym manifestem C2PA jest **DERYWATEM** generowanym wyłącznie do
dystrybucji (leniwie, przy pierwszym żądaniu `GET
/api/certificates/:publicId/c2pa`; cache w S3 pod prefiksem `c2pa/`).
Manifest zawiera m.in.: czas capture z envelope (`created_at`), issuer
"Evitrust", id certyfikatu, `envelope_hash`, `original_sha256` (hash
ORYGINAŁU — nie derywatu), URL weryfikacji i score. Claim generator:
"Evitrust Evidence Spine".

Podpis: X.509 (ES256) z env `C2PA_SIGNING_CERT` / `C2PA_SIGNING_KEY`
(PEM lub base64(PEM); generowanie: `scripts/generate-c2pa-cert.sh`).
Certyfikat jest na razie self-signed: manifest waliduje się STRUKTURALNIE
w każdym zgodnym narzędziu (contentcredentials.org/verify, Photoshop),
a provenance wskazuje Evitrust; narzędzia pokażą ostrzeżenie o braku
certyfikatu na liście zaufania C2PA — członkostwo w trust list to przyszły
krok biznesowy.

Weryfikator, który otrzymał plik-derywat, odzyskuje powiązanie z dowodem:
manifest → `verify_url` / `certificate_id` → strona verify → pełna
weryfikacja envelope (§9) względem `original_sha256`.
