[Ondertekende, Merkle-geketende transcripties; PROV-O JSON-LD; HMAC standaard met PQC opt-in]

Provenance en compliance

Elke muterende operatie op een evidence-record schrijft een ondertekende, Merkle-geketende rij in `provenance_records` binnen dezelfde databasetransactie als de audit-entry. Partners kunnen de volledige transcriptie ophalen als JSON of PROV-O JSON-LD, server-side verifiëren of lokaal met een offline helper in de SDK, en hem presenteren als chain of custody voor juridische procedures. HMAC-SHA256 standaard; ML-DSA-65 (post-kwantum) opt-in via de bestaande PQC-adapter.

Provenance en compliance

Merkle-geketende provenance

Elke rij in `provenance_records` draagt `parent_id` en `parent_hash`, zodat de transcriptie per `evidence_id` een append-only ketting vormt. Het eerste record van een evidence-object heeft `parent_hash: null`; elke volgende operatie (`evidence.create`, `evidence.update`, `redaction.apply`, `access.read`, door jobs uitgezonden events, enzovoort) hasht het canonieke JSON van het vorige record in zijn eigen `parent_hash`. De transcriptie wordt vervolgens gebonden door een Merkle root over de geordende records en onafhankelijk ondertekend. Een verifier loopt de ketting terug tot het genesis-record, herberekent elke schakel en wijst de transcriptie af als een `parent_hash` niet klopt - dat is wat een wijziging van één byte end-to-end detecteerbaar maakt.

Handtekeningalgoritmen

JSON-canonicalisatie volgt RFC 8785 (JCS): de handtekening-payload per record is het canonieke JSON van `{tenant_id, evidence_id, parent_id, operation, actor_id, actor_kind, content_hash, parent_hash, signed_at}`. Het standaardalgoritme is HMAC-SHA256 met een per-tenant secret afgeleid van de platform-master-sleutel (`ARGUS_PROVENANCE_MASTER_KEY`) via HKDF over het label `argus.provenance.tenant.{tenant_id}`. ML-DSA-65 is opt-in via `?algorithm=ml-dsa-65` en hergebruikt de bestaande `pqc_adapter`; de publieke verify-keys worden gepubliceerd op `/v1/.well-known/provenance-keys/ml-dsa-65`, zodat externe auditoren en offline verifiers handtekeningen kunnen valideren zonder contact met het platform.

Uitvoerformaten

`GET /v1/evidence/{evidence_id}/provenance` accepteert `?format=json` (standaard) en `?format=jsonld`. De JSON-vorm is de wire-native vorm die door het verify-endpoint en de SDK’s wordt gebruikt. De JSON-LD-vorm produceert een PROV-O-document met `@context: https://www.w3.org/ns/prov` en standaard term-mapping: elk record mapt op een `prov:Activity`, `prov:wasGeneratedBy` koppelt de activiteit aan haar actor (`prov:Agent`), `prov:wasDerivedFrom` verbindt de nieuwe evidence-staat met de eerdere staat (`prov:Entity`), en de Merkle root wordt blootgesteld als top-level `prov:Bundle` met de bindende handtekening als `prov:hadPrimarySource`-achtige attributie. Kies JSON-LD als je moet interopereren met W3C PROV-tooling, evidence-catalogi of legaltech-platformen die al PROV-O verwerken.

Verificatie

Twee verificatiepaden worden ondersteund. Server-side: `POST /v1/evidence/{evidence_id}/provenance/verify` met `{transcript: <transcriptie-object>}` voert de canonicalisatie opnieuw uit, herberekent elke record-handtekening, valideert de Merkle root en levert `{ valid, broken_links, checked_records, merkle_root_verified }`. Het endpoint geeft nooit 4xx terug voor een gemanipuleerde maar correct opgemaakte transcriptie: 400 is gereserveerd voor malformed input, en een gemanipuleerde transcriptie levert 200 met `valid: false` en een gevuld `broken_links`-array. Lokaal/offline: de SDK’s leveren `evidence.verifyProvenanceLocal(transcript)` (TypeScript) en `evidence.verify_provenance_local(transcript)` (Python). De offline helper voert dezelfde controles uit zonder netwerk-round-trip; voor ML-DSA-65 leest hij de publieke verify-key uit `/v1/.well-known/provenance-keys/ml-dsa-65`. HMAC-verificatie blijft server-side omdat het per-tenant secret nooit wordt gepubliceerd.

