Signed reports
Verify the signature with /v1/verify and offline with the public key.
An approved report always carries its signed manifest. The contract enforces it: approved without a signature is not verifiable evidence, and publishing it as if it were is worse than having no signature at all.
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)'Offline verification
The signature is Ed25519 over the canonicalized payload
(json-stable-stringify-v1). No network, database or runtime needed: the
published public key is enough. That is what keeps the report verifiable when
Ucotron is not on the other side.
Verifying a damage assessment PDF
All three products — lightning, buildings and damage — sign the same way, and the PDF carries everything you need in its provenance block at the foot: the content hash, the signature, the key identifier and the URL where that key is published.
The key directory is public and asks for no credentials. That is deliberate: the people who need to check an assessment are an opposing expert, a court or a reinsurer, and none of them has an account with us. A document whose verification requires the issuer's credentials is not evidence.
curl -s https://app.ucotron.com/v1/verify/keysIt returns every key with its validity window. Retired keys stay published,
carrying their retiredAt: a signature made while the key was active remains
valid, and removing it would strand everything issued in that period.
With that, the check is one call and it does not talk to us:
import { createPublicKey, verify } from "node:crypto";
// All three values come from the provenance block printed in the PDF.
const contentHash = "…"; // 64 hex characters
const signature = "…"; // base64url
const publicKey = "…"; // the `publicKey` field of the key the PDF names
const valid = verify(
null,
Buffer.from(contentHash), // the hash is signed as ASCII, as printed
createPublicKey({
key: Buffer.from(publicKey, "base64url"),
format: "der",
type: "spki",
}),
Buffer.from(signature, "base64url"),
);What this proves and what it does not: it proves we issued the document and that its content has not changed since. It does not prove the analysis is correct — that is what each report's limitations section is for, along with the provenance stating which scenes it was computed over.
Why not an HMAC
An HMAC is verified by whoever holds the secret it was made with — that is, by whoever could forge it. It detects corruption; it does not prove authorship to a third party, which is the only thing a case file cares about. With Ed25519 the private key signs and the public key verifies, and that asymmetry is the whole point: we hand the key to anyone who asks without enabling anyone to sign in our name.
The private key lives in a secret custodian and never leaves it — not in the repository, not in an environment variable, not in the browser bundle. And the issuer refuses to sign if its key is not in the public directory: a document signed with a key nobody can verify would be the worst possible failure, because it surfaces only when someone audits.
What breaks
A manifest with a corrupt key or signature returns "does not verify", not a 500. A 500 on hostile input confirms to the attacker that they reached the engine, and denies the honest integrator the only answer that helps them.
A tampered manifest returns 422 with invalid_ed25519_signature, even if
the rest of the document is intact.
