[Transcripciones firmadas y encadenadas por Merkle; PROV-O JSON-LD; HMAC por defecto con PQC opt-in]

Proveniencia y cumplimiento

Cada operación que muta un registro de evidencia emite una fila firmada y encadenada por Merkle en `provenance_records`, dentro de la misma transacción de base de datos que la entrada de auditoría. Los partners pueden obtener la transcripción completa como JSON o como PROV-O JSON-LD, verificarla en el servidor o localmente con un helper sin red en el SDK, y presentarla como cadena de custodia para procedimientos legales. HMAC-SHA256 por defecto; ML-DSA-65 (post-cuántico) opt-in mediante el adaptador PQC existente.

Proveniencia y cumplimiento

Proveniencia encadenada con Merkle

Cada fila de `provenance_records` lleva `parent_id` y `parent_hash`, de modo que la transcripción forma una cadena append-only por `evidence_id`. El primer registro de un objeto de evidencia tiene `parent_hash: null`; cada operación posterior (`evidence.create`, `evidence.update`, `redaction.apply`, `access.read`, eventos emitidos por jobs, etc.) hashea el JSON canónico del registro anterior dentro de su `parent_hash`. La transcripción se enlaza a continuación mediante un Merkle root sobre los registros ordenados, firmado de forma independiente. Un verificador recorre la cadena hasta el registro génesis recomputando cada enlace y rechaza la transcripción si algún `parent_hash` no coincide - eso es lo que hace que un cambio de un solo byte sea detectable de extremo a extremo.

Algoritmos de firma

La canonicalización JSON sigue RFC 8785 (JCS): el payload de firma por registro es el JSON canónico de `{tenant_id, evidence_id, parent_id, operation, actor_id, actor_kind, content_hash, parent_hash, signed_at}`. El algoritmo por defecto es HMAC-SHA256 con un secreto por tenant derivado de la clave maestra de plataforma (`ARGUS_PROVENANCE_MASTER_KEY`) mediante HKDF sobre la etiqueta `argus.provenance.tenant.{tenant_id}`. ML-DSA-65 es opt-in vía `?algorithm=ml-dsa-65` y reutiliza el `pqc_adapter` existente; sus claves públicas de verificación se exponen en `/v1/.well-known/provenance-keys/ml-dsa-65` para que auditores externos y verificadores offline puedan validar firmas sin contactar a la plataforma.

Formatos de salida

`GET /v1/evidence/{evidence_id}/provenance` acepta `?format=json` (por defecto) y `?format=jsonld`. La forma JSON es la representación nativa en wire usada por el endpoint de verificación y los SDKs. La forma JSON-LD emite un documento PROV-O con `@context: https://www.w3.org/ns/prov` y mapeo estándar de términos: cada registro mapea a un `prov:Activity`, `prov:wasGeneratedBy` enlaza la actividad con su actor (`prov:Agent`), `prov:wasDerivedFrom` une el nuevo estado de la evidencia con el estado previo (`prov:Entity`), y el Merkle root se expone como un `prov:Bundle` de nivel superior con la firma de enlace adjunta como atribución estilo `prov:hadPrimarySource`. Elige JSON-LD cuando necesites interoperar con tooling W3C PROV, catálogos de evidencia o plataformas legaltech que ya consumen PROV-O.

Verificación

Se admiten dos rutas de verificación. Server-side: `POST /v1/evidence/{evidence_id}/provenance/verify` con `{transcript: <objeto transcripción>}` re-ejecuta la canonicalización, recomputa cada firma por registro, valida el Merkle root y devuelve `{ valid, broken_links, checked_records, merkle_root_verified }`. El endpoint nunca devuelve 4xx para una transcripción manipulada pero bien formada: 400 se reserva para entrada malformada, y una transcripción manipulada devuelve 200 con `valid: false` y un array `broken_links` poblado. Local/offline: los SDKs incluyen `evidence.verifyProvenanceLocal(transcript)` (TypeScript) y `evidence.verify_provenance_local(transcript)` (Python). El helper offline realiza las mismas verificaciones sin round-trip de red; para ML-DSA-65 lee la clave pública de verificación desde `/v1/.well-known/provenance-keys/ml-dsa-65`. La verificación HMAC se mantiene en el servidor porque el secreto por tenant nunca se publica.

Vocabulario PROV-O e interoperabilidad legal

PROV-O es la W3C Recommendation para representar proveniencia: quién hizo qué, cuándo y sobre qué estado previo. Mapear nuestros registros a `prov:Activity`, `prov:Agent` y `prov:Entity` permite que un auditor designado por el tribunal, un regulador o un partner ingiera una transcripción con tooling estándar en lugar de un parser específico de Knogin. La forma JSON-LD mantiene todos los campos específicos de Argus (`tenant_id`, `signature_alg`, `merkle_root`, `trace_id`, `job_id`) bajo el namespace `argus:` junto a los términos PROV-O, de modo que el auditor ve primero el grafo estándar y los detalles de implementación después. Esta separación importa para la admisibilidad legal: el auditor puede razonar sobre la cadena en el lenguaje del estándar W3C y luego profundizar en los campos del namespace `argus:` cuando una pregunta probatoria requiera contexto específico de la plataforma.

Uso en procedimientos legales

La transcripción está diseñada para satisfacer un test de cadena de custodia. Cada registro nombra al actor (`actor_id`, `actor_kind` de `user`, `service`, `job` o `system`), la operación, el content hash de la evidencia en ese punto, el registro previo del que enlaza, y un `signed_at` de reloj de pared tomado de la transacción de base de datos. La transcripción lleva el mismo `trace_id` que fluye por la superficie de observabilidad de G2 y, cuando aplica, el `job_id` de G3 - así un investigador puede correlacionar una entrada de proveniencia con la actividad del sistema que la produjo. La integridad criptográfica (HMAC o ML-DSA-65) más el Merkle root significan que un solo byte alterado invalida la transcripción al verificarla. ML-DSA-65 es el algoritmo recomendado para paquetes de evidencia de alta garantía porque sus claves públicas de verificación hacen que la transcripción sea verificable de forma independiente años después de la apertura del caso, incluso si Knogin ya no es el custodio.

Aislamiento por tenant

Toda lectura filtra por `tenant_id` Y `organization_id` del bearer token. Los accesos cross-tenant devuelven 404 (nunca 403), por lo que la existencia de una cadena de proveniencia en otro tenant nunca se revela. El SecrecyLevel de la evidencia subyacente aplica: un operador por debajo del nivel de secrecía de la evidencia no puede leer su proveniencia ni siquiera dentro de su propio tenant. El secreto HMAC es por tenant (derivado por HKDF de la clave maestra de plataforma sobre la etiqueta `argus.provenance.tenant.{tenant_id}`), así que una transcripción firmada para un tenant no puede revalidarse contra otro. Ambos endpoints emiten una entrada de auditoría `provenance.read` con `resource_id = evidence_id` y `metadata = { record_count, format, algorithm, valid }`, de modo que el acto de obtener o verificar una transcripción queda registrado en la pista de auditoría.

Algoritmos soportados

HMAC-SHA256 (por defecto, tenant-scoped) y ML-DSA-65 (post-cuántico, FIPS 204; opt-in mediante ?algorithm=ml-dsa-65).

Obtener una transcripción 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"

Obtener una transcripción 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 una transcripción server-side

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

Verificación 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");

Verificación 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}")

¿Necesitas proveniencia para un workflow regulado?

Abre la referencia API para el contrato público o contacta a Knogin si necesitas que se aprovisionen claves ML-DSA-65 para un auditor externo.