PROV-O-vocabulaire en juridische interoperabiliteit

PROV-O is de W3C Recommendation voor het representeren van provenance: wie heeft wat gedaan, wanneer en op basis van welke eerdere staat. Onze records mappen op `prov:Activity`, `prov:Agent` en `prov:Entity` zorgt ervoor dat een door de rechtbank aangewezen auditor, een toezichthouder of een partner een transcriptie kan inlezen met standaard tooling in plaats van een Knogin-specifieke parser. De JSON-LD-vorm houdt alle Argus-specifieke velden (`tenant_id`, `signature_alg`, `merkle_root`, `trace_id`, `job_id`) onder de `argus:`-namespace naast de PROV-O-termen, zodat de auditor eerst de standaardgraaf ziet en daarna de implementatiedetails. Deze scheiding is belangrijk voor juridische ontvankelijkheid: de auditor kan de ketting beredeneren in de taal van de W3C-standaard en pas vervolgens in de `argus:`-velden duiken wanneer een bewijsvraag platformspecifieke context vereist.

Gebruik in juridische procedures

De transcriptie is ontworpen om een chain-of-custody-test te doorstaan. Elk record noemt de actor (`actor_id`, `actor_kind` uit `user`, `service`, `job`, `system`), de operatie, de content hash van de evidence op dat moment, het vorige record waarvan het is afgeleid, en een wall-clock `signed_at` uit de databasetransactie. De transcriptie draagt dezelfde `trace_id` die via de G2-observability-surface stroomt en, waar van toepassing, de `job_id` uit G3 - zodat een onderzoeker een provenance-entry kan correleren met de omringende systeemactiviteit die het opleverde. Cryptografische integriteit (HMAC of ML-DSA-65) plus de Merkle root betekenen dat één gewijzigde byte de transcriptie bij verificatie ongeldig maakt. ML-DSA-65 is het aanbevolen algoritme voor evidence-pakketten met hoge zekerheid omdat de publieke verify-keys de transcriptie jaren na opening van een zaak onafhankelijk verifieerbaar maken, ook als Knogin niet langer de bewaarder is.

Tenant-isolatie

Elke lees-operatie filtert op `tenant_id` EN `organization_id` uit het bearer token. Cross-tenant-toegang levert 404 op (nooit 403), dus het bestaan van een provenance-keten in een andere tenant wordt nooit prijsgegeven. Het SecrecyLevel van de onderliggende evidence geldt: een operator onder het secrecy-niveau van de evidence kan haar provenance niet lezen, ook niet binnen zijn eigen tenant. Het HMAC-secret is per tenant (HKDF-afgeleid van de platform-master-sleutel over het label `argus.provenance.tenant.{tenant_id}`), zodat een voor één tenant ondertekende transcriptie niet tegen een andere kan worden gehervalideerd. Beide endpoints schrijven een `provenance.read`-audit-entry met `resource_id = evidence_id` en `metadata = { record_count, format, algorithm, valid }`, zodat het ophalen of verifiëren van een transcriptie zelf in het audit-trail wordt vastgelegd.

Ondersteunde algoritmen

HMAC-SHA256 (standaard, tenant-scoped) en ML-DSA-65 (post-kwantum, FIPS 204; opt-in via ?algorithm=ml-dsa-65).

Een JSON-transcriptie ophalen

# Fetch a JSON transcript (default algorithm: HMAC-SHA256)
curl "https://api.knogin.com/v1/evidence/ev_abc123/provenance" \
  -H "Authorization: Bearer $TOKEN"

# 200 OK
# {
#   "evidence_id": "ev_abc123",
#   "tenant_id": "tnt_123",
#   "records": [
#     {
#       "id": "01HFXY...",
#       "operation": "evidence.create",
#       "actor_id": "user_42",
#       "actor_kind": "user",
#       "content_hash": "sha256:b94d...",
#       "parent_hash": null,
#       "signature": "<hex>",
#       "signature_alg": "hmac-sha256",
#       "signed_at": "2026-05-08T10:00:00Z",
#       "trace_id": "0af7651916cd43dd8448eb211c80319c",
#       "job_id": null
#     }
#   ],
#   "merkle_root": "sha256:...",
#   "root_signature": "<hex>",
#   "root_signature_alg": "hmac-sha256",
#   "format": "json",
#   "version": "2026.4"
# }

