@dcp-ai/wasm — SDK WebAssembly v2.0¶
Traducción informativa del README en inglés, actualizada el 02/11/2026. Los identificadores y los bloques de código, incluidos sus comentarios, se conservan para mantener los ejemplos probados.
Módulo WebAssembly para el Digital Citizenship Protocol (DCP) v2.0, compilado a partir del SDK de Rust. Ofrece firmas compuestas poscuánticas, generación de claves híbridas, encapsulación de claves ML-KEM-768, hashes duales, construcción y verificación de bundles y cálculo del nivel de seguridad. Se ejecuta directamente en el navegador o en Node.js, sin servidor.
Instalación¶
Publicado en npm (26/10/2026). @dcp-ai/wasm 2.1.0 se publicó como latest en esa fecha e incluía las API descritas aquí, incluidas las experimentales de dcp-jcs-v2. Publicarlas no activó el candidato: siguió siendo opt-in y no normativo, sin cambios en dcp-jcs-v1. El crate de Rust tiene un ciclo de publicación independiente.
@dcp-ai/wasm 2.1.1. Esta versión también verifica los campos secundarios SHA3-256 de VERIFICATION.md V2-11 cuando están presentes: audit_merkle_root_secondary, cada prev_hash_secondary y cada intent_hash_secondary. Los campos ausentes no se exigen, así que un bundle verificado puede no tener una cadena secundaria completa; un campo presente que sea null, no sea una cadena o sea incoherente se rechaza. No cambian las API, los campos de resultado, dcp-jcs-v1 ni el tratamiento de hash_alg. La versión 2.1.0 no comprueba estos campos. Los comandos de instalación siguientes indican esta versión; para su disponibilidad en el registro, véase Versiones publicadas.
Consulta el inventario de publicaciones npm para ver todas las versiones.
@dcp-ai/wasm 2.1.2 (preparada, no publicada). Corrige dualMerkleRoot, que en 2.1.1 y anteriores da raíces distintas de las de V2-11 con dos o más hojas (véase Operaciones de hash más abajo). No está en npm: el comando de arriba sigue en 2.1.1. Véanse los paquetes npm.
Para desarrollar con una copia del repositorio, compila e instala el paquete local:
Compilación¶
# Build WASM + TypeScript wrapper
npm run build
# WASM only (browser target)
npm run build:wasm
# WASM only (Node.js target)
npm run build:wasm:node
Requisitos: wasm-pack y la cadena de herramientas de Rust con el destino wasm32-unknown-unknown.
Inicialización: navegador y Node.js¶
initDcp(wasmUrl?) carga dcp_ai_bg.wasm una sola vez.
- Navegador (o un empaquetador que sirva
pkg/):await initDcp()obtiene el archivo situado junto apkg/dcp_ai.js. También puedes pasar su URL:await initDcp('/assets/dcp_ai_bg.wasm'). - Node.js: su implementación de
fetchno puede leer una URLfile:, por lo queinitDcp()sin argumentos falla en ese entorno. Lee el archivo y pásalo como URLdata::
import { readFileSync } from 'node:fs';
import { initDcp } from '@dcp-ai/wasm';
// The .wasm file sits next to the `@dcp-ai/wasm/pkg` entry point.
const wasmFile = new URL('dcp_ai_bg.wasm', import.meta.resolve('@dcp-ai/wasm/pkg'));
const dcp = await initDcp(`data:application/wasm;base64,${readFileSync(wasmFile).toString('base64')}`);
wasm-bindgen puede advertir que esta forma del parámetro está obsoleta; aun así, inicializa el módulo.
Inicio rápido — Envoltorio de TypeScript¶
Se recomienda utilizar el SDK mediante su envoltorio de TypeScript. En este bundle V2, el principal firma el RPR y el manifiesto; el agente firma el pasaporte, la intención y la decisión de política. Cada uno utiliza claves híbridas Ed25519 + ML-DSA-65. El ejemplo se ejecuta en Node.js; en un navegador, sustituye la inicialización por await initDcp().
import { readFileSync } from 'node:fs';
import { initDcp } from '@dcp-ai/wasm';
const wasmFile = new URL('dcp_ai_bg.wasm', import.meta.resolve('@dcp-ai/wasm/pkg'));
const dcp = await initDcp(`data:application/wasm;base64,${readFileSync(wasmFile).toString('base64')}`);
const principal = dcp.generateHybridKeypair();
const agent = dcp.generateHybridKeypair();
type Hybrid = ReturnType<typeof dcp.generateHybridKeypair>;
const keyEntries = (keys: Hybrid) => [keys.classical, keys.pq].map((k) => ({
kid: k.kid ?? dcp.deriveKid(k.alg, k.public_key_b64), alg: k.alg, public_key_b64: k.public_key_b64,
created_at: '2026-01-01T00:00:00Z', expires_at: null, status: 'active',
}));
const sign = (context: string, payload: object, keys: Hybrid) =>
dcp.signPayload(
context, payload,
keys.classical.secret_key_b64, keys.classical.kid ?? dcp.deriveKid('ed25519', keys.classical.public_key_b64),
keys.pq.secret_key_b64, keys.pq.kid ?? dcp.deriveKid('ml-dsa-65', keys.pq.public_key_b64),
);
const nonce = dcp.generateSessionNonce();
const now = new Date().toISOString();
const intent = {
dcp_version: '2.0', intent_id: 'intent:example-001', session_nonce: nonce, agent_id: 'agent:example-001',
human_id: 'human:example-001', timestamp: now, action_type: 'browse', target: { channel: 'web' },
data_classes: ['none'], estimated_impact: 'low', requires_consent: false,
};
const bundle = dcp.buildBundle({
rpr: sign('DCP-AI.v2.ResponsiblePrincipal', {
dcp_version: '2.0', human_id: 'human:example-001', session_nonce: nonce, legal_name: 'Example Principal',
entity_type: 'natural_person', jurisdiction: 'ES', liability_mode: 'owner_responsible', override_rights: true,
issued_at: now, expires_at: null, contact: null, binding_keys: keyEntries(principal),
}, principal),
passport: sign('DCP-AI.v2.AgentPassport', {
dcp_version: '2.0', agent_id: 'agent:example-001', session_nonce: nonce, keys: keyEntries(agent),
principal_binding_reference: 'human:example-001', capabilities: ['browse'], risk_tier: 'low',
created_at: now, status: 'active',
}, agent),
intent: sign('DCP-AI.v2.Intent', intent, agent),
policy: sign('DCP-AI.v2.PolicyDecision', {
dcp_version: '2.0', intent_id: intent.intent_id, session_nonce: nonce, decision: 'approve', risk_score: 50,
reasons: ['low-impact browse'], required_confirmation: null,
applied_policy_hash: dcp.preparePayload({ policy: 'default' }).payload_hash, timestamp: now,
}, agent),
auditEntries: [{
dcp_version: '2.0', audit_id: 'audit:example-001', session_nonce: nonce, prev_hash: 'GENESIS', hash_alg: 'sha256',
timestamp: now, agent_id: 'agent:example-001', human_id: 'human:example-001', intent_id: intent.intent_id,
intent_hash: dcp.preparePayload(intent).payload_hash, policy_decision: 'approved', outcome: 'page_read',
evidence: { tool: 'browser', result_ref: null, evidence_hash: null }, pq_checkpoint_ref: null,
}],
sessionNonce: nonce,
});
// The principal signs the manifest with a composite signature.
const signed = dcp.signBundle(
bundle,
principal.classical.secret_key_b64, principal.classical.kid ?? dcp.deriveKid('ed25519', principal.classical.public_key_b64),
principal.pq.secret_key_b64, principal.pq.kid ?? dcp.deriveKid('ml-dsa-65', principal.pq.public_key_b64),
);
const result = dcp.verifyBundle(signed);
console.log(result.verified, result.classical_valid, result.pq_valid); // true true true
Referencia de la API¶
Inicialización¶
initDcp(wasmUrl?: string): Promise<DcpWasm>¶
Inicializa el módulo WASM. Debe llamarse una vez antes de utilizar cualquier API. Opcionalmente acepta la URL del archivo .wasm; en Node.js, pásala como URL data: (véase Inicialización).
Generación de pares de claves¶
| Método | Devuelve | Descripción |
|---|---|---|
generateEd25519Keypair() |
KeypairResult |
Par de claves clásicas Ed25519 |
generateMlDsa65Keypair() |
KeypairResult |
Par de claves de firma poscuántica ML-DSA-65 |
generateSlhDsa192fKeypair() |
KeypairResult |
Par de claves de firma SLH-DSA-192f basada en hashes y sin estado |
generateHybridKeypair() |
HybridKeypairResult |
Par de claves híbridas Ed25519 + ML-DSA-65 en una sola llamada |
Encapsulación de claves ML-KEM-768¶
| Método | Devuelve | Descripción |
|---|---|---|
mlKem768Keygen() |
KemKeypairResult |
Genera un par de claves de encapsulación y desencapsulación ML-KEM-768 |
mlKem768Encapsulate(pk) |
KemEncapsulateResult |
Encapsula un secreto compartido mediante una clave pública |
mlKem768Decapsulate(ct, sk) |
string |
Desencapsula el secreto compartido a partir del texto cifrado; devuelve hexadecimal |
Firma compuesta¶
| Método | Descripción |
|---|---|
compositeSign(context, payload, classicalSk, classicalKid, pqSk, pqKid) |
Firma híbrida completa Ed25519 + ML-DSA-65 con enlace pq_over_classical |
classicalOnlySign(context, payload, sk, kid) |
Firma únicamente clásica Ed25519, para el modo de transición |
signPayload(context, payload, classicalSk, classicalKid, pqSk, pqKid) |
Firma y envuelve el contenido en un sobre SignedPayload |
Verificación¶
La verificación no otorga confianza en el principal, admisión ni autorización para actuar. El verificador V2 comprueba la cadena de auditoría primaria y la raíz Merkle primaria; desde la 2.1.1 también comprueba los enlaces secundarios opcionales, los hashes de intención y audit_merkle_root_secondary que estén presentes, mientras que la 2.1.0 no (cadenas de doble hash). El candidato experimental reutiliza estas comprobaciones.
| Método | Devuelve | Descripción |
|---|---|---|
compositeVerify(context, payload, sig, classicalPk, pqPk?) |
CompositeVerifyResult |
Verificación criptográfica de una firma compuesta |
verifyBundle(signedBundle, policy?) |
V2VerificationResult |
Verifica un bundle V2 —estructura, criptografía y cadena de hashes primaria— con el más estricto de los modos de la política y del nivel de seguridad (spec §14.3); informa resolved_tier y verification_mode. policy solo puede diferir de los valores predeterminados en default_mode, risk_overrides, los campos de la fase 3 (advisory_rejected_algs, warn_classical_only_deprecated, allow_classical_fallback_disable), max_session_duration_seconds (medida desde el session_issued_at firmado, que signBundle escribe cuando el manifiesto contiene session_expires_at), require_session_expiry (rechaza un manifiesto sin session_expires_at) y require_audience_binding con verifier_id (rechaza un manifiesto sin intended_verifier o con un valor distinto de verifier_id). Cuando se proporciona una política, se rechazan los bundles V1 |
Validación de esquemas (V2-2; en main, no en una versión publicada). verifyBundle y wasm_verify_signed_bundle_v2 validan primero el sobre del bundle firmado y los cuatro payloads contra schemas/v2, después de las comprobaciones de política y reloj y antes de que el verificador compruebe el manifiesto, los hashes de los payloads, las claves y las firmas; las exportaciones experimentales del candidato pasan por la misma puerta. Un bundle que falla devuelve verified: false con errores schema: <JSON pointer> <keyword>[ <member>] (como mucho 16, y luego schema: further errors not listed) y sin detalles criptográficos. Los miembros desconocidos de los objetos cerrados se rechazan; en los objetos abiertos, como el target del intent, los miembros declarados conservan sus restricciones y solo los adicionales quedan sin inspeccionar. session_issued_at y session_expires_at del manifiesto deben ser cadenas cuando están presentes, no null. date-time sigue la comprobación de ajv-formats 3.0.1 del verificador de referencia, que no es RFC 3339 estricto. El error de parseo de la política, JSON parse error y la detección de versión (Unknown DCP version) del binding siguen yendo antes de V2-2. Cambio de aceptación: los bundles inválidos por esquema que antes verificaban ahora se rechazan. No hay ninguna API solo de esquema. Véase el mapa de cobertura.
Operaciones de hash¶
| Método | Devuelve | Descripción |
|---|---|---|
dualHash(data) |
DualHash |
Hash dual SHA-256 + SHA3-256 |
sha3_256(data) |
string |
Hash SHA3-256 en hexadecimal |
hashObject(obj) |
string |
Hash SHA-256 de un objeto JSON |
dualMerkleRoot(leaves) |
DualHash |
Raíz Merkle dual de VERIFICATION.md V2-11 a partir de un arreglo no vacío de hojas DualHash, cada una un digest hex de 32 bytes; lanza una excepción con un arreglo vacío o con una hoja no válida. La raíz va en hex en minúsculas. 2.1.2 preparada, sin publicar: 2.1.1 y anteriores hashean el texto hex, así que sus raíces de dos o más hojas difieren |
Canonicalización y separación de dominios¶
| Método | Devuelve | Descripción |
|---|---|---|
canonicalize(value) |
string |
Canonicalización dcp-jcs-v1, basada en RFC 8785 JCS |
domainSeparatedMessage(context, payloadHex) |
string |
Mensaje con separación de dominios, en hexadecimal |
deriveKid(alg, publicKeyB64) |
string |
Identificador de clave determinista a partir del algoritmo y la clave pública |
Sesión y seguridad¶
| Método | Devuelve | Descripción |
|---|---|---|
generateSessionNonce() |
string |
Nonce aleatorio de 256 bits, representado por 64 caracteres hexadecimales |
verifySessionBinding(artifacts) |
SessionBindingResult |
Comprueba la coherencia del nonce entre artefactos |
computeSecurityTier(intent, policyDecision?) |
SecurityTierResult |
Calcula el nivel de seguridad adaptativo: routine/standard/elevated/maximum; risk_score procede de la decisión de política |
Preparación del contenido¶
| Método | Devuelve | Descripción |
|---|---|---|
preparePayload(payload) |
PreparedPayload |
Canonicaliza el contenido y calcula su hash |
Construcción y firma de bundles¶
| Método | Devuelve | Descripción |
|---|---|---|
buildBundle(opts) |
CitizenshipBundleV2 |
Construye un bundle V2 con manifiesto a partir de artefactos firmados con signPayload; sessionNonce usa por defecto el session_nonce del RPR |
signBundle(bundle, classicalSk, classicalKid, pqSk, pqKid) |
SignedBundleV2 |
Firma un bundle con firma compuesta; signer.id es el human_id del RPR. Si el manifiesto tiene session_expires_at pero no session_issued_at, establece session_issued_at en el instante de firma |
Prueba de posesión¶
| Método | Devuelve | Descripción |
|---|---|---|
generateRegistrationPop(challenge, sk, alg) |
SignatureEntry |
Genera una prueba de posesión (PoP) para registrar una clave |
verifyRegistrationPop(challenge, pop, pk, alg) |
PopResult |
Verifica una PoP |
Utilidades¶
| Método | Devuelve | Descripción |
|---|---|---|
detectVersion(value) |
string \| null |
Detecta la versión del protocolo DCP en un objeto JSON |
Tipos DCP-05–09¶
El SDK WASM incluye interfaces de TypeScript para todos los artefactos DCP-05 a DCP-09, equivalentes a los tipos del SDK de Rust:
| Especificación | Interfaces |
|---|---|
| DCP-05 Ciclo de vida | LifecycleState, CommissioningCertificate, VitalityReport, VitalityMetrics, DecommissioningRecord, TerminationMode, DataDisposition |
| DCP-06 Sucesión | DigitalTestament, SuccessionRecord, MemoryTransferManifest, MemoryTransferEntry, SuccessorPreference, MemoryClassification, TransitionType, MemoryDisposition |
| DCP-07 Disputas | DisputeRecord, ArbitrationResolution, JurisprudenceBundle, ObjectionRecord, DisputeType, EscalationLevel, DisputeStatus, ObjectionType, AuthorityLevel |
| DCP-08 Derechos | RightsDeclaration, RightEntry, ObligationRecord, RightsViolationReport, RightType, ComplianceStatus |
| DCP-09 Delegación | DelegationMandate, AdvisoryDeclaration, PrincipalMirror, InteractionRecord, AwarenessThreshold, ThresholdRule, AuthorityScopeEntry, ThresholdOperator, ThresholdAction |
Contextos de separación de dominios disponibles mediante domainSeparatedMessage(): Lifecycle, Succession, Dispute, Rights, Delegation, Awareness.
Experimental: candidato de canonicalización dcp-jcs-v2 (activación explícita)¶
No es normativo ni está activado: dcp-jcs-v1 sigue siendo el único perfil de canonicalización y no cambian los valores predeterminados, constructores, firmantes ni verificadores. Estos métodos están disponibles para quien opte explícitamente por ellos también en npm WASM 2.1.0. Los enlaces WASM reciben únicamente bytes; los valores del entorno anfitrión se comprueban primero en JavaScript. Un bundle verificado mediante el candidato no otorga confianza ni admisión. Diseño y contrato: docs/proposals/canonicalization-candidate (§15 entrada, §16 enrutador, §17 verificador, §18 productores).
| Método | Descripción |
|---|---|
experimentalCandidateIngress(bytes) |
{ status: 'accept', canonical, ... } o { status: 'reject', class, offset } |
experimentalCandidateExtractUnit(bytes, transport) |
Unidad de un transporte bare o envelope |
createExperimentalCandidateRouter(config) |
Enruta bytes; historical es uno de los experimentalWasmHistoricalAdapters (w1… para objetos mediante JSON.stringify, w2… para texto); ninguno se elige por defecto |
createExperimentalCandidateVerifier(config).verify(bytes, policy?) |
Verifica desde los bytes originales con un máximo de un verificador; obtiene la hora de Date.now |
experimentalCandidateSignPayload(context, input, keys) |
Firma un artefacto a partir de { bytes } o { value } |
experimentalCandidateSignBundle(parts, signer, options) |
Ensambla y firma un bundle cuyo manifiesto identifica dcp-jcs-v2 |
ExperimentalWasmCandidateProduceError |
Rechazo del dominio o de la forma de las entradas, antes de que cualquier enlace WASM firme |
import { readFileSync } from 'node:fs';
import { initDcp } from '@dcp-ai/wasm';
const wasmFile = new URL('dcp_ai_bg.wasm', import.meta.resolve('@dcp-ai/wasm/pkg'));
const dcp = await initDcp(`data:application/wasm;base64,${readFileSync(wasmFile).toString('base64')}`);
// The ingress decides on the original bytes: 1.0 is the integer 1, members are sorted.
const accepted = dcp.experimentalCandidateIngress(new TextEncoder().encode('{"b":1.0,"a":[]}'));
if (accepted.status === 'accept') console.log(new TextDecoder().decode(accepted.canonical)); // {"a":[],"b":1}
// A producer signs an artifact from those bytes (hybrid keys, today's composite signature).
const h = dcp.generateHybridKeypair();
const key = (k: typeof h.classical) => ({
kid: k.kid ?? dcp.deriveKid(k.alg, k.public_key_b64), alg: k.alg, secretKeyB64: k.secret_key_b64, publicKeyB64: k.public_key_b64,
});
const artifact = dcp.experimentalCandidateSignPayload(
'DCP-AI.v2.Intent', { bytes: new TextEncoder().encode('{"intent_id":"intent:1","risk":1.0}') },
{ classical: key(h.classical), pq: key(h.pq) },
);
console.log(artifact.signedPayload.payload_hash); // sha256:… of {"intent_id":"intent:1","risk":1}
API de bajo nivel¶
También puedes utilizar directamente las funciones WASM, sin el envoltorio de TypeScript:
import init, {
wasm_generate_hybrid_keypair,
wasm_composite_sign,
wasm_composite_verify,
wasm_build_bundle,
wasm_sign_bundle,
wasm_verify_signed_bundle_v2,
wasm_ml_kem_768_keygen,
wasm_ml_kem_768_encapsulate,
wasm_ml_kem_768_decapsulate,
wasm_dual_hash,
wasm_compute_security_tier,
} from '@dcp-ai/wasm/pkg';
await init();
const keys = JSON.parse(wasm_generate_hybrid_keypair());
// ... use raw functions, all return JSON strings
Véase example.html para una demostración interactiva completa en el navegador.
Niveles de seguridad¶
computeSecurityTier(intent, policyDecision?) calcula el nivel de seguridad adaptativo (spec §14.2). risk_score procede de la decisión de política (0 si no se proporciona una); un valor desconocido o ausente de action_type, estimated_impact o data_classes, o un risk_score ausente o que no sea un entero entre 0 y 1000, se considera elevated. intent.security_tier solo puede elevar el resultado. Se aplica el nivel más alto que coincida.
| Nivel | Condición (cualquiera de ellas) | Modo de verificación | Intervalo de checkpoints |
|---|---|---|---|
routine |
Ninguna de las condiciones siguientes | classical_only |
50 |
standard |
risk_score 200–499, estimated_impact: medium |
hybrid_preferred |
10 |
elevated |
risk_score 500–799, pii / financial_data / health_data, initiate_payment, estimated_impact: high, valores desconocidos |
hybrid_required |
1 |
maximum |
risk_score ≥ 800, credentials / children_data |
hybrid_required |
1 |
Algoritmos compatibles¶
| Categoría | Algoritmo | Estándar |
|---|---|---|
| Firma clásica | Ed25519 | RFC 8032 |
| Firma poscuántica | ML-DSA-65 | FIPS 204 |
| Firma poscuántica sin estado | SLH-DSA-192f | FIPS 205 |
| Encapsulación de claves poscuántica | ML-KEM-768 | FIPS 203 |
| Hashes | SHA-256 + SHA3-256 | FIPS 180-4, FIPS 202 |
| Canonicalización | dcp-jcs-v1 |
Basada en RFC 8785 (JCS) |
Desarrollo¶
Requisitos previos¶
# Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Install WASM target
rustup target add wasm32-unknown-unknown
Ejecutar las pruebas WASM de Rust¶
Licencia¶
Apache-2.0