Reportes firmados
Verificar la firma con /v1/verify y offline con la clave pública.
Un reporte aprobado trae siempre su manifiesto firmado. El contrato lo impone: aprobado sin firma no es evidencia verificable, y publicarlo como si lo fuera es peor que no tener firma.
Fuente del diagrama (mermaid)
sequenceDiagram
participant C as Cliente
participant S as SBOX
C->>S: GET /v1/reports/{id}
S-->>C: 200 · signature { manifestId, algorithm: Ed25519, publicKeyId }
C->>S: POST /v1/verify
S-->>C: 200 · verified_ed25519_offline
Note over C: o verificación offline con la clave pública publicada
curl -sS "$SBOX/v1/reports" -H "authorization: Bearer $TOKEN" | jq '.data[] | select(.signature != null)'Verificación offline
La firma es Ed25519 sobre el payload canonicalizado
(json-stable-stringify-v1). No hace falta red, base de datos ni runtime: con
la clave pública publicada alcanza. Eso es lo que hace que el reporte siga
siendo verificable cuando Ucotron no está del otro lado.
Verificar un PDF de peritaje
Los tres productos —rayos, edificación y peritaje— firman igual, y el PDF trae todo lo necesario en su bloque de procedencia al pie: el hash de contenido, la firma, el identificador de la clave y la URL donde está publicada.
El directorio de claves es público y no pide credenciales. Es deliberado: quien tiene que poder comprobar un peritaje es un perito de contraparte, un tribunal o un reasegurador, y ninguno de ellos tiene cuenta con nosotros. Un documento cuya verificación exige credenciales del emisor no es evidencia.
curl -s https://app.ucotron.com/v1/verify/keysDevuelve cada clave con su ventana de vigencia. Las retiradas se siguen
publicando, con su retiredAt: una firma hecha mientras la clave estaba
vigente sigue siendo válida, y borrarla dejaría sin verificar todo lo que se
emitió en ese período.
Con eso, la comprobación es de una línea y no habla con nosotros:
import { createPublicKey, verify } from "node:crypto";
// Los tres valores salen del bloque de procedencia impreso en el PDF.
const contentHash = "…"; // 64 caracteres hexadecimales
const signature = "…"; // base64url
const publicKey = "…"; // el campo `publicKey` de la clave que el PDF nombra
const valido = verify(
null,
Buffer.from(contentHash), // se firma el hash en ASCII, tal cual
createPublicKey({
key: Buffer.from(publicKey, "base64url"),
format: "der",
type: "spki",
}),
Buffer.from(signature, "base64url"),
);Lo que esto prueba y lo que no: prueba que el documento lo emitimos nosotros y que su contenido no cambió desde entonces. No prueba que el análisis sea correcto — para eso está la sección de limitaciones de cada informe, y la procedencia que dice sobre qué escenas se calculó.
Por qué no es un HMAC
Un HMAC lo verifica quien tiene el secreto con el que se hizo, es decir quien puede fabricarlo. Sirve para detectar corrupción, no para probar autoría ante un tercero, que es lo único que le importa a un expediente. Con Ed25519 la privada firma y la pública verifica, y esa asimetría es toda la diferencia: entregamos la clave a quien la pida sin habilitar a nadie a firmar en nuestro nombre.
La privada vive en un custodio de secretos y nunca sale de ahí — no está en el repositorio, ni en una variable de entorno, ni en el bundle del navegador. Y el emisor se niega a firmar si su clave no figura en el directorio público: un documento firmado con una clave que nadie puede verificar sería el peor error posible, porque se descubre recién cuando alguien audita.
Lo que se rompe
Un manifiesto con clave o firma corrupta devuelve "no verifica", no un 500. Un 500 ante entrada hostil le confirma al atacante que llegó a tocar el motor y le niega al integrador honesto la única respuesta que le sirve.
Un manifiesto alterado devuelve 422 con invalid_ed25519_signature, incluso
si el resto del documento está intacto.