# Same transcript, post-quantum signatures:
curl "https://api.knogin.com/v1/evidence/ev_abc123/provenance?algorithm=ml-dsa-65" \
  -H "Authorization: Bearer $TOKEN"

Een PROV-O JSON-LD-transcriptie ophalen

# Fetch the same transcript as PROV-O JSON-LD
curl "https://api.knogin.com/v1/evidence/ev_abc123/provenance?format=jsonld" \
  -H "Authorization: Bearer $TOKEN"

# 200 OK
# {
#   "@context": [
#     "https://www.w3.org/ns/prov",
#     { "argus": "https://schema.knogin.com/argus/v1#" }
#   ],
#   "@type": "prov:Bundle",
#   "@id": "argus:evidence/ev_abc123/provenance",
#   "argus:tenant_id": "tnt_123",
#   "argus:merkle_root": "sha256:...",
#   "argus:root_signature": "<hex>",
#   "argus:root_signature_alg": "hmac-sha256",
#   "@graph": [
#     {
#       "@id": "argus:provenance/01HFXY",
#       "@type": "prov:Activity",
#       "prov:startedAtTime": "2026-05-08T10:00:00Z",
#       "argus:operation": "evidence.create",
#       "argus:content_hash": "sha256:b94d...",
#       "argus:signature": "<hex>",
#       "argus:signature_alg": "hmac-sha256",
#       "argus:trace_id": "0af7651916cd43dd8448eb211c80319c",
#       "prov:wasGeneratedBy": { "@id": "argus:agent/user_42", "@type": "prov:Agent" },
#       "prov:wasDerivedFrom": null
#     }
#   ]
# }

Een transcriptie server-side verifiëren

# Server-side verification of a transcript fetched earlier.
curl -X POST https://api.knogin.com/v1/evidence/ev_abc123/provenance/verify \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"transcript\": $(cat transcript.json)}"

# 200 OK (intact)
# {
#   "valid": true,
#   "evidence_id": "ev_abc123",
#   "broken_links": [],
#   "checked_records": 5,
#   "merkle_root_verified": true
# }

# 200 OK (tampered: a single byte changed in record 01HFXY)
# {
#   "valid": false,
#   "evidence_id": "ev_abc123",
#   "broken_links": [
#     { "record_id": "01HFXY...", "reason": "content_hash_mismatch" }
#   ],
#   "checked_records": 5,
#   "merkle_root_verified": false
# }

Offline verifiëren (TypeScript-SDK)

// Offline verification with @argus/client (TypeScript).
// HMAC verification stays server-side; ML-DSA-65 transcripts can be verified
// locally using the public verify keys from /v1/.well-known/provenance-keys.

import { ArgusClient } from "@argus/client";

const argus = new ArgusClient({
  baseUrl: "https://api.knogin.com",
  token: process.env.ARGUS_TOKEN!,
});

const transcript = await argus.evidence.getProvenance("ev_abc123", {
  format: "json",
  algorithm: "ml-dsa-65",
});

// No network round-trip; the helper fetches /v1/.well-known/provenance-keys
// once per kid, caches the result, and verifies signatures + Merkle root.
const result = await argus.evidence.verifyProvenanceLocal(transcript);

if (!result.valid) {
  console.error("Tampered transcript", result.brokenLinks);
  process.exit(1);
}
console.log("Verified", result.checkedRecords, "records");

Offline verifiëren (Python-SDK)

# Offline verification with argus-client (Python).
# Same shape as the TypeScript helper; suitable for evidence packages
# stored in court archives long after the case opens.

from argus_client import ArgusClient

argus = ArgusClient(base_url="https://api.knogin.com", token=os.environ["ARGUS_TOKEN"])

transcript = argus.evidence.get_provenance(
    "ev_abc123",
    format="json",
    algorithm="ml-dsa-65",
)

result = argus.evidence.verify_provenance_local(transcript)

if not result.valid:
    raise SystemExit(f"Tampered transcript: {result.broken_links}")

print(f"Verified {result.checked_records} records; merkle_root_verified={result.merkle_root_verified}")

Provenance nodig voor een gereguleerd workflow?

Open de API-referentie voor het publieke contract of neem contact op met Knogin als je ML-DSA-65-sleutels wilt laten provisioneren voor een externe auditor.