[Transcrições assinadas e encadeadas por Merkle; PROV-O JSON-LD; HMAC por defeito com PQC opt-in]

Proveniência e conformidade

Cada operação que muta um registo de evidência emite uma linha assinada e encadeada por Merkle em `provenance_records`, dentro da mesma transação de base de dados que a entrada de auditoria. Os parceiros podem obter a transcrição completa como JSON ou PROV-O JSON-LD, verificá-la no servidor ou localmente com um helper sem rede no SDK, e apresentá-la como cadeia de custódia para procedimentos legais. HMAC-SHA256 por defeito; ML-DSA-65 (pós-quântico) opt-in via o adaptador PQC existente.

Proveniência e conformidade

Proveniência encadeada por Merkle

Cada linha de `provenance_records` carrega `parent_id` e `parent_hash`, pelo que a transcrição forma uma cadeia append-only por `evidence_id`. O primeiro registo de um objeto de evidência tem `parent_hash: null`; cada operação seguinte (`evidence.create`, `evidence.update`, `redaction.apply`, `access.read`, eventos emitidos por jobs, e assim por diante) faz hash do JSON canónico do registo anterior dentro do seu `parent_hash`. A transcrição é então ligada por uma Merkle root sobre os registos ordenados e assinada de forma independente. Um verificador percorre a cadeia até ao registo génesis, recomputa cada elo e rejeita a transcrição se algum `parent_hash` não corresponder - é isto que torna uma alteração de um único byte detetável de ponta a ponta.

Algoritmos de assinatura

A canonicalização JSON segue RFC 8785 (JCS): o payload de assinatura por registo é o JSON canónico de `{tenant_id, evidence_id, parent_id, operation, actor_id, actor_kind, content_hash, parent_hash, signed_at}`. O algoritmo por defeito é HMAC-SHA256 com um segredo por tenant derivado da chave-mestra de plataforma (`ARGUS_PROVENANCE_MASTER_KEY`) via HKDF sobre o label `argus.provenance.tenant.{tenant_id}`. ML-DSA-65 é opt-in via `?algorithm=ml-dsa-65` e reutiliza o `pqc_adapter` existente; as suas chaves públicas de verificação são expostas em `/v1/.well-known/provenance-keys/ml-dsa-65` para que auditores externos e verificadores offline possam validar assinaturas sem contactar a plataforma.

Formatos de saída

`GET /v1/evidence/{evidence_id}/provenance` aceita `?format=json` (por defeito) e `?format=jsonld`. A forma JSON é a representação nativa em wire usada pelo endpoint de verificação e pelos SDKs. A forma JSON-LD emite um documento PROV-O com `@context: https://www.w3.org/ns/prov` e mapeamento padrão de termos: cada registo mapeia para um `prov:Activity`, `prov:wasGeneratedBy` liga a atividade ao seu ator (`prov:Agent`), `prov:wasDerivedFrom` une o novo estado da evidência ao estado anterior (`prov:Entity`), e a Merkle root é exposta como `prov:Bundle` de topo com a assinatura de ligação anexada como atribuição estilo `prov:hadPrimarySource`. Escolhe JSON-LD quando precisares de interoperar com tooling W3C PROV, catálogos de evidência ou plataformas legaltech que já consomem PROV-O.

Verificação

Dois caminhos de verificação são suportados. No servidor: `POST /v1/evidence/{evidence_id}/provenance/verify` com `{transcript: <objeto transcrição>}` reexecuta a canonicalização, recomputa cada assinatura por registo, valida a Merkle root e devolve `{ valid, broken_links, checked_records, merkle_root_verified }`. O endpoint nunca devolve 4xx para uma transcrição manipulada mas bem formada: 400 está reservado para entrada malformada, e uma transcrição manipulada devolve 200 com `valid: false` e um array `broken_links` populado. Local/offline: os SDKs trazem `evidence.verifyProvenanceLocal(transcript)` (TypeScript) e `evidence.verify_provenance_local(transcript)` (Python). O helper offline executa as mesmas verificações sem round-trip de rede; para ML-DSA-65 lê a chave pública de verificação a partir de `/v1/.well-known/provenance-keys/ml-dsa-65`. A verificação HMAC mantém-se no servidor porque o segredo por tenant nunca é publicado.

