Lograr el ciclo completo de cifrado y descifrado extremo a extremo (E2E) de mensajes DIDComm v2 (anoncrypt) entre dos motores distintos:
- Veramo (
@veramo/did-comm, JavaScript) - didcomm-node (binding WASM del motor de Rust
didcomm)
La meta es demostrar que un mensaje cifrado por uno de los motores puede ser
descifrado por el otro y viceversa, resolviendo los DIDs contra el resolver real
de LACChain (https://dev-identity-resolver.l-net.io).
Se cubren las dos direcciones:
| # | Secuencia | Cifra | Descifra |
|---|---|---|---|
| 1 | Veramo → didcomm-node | encrypt-veramo.mjs |
decrypt-didcomm-node.mjs |
| 2 | didcomm-node → Veramo | encrypt-didcomm-node.mjs |
decrypt-veramo.mjs |
Para la secuencia 1 (cifrar en Veramo → descifrar en didcomm-node) es
necesario aplicar un fix en el módulo didcomm.js de Veramo.
El motor didcomm-node valida el campo apv del header protegido
(apv = base64url(SHA-256(kid_del_destinatario))). La versión original de
@veramo/did-comm ignora el apv que se le pasa por options y no lo
escribe en el header, por lo que el mensaje generado por Veramo es rechazado al
desempaquetarlo con didcomm-node.
Se modifica @veramo/did-comm/build/didcomm.js para que respete el apv
recibido por options y se lo pase al encrypter:
+ if (options.apv) protectedHeader.apv = options.apv;
...
- return a256gcmAnonEncrypterX25519WithA256KW(recipient.publicKeyBytes, recipient.kid);
+ return a256gcmAnonEncrypterX25519WithA256KW(recipient.publicKeyBytes, recipient.kid, options.apv);El archivo con el fix ya aplicado está versionado en este repositorio en:
fix-veramo-didcom/@veramo/build/didcomm.js
Copiar el archivo corregido sobre el de node_modules (conviene respaldar el
original primero):
# Respaldo del original (si no existe ya)
cp node_modules/@veramo/did-comm/build/didcomm.js \
node_modules/@veramo/did-comm/build/didcomm-ori.js
# Aplicar el fix
cp fix-veramo-didcom/@veramo/build/didcomm.js \
node_modules/@veramo/did-comm/build/didcomm.jsNota:
node_modules/@veramo/did-comm/build/didcomm-ori.jses el backup del original sin modificar. Como el fix vive ennode_modules, debe reaplicarse después de cadanpm install.
- Node.js ≥ 20.12 (se usa
process.loadEnvFile()nativo para leer el.env, sin dependencias extra). Probado con Node 22. - Dependencias del proyecto instaladas:
npm install- Aplicar el fix de Veramo.
Toda la configuración está centralizada en config.mjs, que expone un objeto
agrupado por script. Cada valor se lee de una variable de entorno y, si no está
definida, usa un valor por defecto (los datos reales de prueba). Por eso los
scripts funcionan aunque el .env esté vacío.
config.mjs— carga./.env(si existe) y resuelve cada propiedad comoprocess.env.VARIABLE || valor_por_defecto..env— plantilla con todas las variables. Vienen vacías: al no tener valor, se aplican los defaults deconfig.mjs. Para sobrescribir un valor, asígnalo a la derecha del=.
🔐 Seguridad: el
.envpuede contener claves privadas. Si las defines, añade.enva tu.gitignore.
| Variable | Descripción |
|---|---|
EV_ISSUER_DID |
DID del emisor |
EV_ISSUER_PRIVATE_KEY |
Clave privada de firma (secp256k1) del emisor |
EV_ISSUER_ENCRYPTION_PRIVATE_KEY |
Clave privada de cifrado (X25519) del emisor |
EV_RECEPTOR_DID |
DID del destinatario al que se cifra |
EV_DID_RESOLVER_URL |
URL del resolver de DIDs (LACChain) |
EV_OUTPUT_FILE |
Archivo de salida del JWE |
| Variable | Descripción |
|---|---|
DDN_DID_RESOLVER_URL |
URL del resolver de DIDs (LACChain) |
DDN_RECIPIENT_DID |
DID del destinatario (quien descifra) |
DDN_RECIPIENT_ENCRYPTION_PRIVATE_KEY |
Clave privada X25519 del destinatario |
DDN_PACKED_MESSAGE_FILE |
Archivo cifrado de entrada |
| Variable | Descripción |
|---|---|
EDN_NETWORK |
URL del resolver de DIDs (¡aquí es http://…/ con barra final!) |
EDN_SENDER_WALLET_PRIVATE_KEY |
Clave privada de la wallet del emisor |
EDN_SUBJECT_DID |
DID del destinatario al que se cifra |
EDN_SENDER_PRIV_KEY_MAIL |
Clave privada X25519 del emisor (base64url) |
EDN_OUTPUT_FILE |
Archivo de salida del JWE |
| Variable | Descripción |
|---|---|
DV_PRIVATE_KEY_SUBJECT |
Clave privada del subject (auxiliar) |
DV_PRIVATE_MSG |
JWK X25519 del destinatario (JSON con x y d) |
DV_PACKED_MESSAGE_FILE |
Archivo cifrado de entrada |
DV_RECIPIENT_DID |
DID del destinatario (quien descifra) |
DV_RECIPIENT_KID_FRAGMENT |
Fragmento del kid (p. ej. #vm-1) |
| Archivo | Rol | Descripción |
|---|---|---|
config.mjs |
Config | Carga el .env y centraliza todas las constantes con valores por defecto. |
.env |
Config | Plantilla de variables de entorno (vacías → se usan los defaults). |
encrypt-veramo.mjs |
Cifrado (Veramo) | Crea un agente Veramo en memoria, resuelve el DID del receptor contra LACChain y empaqueta (anoncrypt) el mensaje. Escribe el JWE en msg-encrypted-veramo.json. |
decrypt-didcomm-node.mjs |
Descifrado (didcomm-node) | Lee msg-encrypted-veramo.json, resuelve el DID contra LACChain, normaliza el documento a JsonWebKey2020 y desempaqueta con el WASM de Rust. |
encrypt-didcomm-node.mjs |
Cifrado (didcomm-node) | Cifra (anoncrypt) con el motor WASM resolviendo el destinatario contra LACChain. Escribe el JWE en msg-encrypted-didcomm-node.json. |
decrypt-veramo.mjs |
Descifrado (Veramo) | Importa la clave del destinatario en un agente Veramo y desempaqueta msg-encrypted-didcomm-node.json. |
fix-veramo-didcom/ |
Fix | Versión corregida de @veramo/did-comm/build/didcomm.js (soporte de apv). |
Regla general: primero se ejecuta el
encrypt-*(genera el archivo) y luego eldecrypt-*correspondiente (lo lee).
# 1) Cifrar con Veramo -> genera msg-encrypted-veramo.json
node encrypt-veramo.mjs
# 2) Descifrar con didcomm-node <- lee msg-encrypted-veramo.json
node decrypt-didcomm-node.mjsSalida esperada del paso 2: el mensaje en claro {"content":"Hola mundo lnet"}.
Esta secuencia requiere el fix de Veramo descrito arriba.
# 1) Cifrar con didcomm-node -> genera msg-encrypted-didcomm-node.json
node encrypt-didcomm-node.mjs
# 2) Descifrar con Veramo <- lee msg-encrypted-didcomm-node.json
node decrypt-veramo.mjsSalida esperada del paso 2: packing: anoncrypt y la credencial verificable
(ISSUED_VERIFIABLE_CREDENTIAL).
El acoplamiento real entre los módulos es el par DID destinatario ↔ su clave privada X25519:
- La clave pública X25519 publicada on-chain para el destinatario debe derivar
de la clave privada configurada en el lado que descifra
(
x25519.getPublicKey(privada) === x_publicada_en_el_DID). - Si cambias el destinatario en un
encrypt-*, debes tener su clave privada en eldecrypt-*correspondiente; de lo contrario el ECDH no cuadra y el desempaquetado falla con "unable to decrypt …".
Los datos de prueba por defecto usan el destinatario
did:lac:openprotest:0x3c1a…, cuya clave privada (0x6c3f…) deriva exactamente
en su clave pública on-chain, garantizando la compatibilidad E2E en ambas
direcciones.