Hashing y firma de solicitudes
La seguridad del Ledger de Kamin se basa en criptografía asimétrica. Este enfoque se basa en pares de llaves, una llave pública y una llave privada, para firmar los mensajes que intercambia el sistema. Los mensajes se firman primero generando un hash del contenido del mensaje. Las llaves privadas deben mantenerse en secreto y nunca deben compartirse con nadie. Solo la llave pública se comparte con el Ledger mediante la creación de un registro llamado Firmante. Esto permite que cualquier persona valide los mensajes recibidos, pero solo el propietario de la llave privada puede firmar nuevos mensajes.
El algoritmo de firma digital principal utilizado por el ledger es Ed25519.
Para saber más sobre este algoritmo, consulta Un análisis profundo de las firmas Ed25519.
Todas las solicitudes que intentan mutar datos del Ledger deben estar firmadas y con hash antes de enviarse.
Si una solicitud no está firmada, el Ledger no la aceptará.
Cada operación debe estar firmada por una llave privada con los permisos adecuados.
Debemos tener una forma determinista de generar hashes y firmas para poder verificarlas desde otros dispositivos.
Hash
La primera parte del proceso consiste en generar representaciones de cadena estables del contenido para siempre obtener el mismo hash ante la misma entrada.
Como estamos usando JSON como medio de transporte, tiene sentido usarlo también para el hashing.
JSON no es un formato determinista, por lo que los serializadores JSON de diferentes lenguajes no necesariamente producen la misma salida para la misma entrada. Para resolver esto, el ledger utiliza una estandarización de serialización JSON descrita en RFC 8785. Este documento describe las reglas de serialización y proporciona implementaciones de referencia para varios lenguajes.
Muchos serializadores existentes ya cumplen con las reglas del RFC, pero la regla más importante que normalmente debe implementarse manualmente es el orden alfabético de las propiedades de los objetos JSON.
Algunos serializadores implementados en lenguajes estáticos imprimen las propiedades en el orden en que se definen en las clases. Por lo tanto, una forma sencilla de cumplir este requerimiento es ordenar alfabéticamente las propiedades en tus objetos tipados.
Ejemplo de serialización de payload en NodeJS:
import stringify from 'safe-stable-stringify';
export function serializeData(data: any): string {
return stringify(data);
}Aquí usamos un paquete de terceros llamado safe-stable-stringify, que es
compatible con las reglas del RFC
8785. Hay muchos paquetes
similares disponibles en npm.
Una vez que tenemos los datos serializados canónicamente, podemos aplicar el hash.
Para el hash usamos el algoritmo SHA-256
y generamos el valor final siguiendo estos pasos:
- Serializar los datos de entrada usando un serializador compatible con RFC 8785
- Aplicar el algoritmo
SHA-256y codificar como cadenahex
Ejemplo en NodeJS:
import crypto from 'crypto';
const HASHING_ALGORITHM = 'sha256';
export function createHash(data: any): string {
const serializedData = serializeData(data);
return crypto
.createHash(HASHING_ALGORITHM)
.update(serializedData)
.digest('hex');
}La función serializeData utilizada aquí es la función de serialización del
ejemplo anterior.
Resúmenes de firma (digest)
Ledger también admite adjuntar datos adicionales al firmar objetos. Esto se puede hacer incluyendo datos en la propiedad custom del objeto de firma.
Firmar solo el hash del payliad principal, como el que generamos en el capítulo anterior, no nos permitiría garantizar la integridad de estos datos adicionales
incluidos en las firmas. Por eso se calcula un resumen de firma (digest) adicional en estas situaciones.
Para incluir datos adicionales en el hash, utilizamos un algoritmo de doble hash. El doble hash tiene algunos otros beneficios además
de la capacidad de extender el hash con datos adicionales. El más importante es que previene ciertos ataques criptográficos. Por eso
se utiliza el resumen de firma (digest) para todas las firmas, independientemente de si incluyen datos personalizados.
Los pasos para calcular un resumen de firma son los siguientes:
- Crear un hash del payload principal siguiendo los pasos del capítulo anterior.
- Serializar los datos adicionales (
custom) que se añaden a la firma utilizando el algoritmo de serialización del capítulo anterior; usar una cadena vacía sicustomno existe. - Realizar otro hash
SHA-256concatenando el hash del paso 1 con los datos personalizados serializados de la firma:sha256(dataHash + serializedCustomData)y devolver este hash como una cadena codificada enhex. - Usar el hash del paso 3 como el resumen de firma.
Este objeto custom va en las proof.custom, no debe ser confundido con el
custom del objeto data, en caso que no se tenga custom, el serializado
de la firma será: sha256(dataHash)
Aquí hay un ejemplo de implementación en Node.js:
export function createSignatureDigest(
dataHash: string,
signatureCustom?: Record<string, any>
): string {
// Serialize the custom data, if it exists
const serializedCustomData = signatureCustom
? serializeData(signatureCustom)
: '';
// Create a hash by concatenating the data hash
// with serialized custom data
return crypto
.createHash(HASHING_ALGORITHM)
.update(dataHash + serializedCustomData)
.digest('hex');
}Firmado
Ahora podemos firmar las solicitudes al ledger firmando el digest previamente calculado
con nuestra llave privada usando una implementación compatible con ed25519.
En NodeJS, esto puede lograrse con el paquete estándar crypto:
import crypto from 'crypto';
const digestBuffer = Buffer.from(digest, 'hex');
// Esto asume que tienes una llave privada en formato DER,
// puede que necesites convertir tu llave si no es el caso
const key = crypto.createPrivateKey({
format: 'der',
type: 'pkcs8',
key: keyDer,
});
// El primer argumento debe ser `undefined` para ed25519.
// Aunque normalmente se define un algoritmo de hash, ed25519 ya usa sha512 internamente.
// La firma y verificación con ed25519 no funcionarán correctamente si se pasa un valor aquí.
// También, muchos ejemplos de crypto usan createSign(algoritmo) para crear una clase Sign,
// pero esto no funciona con ed25519.
// ver: https://github.com/mscdex/io.js/commit/7d0e50dcfef98ca56715adf74678bcaf4aa08796
const result = crypto.sign(undefined, digestBuffer, key).toString('base64');Historial de cambios
- Cambiado• Se aclaró la serializacion del serializedCustomData el cual no es requerido en todos los casos
- Agregado• Versión inicial