Vocabulário PROV-O e interoperabilidade legal

PROV-O é a W3C Recommendation para representar proveniência: quem fez o quê, quando e com base em que estado anterior. Mapear os nossos registos para `prov:Activity`, `prov:Agent` e `prov:Entity` permite que um auditor nomeado pelo tribunal, um regulador ou um parceiro ingira uma transcrição com tooling padrão em vez de um parser específico da Knogin. A forma JSON-LD mantém todos os campos específicos da Argus (`tenant_id`, `signature_alg`, `merkle_root`, `trace_id`, `job_id`) sob o namespace `argus:` ao lado dos termos PROV-O, de modo que o auditor vê primeiro o grafo padrão e depois os detalhes de implementação. Esta separação importa para a admissibilidade legal: o auditor pode raciocinar sobre a cadeia na linguagem do padrão W3C e só depois aprofundar nos campos do namespace `argus:` quando uma questão probatória exigir contexto específico da plataforma.

Uso em procedimentos legais

A transcrição é projetada para satisfazer um teste de cadeia de custódia. Cada registo nomeia o ator (`actor_id`, `actor_kind` de `user`, `service`, `job` ou `system`), a operação, o content hash da evidência nesse ponto, o registo anterior do qual deriva, e um `signed_at` de relógio de parede tirado da transação de base de dados. A transcrição leva o mesmo `trace_id` que flui pela superfície de observabilidade do G2 e, quando aplicável, o `job_id` do G3 - assim um investigador pode correlacionar uma entrada de proveniência com a atividade de sistema que a produziu. A integridade criptográfica (HMAC ou ML-DSA-65) mais a Merkle root significam que um único byte alterado invalida a transcrição na verificação. ML-DSA-65 é o algoritmo recomendado para pacotes de evidência de alta garantia porque as suas chaves públicas de verificação tornam a transcrição verificável de forma independente anos depois de o caso ser aberto, mesmo que a Knogin já não seja a depositária.

Isolamento por tenant

Toda a leitura filtra por `tenant_id` E `organization_id` do bearer token. Acessos cross-tenant devolvem 404 (nunca 403), pelo que a existência de uma cadeia de proveniência noutro tenant nunca é divulgada. O SecrecyLevel da evidência subjacente aplica-se: um operador abaixo do nível de segredo da evidência não pode ler a sua proveniência mesmo dentro do seu próprio tenant. O segredo HMAC é por tenant (derivado por HKDF da chave-mestra de plataforma sobre o label `argus.provenance.tenant.{tenant_id}`), pelo que uma transcrição assinada para um tenant não pode ser revalidada contra outro. Ambos os endpoints emitem uma entrada de auditoria `provenance.read` com `resource_id = evidence_id` e `metadata = { record_count, format, algorithm, valid }`, de modo que o ato de obter ou verificar uma transcrição fica também registado na pista de auditoria.

Algoritmos suportados

HMAC-SHA256 (por defeito, tenant-scoped) e ML-DSA-65 (pós-quântico, FIPS 204; opt-in via ?algorithm=ml-dsa-65).

Obter uma transcrição JSON

# 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"

Obter uma transcrição PROV-O JSON-LD

# 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
#     }
#   ]
# }

Verificar uma transcrição no servidor

# 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
# }

Verificação offline (SDK TypeScript)

// 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");

Verificação offline (SDK Python)

# 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}")

Precisas de proveniência para um fluxo regulado?

Abre a referência API para o contrato público ou contacta a Knogin se precisares de chaves ML-DSA-65 provisionadas para um auditor externo.