Trustvice está en beta restringida. Al registrarte o iniciar sesión, debes ingresar la contraseña de beta tester que recibiste o solicitar una en preview@trustvice.com. Gracias.
Cargando…
Ingeniería
Verificación
Tutorial
Verifica un recibo sin conexión en unas cuarenta líneas de código
Un ejemplo práctico que construye un verificador de recibos sin dependencias con WebCrypto, lo comprueba con un vector de prueba firmado, demuestra que rechaza recibos alterados y lleva el mismo procedimiento a Python y a la shell.
Por The Trustvice team··5 minutos
Empieza gratis
Trustvice 360
Declara las reglas. Que cada ejecución se compruebe.
Agentes, evidencias, gobernanza, comercio y divulgación están en Trustvice 360. Regístrate gratis y abre todas las suites desde un solo panel.
La forma más sólida de comprobar un recibo de Trustvice es con código que controlas, en una máquina que controlas, contra claves que fijaste tú. Este artículo construye ese verificador desde cero. Al final tendrás un único módulo de JavaScript sin dependencias, un vector de prueba que demuestra que funciona y una lista de casos que demuestran que falla cuando debe.
Node 20 o posterior, o cualquier runtime cuyo crypto.subtle admita Ed25519: los navegadores actuales, Deno, Bun y Cloudflare Workers sirven. No hay paquete que instalar. Tampoco necesitas una cuenta de Trustvice: el vector de prueba de abajo está firmado con una clave desechable generada para este artículo.
Guarda esto como receipt.json. Es un recibo de prueba firmado con esa clave desechable. Su body es solo ilustrativo; lo que importa es que está firmado de verdad.
Para recibos reales sustituirás keys.json por el conjunto publicado en https://trustvice.com/.well-known/receipt-keys.json, idealmente una copia que hayas fijado. El procedimiento no cambia.
La firma cubre la forma canónica RFC 8785 del recibo sin su miembro signature. En JavaScript, JSON.stringify ya produce la forma numérica de ECMAScript y un escapado mínimo de cadenas. Solo falta ordenar las claves de los objetos, de forma recursiva, por unidad de código UTF-16, que es el orden por defecto de Array.prototype.sort:
js
function canonicalize(value) {
if (Array.isArray(value)) return value.map(canonicalize);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]),
);
}
return value;
}
exportconst jcs = (value) => JSON.stringify(canonicalize(value));
Para el vector de prueba, la cadena canónica es una sola línea sin espacios:
Paso 4: comprueba la forma, busca la clave, verifica#
Ahora, el verificador en sí. Nunca lanza excepciones ante entradas hostiles; devuelve uno de tres motivos, los mismos que usan la página de verificación y la API.
Fíjate en la línea de desestructuración. Quita signature y conserva todo lo demás, incluidos miembros que este código no conoce. Es intencionado: todo miembro salvo signature está firmado, así que un verificador que descarte un miembro que no reconoce rechazaría recibos válidos.
Junta los tres fragmentos en verify-receipt.mjs. Ese es todo el verificador.
Un pequeño wrapper de línea de comandos lee los archivos y sale con 0 si el recibo es válido y con 1 si no. También acepta el envoltorio { "receipt": … } que devuelve la API:
Un verificador que solo dice «válido» es peor que no tener ninguno. Edita receipt.json y ejecuta cada caso:
Cambio
Resultado esperado
byteLength de 4 a 5
bad_signature
Cualquier carácter de issuedAt o receiptId
bad_signature
Agregar "note": "x" en el nivel superior
bad_signature, porque todo miembro salvo está firmado
Conserva estos casos como tests automáticos junto a tu verificador. Son baratos y detectan los errores que más vemos en los ports.
Opcional: comprueba la propia entrada de la clave#
El kid se deriva de la clave: es el base64url de los primeros 16 bytes del SHA-256 de los 32 bytes en bruto de x. Para la clave de prueba sale LfPAIisXFvNmFhD6vBQbBw. Recalcularlo confirma que la entrada de tu archivo de claves es coherente:
js
exportasyncfunction keyIdOf(x) {
const digest = await crypto.subtle.digest("SHA-256", base64urlToBytes(x));
let bin = "";
for (const b ofnew Uint8Array(digest).slice(0, 16)) bin += String.fromCharCode(b);
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
El procedimiento es el mismo en todas partes, pero dos lenguajes requieren cuidado.
Python.json.dumps no es JCS: imprime los floats enteros como 1.0, rellena los exponentes y escapa por defecto lo que no es ASCII. Una versión en Python necesita un pequeño serializador RFC 8785 que ordene las claves por unidad de código UTF-16, y puede verificar con Ed25519PublicKey del paquete cryptography.
Shell. Con jq y OpenSSL 3, jq -S -c 'del(.signature)' produce los bytes canónicos de los recibos, porque sus bodies solo contienen cadenas, enteros, booleanos, null, arrays y objetos. No es una implementación general de JCS, pero con un recibo o coincide o falla; nunca da por bueno uno que no debe. La clave pública se envuelve como SubjectPublicKeyInfo DER anteponiendo un encabezado fijo de 12 bytes a los 32 bytes en bruto, y openssl pkeyutl -verify -rawin hace el resto. El LibreSSL que trae macOS no puede hacerlo; instala OpenSSL 3.
Una firma válida prueba origen, integridad y una hora firmada: quien tiene una clave de firma de Trustvice firmó exactamente estos bytes, nada ha cambiado desde entonces y Trustvice afirma la hora de issuedAt. No prueba que un hecho que aportaste sea cierto, ni contiene tu contenido. Para demostrar que un documento coincide con un recibo, calcula tú el hash del documento y compáralo. Tratamos esos límites en Qué prueba un recibo y qué no.
Para una comprobación rápida sin escribir código, pega cualquier recibo en /verify. Ejecuta el mismo procedimiento en tu navegador y nunca nos envía el recibo.