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.
Dentro del formato de recibo: JSON canónico, Ed25519 y claves publicadas
Un recorrido campo a campo por el sobre del recibo de Trustvice: por qué firmamos una forma canónica RFC 8785 y no los bytes que viajan, cómo se derivan los IDs de clave de las propias claves y qué promete el conjunto de claves publicado.
Por The Trustvice team··6 minutos
Un recibo tiene una sola tarea: que cualquiera pueda comprobarlo, dentro de años, con código que no escribimos nosotros. Ese requisito marcó cada decisión del formato. Este artículo recorre el sobre, la canonicalización, la firma y el conjunto de claves, y explica el porqué de cada elección.
Los recibos en este formato comparten un sobre; solo cambia el body de un tipo de recibo a otro. Los registros de evidencia firmada, como un Proof-of-Erasure, usan un sobre hermano, con subject y claims en lugar de body, bajo la misma canonicalización, firma y conjunto de claves que se describen abajo.
Siempre "1". La versión del formato de recibo, no de la API.
receiptId
Un ULID de 26 caracteres, ordenable por hora de creación.
issuedAt
Instante ISO 8601 en UTC con milisegundos, fijado por Trustvice al firmar.
type
El tipo de recibo. body.type siempre es igual.
signatureKeyId
El kid de la clave de firma en el conjunto publicado: 22 caracteres de base64url.
body
Los hechos propios del tipo.
signature
La firma Ed25519, en base64url sin relleno: 86 caracteres.
De cómo se construye la firma se siguen dos propiedades, y ambas importan a quien escriba un verificador.
Todo miembro salvo signature está firmado. Eso incluye cualquier miembro que agregue una versión futura. Un verificador no debe descartar miembros que no reconoce antes de comprobar, o fallarán recibos válidos. Los campos opcionales nuevos son aditivos dentro de la versión 1, así que los lectores deben ignorar lo que no conocen, pero conservarlo al verificar.
El recibo no nombra tu workspace ni tu cuenta. El workspace es el ámbito en el que listas y obtienes recibos, pero no se escribe en el sobre firmado. Un recibo es una declaración autónoma sobre un hecho, no una prueba de quién la pidió.
Un ID de recibo es un ULID: 26 caracteres de base32 de Crockford. Los diez primeros son un sello de tiempo de 48 bits en milisegundos y los dieciséis restantes son aleatorios. Por eso los IDs se ordenan por hora de creación.
La hora dentro del ID se genera a unos milisegundos de issuedAt. Trata issuedAt como la hora firmada y el ID como un identificador.
Una firma cubre bytes, no significado. JSON admite muchas grafías a nivel de byte para un mismo valor: claves en cualquier orden, espacios opcionales, 4 o 4.0, "é" o "\u00e9". Si firmáramos los bytes que enviamos, un proxy que reindente la respuesta, o una biblioteca que reordene las claves al guardarla, rompería un recibo perfectamente válido.
Por eso la firma cubre una forma canónica, definida por RFC 8785, el JSON Canonicalization Scheme (JCS). Toda implementación correcta convierte el mismo valor parseado en los mismos bytes. Para un recibo, las reglas se reducen a cinco:
Las claves se ordenan en cada nivel de anidamiento, por unidad de código UTF-16.
Sin espacios entre tokens.
Los números usan la conversión de número a cadena de ECMAScript: los enteros se imprimen sin punto decimal ni exponente. Los bodies de los recibos solo contienen enteros.
Las cadenas usan el escapado JSON mínimo; los caracteres no ASCII se emiten como UTF-8 en bruto, nunca como \uXXXX, y la barra no se escapa.
Los arrays conservan su orden, y los valores null y false se mantienen, no se descartan.
El mensaje firmado es la forma JCS del sobre sin el miembro signature, codificada en UTF-8. En el lado de la firma, los valores que no pueden representarse igual en todas partes, como NaN, los infinitos, undefined o bigint, se rechazan en lugar de convertirse.
La consecuencia práctica para ti: puedes formatear un recibo para leerlo, guardarlo en una base de datos que reordene las claves o pasarlo por cualquier herramienta JSON que conserve los valores, y sigue verificándose. Lo que no puedes hacer es cambiar un valor, quitar un miembro null o redondear un número. Esos son justo los cambios que un verificador debe detectar.
La firma es Ed25519 puro según RFC 8032: sin prehash y sin cadena de contexto. Lo elegimos porque es pequeño, rápido, determinista y está en las bibliotecas estándar que ya tiene la gente. Node 20 o posterior, los navegadores actuales, Deno, Bun y Cloudflare Workers lo verifican solo con WebCrypto; Python, con el paquete cryptography; OpenSSL 3, desde la shell. Un verificador de recibos de Trustvice nunca debería necesitar una biblioteca de Trustvice.
Una firma ocupa 64 bytes, que son los 86 caracteres de base64url del sobre. Una clave pública ocupa 32 bytes.
Trustvice guarda las claves privadas en módulos de seguridad de hardware de AWS KMS, una clave por región de datos, y nunca salen de ellos. El firmante envía los bytes canónicos a KMS y recibe una firma; puede firmar, pero no exportar una clave.
El signatureKeyId de un recibo coincide con el kid de una clave del conjunto publicado. Ese kid no es una etiqueta que elegimos. Se calcula a partir de la clave:
text
kid = base64url( first 16 bytes of SHA-256( the 32 raw bytes of x ) )
sin relleno, lo que da 22 caracteres. De ahí se siguen dos consecuencias útiles.
Primero, una entrada del conjunto de claves se comprueba a sí misma: cualquiera puede recalcular el kid a partir de x, y una entrada en la que no coinciden no es válida. En JavaScript:
Segundo, como el signatureKeyId está dentro de los bytes firmados, no se puede hacer que un recibo apunte a otra clave sin invalidarlo. Y como el kid se deriva de x, un x distinto para un kid que ya tienes no puede ser legítimo.
Las claves públicas forman un JSON Web Key Set en https://trustvice.com/.well-known/receipt-keys.json, que redirige con HTTP 308 a https://trustvice.com/api/v1/receipts/keys. Se sirve como JSON, cacheable durante una hora y con Access-Control-Allow-Origin: *, así que una página de tu propio dominio puede descargarlo y verificar recibos en el navegador.
Para verificar solo necesitas kid, kty, crv y x. El conjunto puede contener varias claves, y las claves retiradas siguen publicadas. Los recibos de un mismo workspace pueden llevar IDs de clave distintos con el tiempo, así que busca siempre la clave para cada recibo.
El conjunto se construye a partir de configuración publicada, de modo que verificar nunca depende de una llamada a AWS. En él solo aparecen campos públicos.
Por defecto, un verificador confía en lo que devuelva el endpoint de claves en el momento de preguntar, lo que es tan sólido como esa conexión TLS y nada más. Para pruebas que importan, fija el conjunto: descárgalo una vez desde una red de confianza, recalcula cada kid a partir de su x, guarda los pares en un lugar que solo cambie tras revisión y verifica contra ese archivo. Actualízalo con calendario y revisa las claves nuevas antes de agregarlas.
Guarda el conjunto de claves que usaste junto a cualquier recibo que conserves como prueba. Un recibo más el conjunto de claves con que se verificó se comprueba por sí solo.
Un recibo nombra la clave que lo firmó, así que agregar una clave al conjunto nunca afecta a recibos anteriores. Hoy no hay rotación automática y las claves no caducan. La rotación es una ceremonia manual, fuera de banda, y el conjunto no lleva ninguna marca de retirada o revocación. La rotación es aditiva: una clave retirada sigue publicada para que los recibos que firmó sigan siendo verificables. Es una política, no algo que imponga el endpoint de claves, y es un motivo más para guardar tu propia copia fijada.
Tampoco hay lista de revocación. Si una clave de firma llegara a comprometerse, el remedio sería dejar de publicarla, con lo que todo recibo firmado con ella fallaría con unknown_key, también los auténticos. Guardar tu propia copia del conjunto de claves del momento en que aceptaste un recibo, y registrar el recibo en un sistema que controlas, mantiene tus pruebas útiles pase lo que pase.