You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
datekeys-doc/PLAN_fase2_ibe_noble2.md

33 KiB

Plan: fase 2 del SDK TypeScript. Descifrado y cifrado tlock sobre noble 2, verificación local de releases y canonicidad de puntos

Estado: v2, 26 de septiembre de 2026. Decisiones de la sección 2 confirmadas por el autor el 26-09-2026, con los ajustes de dos revisiones (la de Claude Opus y la de Fable), verificados contra el código, el spec y npm. Sustituye la "Fase 2, cifrar y descifrar" de la sección 8 del plan v2 (PLAN_codec_cbor_y_pagina_svelte.md), cuyo bloque "Piezas de tlock-js y drand-client" contenía dos errores: para Quicknet el descifrador es decryptOnG2, no decryptOnG1, y drand-client no hace falta.

Alcance: abrir y crear cápsulas del perfil Quicknet en el navegador, sin red, sobre la librería TypeScript de App. El núcleo IBE se escribe en el proyecto sobre @noble/curves 2.x, los releases se verifican con noble y el perfil pinneado, y la envoltura age la pone age-encryption. Ni tlock-js ni drand-client entran en el bundle.

Regla de dependencias: la del plan v2. En ejecución, solo age, drand, tlock y lo que ellas arrastran, en cualquier lenguaje; @noble/curves y @noble/hashes entran por ser lo que age-encryption arrastra. Toda dependencia nueva se propone por escrito y no se instala sin aprobación (sección 3).


1. Situación de partida

Todo lo de esta tabla se ejecutó el 26-09-2026 sobre tlock-js 0.9.0, @noble/curves 1.9.7 y 2.4.0, los fixtures de App/testdata y la librería Go. Los scripts quedaron en el scratchpad de la sesión (…\scratchpad\noble2ibe\c, …\scratchpad\override, …\scratchpad\noblehist); si el directorio temporal ya no existe, cada punto se reproduce a partir de la descripción.

Hecho Detalle
Las 5 615 discrepancias de noble 1.9.7 frente a Go son un solo fenómeno 5 612 codificaciones no canónicas de coordenada (x + p, c0 o c1 + k·p) que decodifican al punto correcto y 3 identidades con flags o carga no nula. Cero en sentido contrario, cero fallos de subgrupo: fromHex de 1.9.7 ya ejecuta assertValidity. Sin la comprobación de longitud, 102 más por codificaciones sin comprimir.
Efecto en tlock-js sobre 1.9.7 decryptOnG2 acepta un U recodificado como c0 + p y una firma x + p y devuelve la misma file key. bls12-381.js:258 (G1) y :358 (G2) reducen módulo p.
Efecto en Go U no canónico, identidad, fuera de curva, fuera de subgrupo o cuerpo de 127 o 129 bytes: ERR_INTEGRITY en el paso 11. Firma no canónica, identidad, negada o de otra ronda: ERR_RELEASE_INVALID en el paso 10. Firma y U malos a la vez: gana el paso 10. Con el cliente drand incorporado, una firma mala servida por el relay es ERR_RELEASE_UNAVAILABLE en el paso 9.
checkCompressedPoint como puerta Exigiendo 'point', bloquea todas las variantes anteriores, incluida la identidad canónica, y coincide con Go en las 49 451 entradas de los corpus. 11,4 ms por punto G2 y 6,5 ms por G1 en Node 24.
Un override a noble 2.4.0 no carga npm install termina con exit 0 y sin avisos. Después: ERR_PACKAGE_PATH_NOT_EXPORTED (2.x es ESM y solo exporta subrutas .js), ProjectivePoint y toRawBytes no existen, fromHex solo acepta string, sha256.update("IBE-H2") lanza con hashes 2.x. Con 12 a 18 líneas cambiadas el núcleo corre e interopera en ambos sentidos con 1.9.7.
Módulo mínimo sobre noble 2.4.0 44 líneas de código (64 con comentarios), solo descifrado Quicknet. Pasa el tsconfig estricto de App. Descifra los cinco fixtures con las mismas file keys que tlock-js y que tlock.TimeUnlock en Go; el MAC de la cabecera age verifica; no carga nada de tlock-js. Rechaza el U no canónico en fromBytes como Go.
Serialización de GT Fp12.toBytes de noble (igual en 1.9.7 y 2.4.0) escribe c0 primero en cada nivel. kilic, kyber y fp12ToBytes de tlock-js escriben los doce limbs de 48 bytes en orden inverso. Con el orden de noble la clave sale mal (control negativo).
Verificación de releases sin drand-client shortSignatures.verify(sig, shortSignatures.hash(sha256(uint64be(ronda)), DST), pk) de noble 2.4.0 da true para la ronda 1000 y false para la 999 con la firma real. El DST por defecto de G1 es el de Quicknet. Ninguna versión de drand-client ha estado en noble 2.x; su verificación rfc9380 aceptó tres codificaciones alternativas de la firma real, cada una con un randomness distinto.
tlock-js 0.9.0 gitHead 17d817e (18-03-2024) es el commit que pasó a @noble/curves; el código auditado en 2022-2023 corría sobre @noble/bls12-381; fp.ts es nuevo en 0.9.0 y cerca del 40 % de ibe.ts cambió después. Sin tags ni releases, master sin cambios desde entonces. Cinco dependencias de ejecución; instalación limpia de 33 paquetes y 10,6 MB. npm audit sin avisos. Su guarda de longitud de mensaje solo salta a partir de 512 bytes.
Spec v0.8.2 No nombra U, V ni W, ni sus tamaños, ni la canonicidad de las codificaciones. §12.1 pide "la codificación comprimida de BLS12-381 que usa drand", subgrupo y no infinito, sin x < p. El paso 11 de §63 solo dice que un cuerpo que no es un ciphertext tlock del scheme da ERR_INTEGRITY. La regla de facto es el decodificador kilic.
Librería TypeScript hoy src/lib/dkc ejecuta los pasos 1 a 8 e inspect; age.ts parsea cabeceras age y checkTimeStanzas aplica cardinalidad, tipo, argumentos, ronda y chain hash con los códigos de Go; bls12381.ts valida puntos comprimidos; no hay writer de cápsulas. @noble/curves 2.4.0 es dependencia de desarrollo y un test prohíbe importarla fuera de tests.
Repositorio main en d5e3236, en verde: la alineación con datekeys-go está fusionada y testdata sincronizado a 692cf87 (rama v0.8.2). La rama wip/align-3820066 ya no existe.
age-encryption 0.3.1 Declara @noble/curves ^2.0.1 y @noble/hashes ^2.0.1, que resuelven a la 2.4.0 de App sin override. Pero arrastra @noble/post-quantum 0.5.4, que declara @noble/curves ~2.0.0 y @noble/hashes ~2.0.0: habrá una segunda copia de noble 2.0.x anidada bajo post-quantum, que la usa para ML-KEM y X25519, no para BLS. Admite la Streams API para cifrar y descifrar.

2. Decisiones

Confirmadas por el autor el 26-09-2026 (paso 0 de la sección 10).

  1. Núcleo IBE propio en src/lib/dkc/ibe.ts, derivado de tlock-js/crypto/ibe.ts (commit 17d817e, licencia Apache-2.0 OR MIT, con aviso de procedencia) y con la semántica de drand/kyber encrypt/ibe. Sobre @noble/curves 2.3.0 o posterior. Se descarta parchear tlock-js y se descarta una puerta alrededor de tlock-js sobre noble 1.9.7, que dejaría dos versiones mayores de noble en el bundle.
  2. Puerta de canonicidad: toda firma, U y clave pública pasa por checkCompressedPoint y solo sigue si el resultado es exactamente 'point'. Aunque noble 2.3.0 y posteriores rechazan lo mismo, la puerta fija la precedencia y el código de error, y trata la identidad canónica antes del pairing, donde noble la acepta y falla con otro mensaje.
  3. Solo Quicknet (bls-unchained-g1-rfc9380: clave pública en G2, firmas en G1, U en G2). Los otros dos schemes de drand quedan fuera de esta fase; se anota en la sección 12.
  4. Releases sin red en esta fase: la obtención (paso 9) queda fuera del SDK. El SDK recibe el release y lo verifica localmente (paso 10). La CSP connect-src 'self' de la página no cambia. El contrato de ReleaseSource se fija ya (sección 5): una fuente verifica cada respuesta, y si no obtiene ningún release verificado el resultado es ERR_RELEASE_UNAVAILABLE en el paso 9, como el cliente drand de Go (provider/drand/client.go); un release entregado por quien llama que no verifica es ERR_RELEASE_INVALID en el paso 10. Para el producto: la página habla con un único origen, la Release API propia, que consulta varios relays y verifica en el servidor (§47, §48); el SDK ofrece además la consulta directa a drand como fuente opcional (§49 SHOULD), nunca como opción por defecto de la página.
  5. age-encryption 0.3.1 para las tres envolturas age, con Identity y Recipient propios para el stanza tlock. Confirma la decisión 4 del plan v2. Sin overrides de npm: el rango ^2.0.1 resuelve a 2.4.0, y la copia 2.0.x que trae @noble/post-quantum (que declara ~2.0.0 a propósito) se acepta si solo la usa post-quantum. Las guardas de la sección 3 lo comprueban y el paso 2 mide su coste en el bundle.
  6. Cambio de spec: canonicidad de puntos y contenido del cuerpo del stanza tlock (sección 7), como enmienda de v0.8.2, que no está fusionada ni etiquetada. En curso en datekeys-go, rama v0.8.2, junto con la limpieza del texto de error de kyber.
  7. Apertura en streaming: PAYLOAD_AGE se descifra con la Streams API de age-encryption hacia un fichero temporal en OPFS, que solo se muestra o se ofrece para descargar cuando age termina sin error (§56). §57 no acota PAYLOAD_AGE, así que el límite es la cuota de almacenamiento del navegador, que se consulta con navigator.storage.estimate() antes de empezar.
  8. Licencia de App: Apache-2.0, como datekeys-go. ibe.ts conserva el aviso de copyright y licencia de tlock-js (Apache-2.0 OR MIT).

3. Dependencias de ejecución y guardas

Paquete Versión Estado
age-encryption 0.3.1 aprobada (decisión 5) e instalada el 28-09-2026. Arrastra @noble/ciphers 2.4.0, @noble/curves 2, @noble/hashes 2, @noble/post-quantum 0.5.4 (con su propia copia de @noble/curves y @noble/hashes 2.0.1) y @scure/base 2.4.0.
@noble/curves 2.4.0 pasó de desarrollo a ejecución el 28-09-2026. Fijada exacta.
@noble/hashes 2.4.0 dependencia exacta de @noble/curves 2.4.0; declarada explícita el 28-09-2026 por importarse directamente.
tlock-js, drand-client — no se instalan.

Guardas, todas en tests que corren en cada ejecución:

  • ningún fichero de src/ importa tlock-js ni drand-client;
  • @noble/* solo se importa desde src/lib/dkc/ibe.ts, src/lib/dkc/release.ts y los tests. El test actual "only tests import @noble/curves" de bls12381.contrast.test.ts pasa a una lista blanca con esos dos ficheros;
  • ibe.ts, release.ts y el test de contraste de bls12381.ts resuelven @noble/curves y @noble/hashes a la copia de la raíz, exactamente 2.4.0 (2.3.0 o posterior es el mínimo por la corrección de canonicidad), y ningún fichero de src/ importa una copia anidada;
  • package-lock.json no contiene ninguna versión 1.x de @noble/curves ni de @noble/hashes, y cualquier copia 2.x distinta de la raíz está anidada bajo @noble/post-quantum;
  • scripts/check-build.mjs comprueba además que el sitio construido no contiene tlock-js, drand-client ni @babel, e informa del tamaño del bundle de la página de apertura.

4. Módulo IBE

src/lib/dkc/ibe.ts, sin dependencias fuera de @noble/curves, @noble/hashes y bls12381.ts.

Semilla. El módulo de descifrado medido el 26-09-2026 (scratchpad\noble2ibe\c\ibe.ts), con sus importaciones cambiadas a especificadores normales (@noble/curves/bls12-381.js, @noble/curves/utils.js, @noble/hashes/sha2.js).

Contenido.

  • Ciphertext { U, V, W } y decryptOnG2(signature, ct): longitudes (48, 96, |V| == |W| <= 32), puerta 'point' sobre firma y U, G1.Point.fromBytes y G2.Point.fromBytes con assertValidity, pairing(sig, U), sigma = V xor H2(gt), msg = W xor H4(sigma), r = H3(sigma, msg), comprobación r·G2 == U.
  • encryptOnG2RFC9380(publicKey, id, msg): puerta 'point' sobre la clave, |msg| <= 32 (kyber comprueba len(msg) > Hash().Size(); tlock-js tiene aquí un fallo que no se copia), Qid = G1.hashToCurve(id, { DST: 'BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_' }), gt = pairing(Qid, pk), sigma de |msg| bytes con crypto.getRandomValues, r = H3(sigma, msg), U = r·G2, V = sigma xor H2(gt^r), W = msg xor H4(sigma). Fp12.pow para gt^r.
  • H2(gt) = SHA-256("IBE-H2" ‖ GT)[:len] con GT serializado en el orden de kilic: para [gt.c1, gt.c0], para [a.c2, a.c1, a.c0], Fp.toBytes(b.c1) ‖ Fp.toBytes(b.c0). Nunca Fp12.toBytes de noble.
  • H3(sigma, msg): base = SHA-256("IBE-H3" ‖ sigma ‖ msg); para i = 1 … 65534, d = SHA-256(uint16le(i) ‖ base), d[0] >>= 1, se acepta el primer d big-endian menor que Fr.ORDER. Si el bucle termina, error.
  • H4(sigma) = SHA-256("IBE-H4" ‖ sigma)[:len].
  • roundIdentity(round) = SHA-256(uint64be(round)).
  • Errores: una clase IbeError con un motivo fijo (length, encoding, identity, proof). Ningún mensaje de error incluye sigma, msg, r ni bytes de entrada. Es la lección del texto de error de kyber, que datekeys-go copiaba a sus diagnósticos (se corrige en la misma rama que la enmienda de la sección 7).
  • sigma y la file key se borran (fill(0)) en cuanto dejan de usarse, en todos los caminos, como hace la librería con access_material e I_PAYLOAD.
  • Para tests, una variante interna de encryptOnG2RFC9380 recibe sigma en vez de generarlo, de modo que el cifrado se compara byte a byte con Go. No se exporta desde index.ts.

Vectores de Go. La referencia de los tests IBE es la librería Go, no los tests de tlock-js, cuyo ibe.ts cambió en un 40 % tras la auditoría. scripts/ibe-go-vectors.go, como scripts/bls12381-go-verdicts.go, genera con kyber y tlock: los bytes de GT de e(G1, G2) y de su cuadrado, H2, H3 y H4 sobre entradas fijas, la file key de cada stanza de los fixtures con la firma de su sidecar y un cifrado con sigma fijo. El resultado se congela en src/lib/dkc/testing/. El vector de GT de tlock-js (cb87319f24560b5231579a09ad79f12e) coincide con el de kyber (comprobado el 26-09-2026) y se conserva como contraste.

Serialización del stanza. Cuerpo U ‖ V ‖ W de 128 bytes para una file key de 16; argumentos [decimal canónico de la ronda, chain hash en hexadecimal minúsculo]. La lectura ya existe en age.ts; la escritura se añade en age.ts junto a ella.


5. Verificación de releases

src/lib/dkc/release.ts, verifyRelease(profile, round, release), en el mismo orden que provider.Verify de Go (provider/provider.go:53-76), que decodifica la clave antes de mirar la firma:

  1. ronda fuera del rango del perfil → ERR_DATEKEY_INVALID;
  2. ronda del release distinta de la ronda de la condición → ERR_ROUND_MISMATCH;
  3. firma que no mide 48 bytes → ERR_RELEASE_INVALID;
  4. clave del perfil pinneado, ya validada en los pasos 1 a 8; si no decodifica → ERR_UNKNOWN_PROFILE;
  5. checkCompressedPoint('G1', firma) !== 'point' → ERR_RELEASE_INVALID;
  6. shortSignatures.verify(firma, shortSignatures.hash(sha256(uint64be(ronda)), DST_QUICKNET), clave) distinto de true → ERR_RELEASE_INVALID. Una excepción de noble en este punto también es ERR_RELEASE_INVALID.

La interfaz ReleaseSource del SDK solo tiene una implementación estática en esta fase: el release embebido en el sidecar del fixture o suministrado por la aplicación. Su contrato se fija ya para las fuentes con red de fases posteriores: una fuente llama a verifyRelease con cada respuesta y, si ninguna verifica, falla con ERR_RELEASE_UNAVAILABLE en el paso 9; solo un release entregado directamente por quien llama da ERR_RELEASE_INVALID en el paso 10. Es el comportamiento de provider/drand/client.go en Go.

Además, desde la corrección 6 de §76, cualquier fallo de una fuente se informa en el paso 9 con ERR_RELEASE_UNAVAILABLE y ningún otro código. Si el error de la fuente lleva otro código normativo, o ninguno, se conserva solo su texto. Si el contexto termina, se sigue pudiendo detectar. Así lo hacen capsule.Open (sourceFailure) y provider/drand.Client en Go 9ac9cd9, y open.ts debe hacer lo mismo.


6. Apertura y cifrado con age-encryption

src/lib/dkc/open.ts reproduce los pasos 9 a 18 de capsule/open.go, sobre los pasos 1 a 8 que ya ejecuta inspect:

  • paso 9, release por ReleaseSource; paso 10, verifyRelease;
  • paso 11, OUTER_TIME_AGE con Decrypter de age-encryption y una Identity propia cuyo unwrapFileKey(stanzas) ejecuta, en este orden, checkTimeStanzas (cardinalidad de §63, como agewrap.TimeIdentity.Unwrap en Go), verifyRelease otra vez, la comprobación de 128 bytes del cuerpo, decryptOnG2 y la longitud 16 de la file key. Cada fallo de cuerpo o de descifrado es ERR_INTEGRITY; el MAC de la cabecera lo comprueba age-encryption;
  • pasos 12 a 18 como en Go: estructura de política, INNER_ACCESS_AGE con las identidades del llamante, CONTROL_CBOR canónico, header_binding sobre los bytes exactos, identidad de payload y PAYLOAD_AGE;
  • PAYLOAD_AGE se descifra en streaming (decisión 7): File.slice(offset).stream() entra en Decrypter.decrypt, la salida se escribe en un fichero temporal de OPFS y el plaintext solo se entrega cuando el stream termina sin error; si falla la autenticación en cualquier chunk, el fichero temporal se borra y no se muestra nada (§56). Antes de empezar se compara el tamaño de PAYLOAD_AGE con la cuota libre.

Cifrado: Encrypter con un Recipient propio cuyo wrapFileKey(fileKey) llama a encryptOnG2RFC9380(clave del perfil, roundIdentity(ronda), fileKey) y devuelve el stanza tlock. La construcción completa de un .dkc (prelude, cabecera, control sellado) requiere el writer TypeScript, que es fase 3; en esta fase el cifrado se prueba a nivel de stanza y de fichero age (sección 8).

Antes de escribir código se comprueba en el paquete instalado la forma exacta de Identity, Recipient y Stanza de age-encryption 0.3.1 y se anota en este plan.


7. Cambios en la especificación y en datekeys-go

Todo en un único cambio normativo, con vectores congelados, según la política de §76 (caso de "segunda implementación independiente" y "prueba de interoperabilidad").

  • Definición de codificación canónica de punto, en §12 o en un apartado nuevo: la serialización comprimida de BLS12-381 que produce drand, 48 bytes en G1 y 96 en G2; bit de compresión a 1; bit de infinito solo en el punto en el infinito, con el resto de bytes a cero; coordenadas big-endian menores que p, y en G2 c1 seguido de c0, ambas menores que p; el punto está en el subgrupo de orden primo. Un decodificador MUST rechazar cualquier otra cadena de bytes, en particular x + p y una identidad con carga o con el bit de compresión ausente.
  • §12.1, punto 2: public_key es una codificación canónica según esa definición, distinta del infinito. Código sin cambio: ERR_UNKNOWN_PROFILE.
  • §63, paso 10: la firma es una codificación canónica de un punto del grupo de firmas del scheme, distinta del infinito, y verifica como firma de la ronda; en otro caso ERR_RELEASE_INVALID. Se mantiene la precedencia de ERR_ROUND_MISMATCH.
  • §63, paso 11: el cuerpo del stanza tlock es U ‖ V ‖ W, con |U| igual al tamaño de punto del grupo de claves del scheme (96 para Quicknet), |V| = |W| = 16; U es una codificación canónica de un punto de ese grupo distinta del infinito; el descifrado IBE-CCA comprueba r·G == U. Cualquier fallo, incluida una longitud distinta de 128, es ERR_INTEGRITY. Se cita drand/tlock para H2, H3 y H4.
  • §64, mutaciones nuevas con código y paso, verificados en Go el 26-09-2026: U con c0 + p, U identidad canónica, U con flag de infinito y carga, cuerpo de 127 y 129 bytes → ERR_INTEGRITY, paso 11; firma x + p (requiere un release de prueba cuya x lo permita, o un vector sintético documentado), firma identidad, firma con flag de infinito y carga, firma negada → ERR_RELEASE_INVALID, paso 10; firma mala y U malo a la vez → ERR_RELEASE_INVALID, paso 10.
  • datekeys-go: las mismas mutaciones en internal/testkit/mutations.go, regeneración de testdata/vectors/mutations.json, traceability.md y CHANGELOG.md. El comportamiento del código no cambia. Aparte, la tarea ya propuesta de no copiar el texto de error de kyber a los errores y a Inspection.Checks[].Detail.
  • App: testdata:sync al commit resultante y reproducción en TypeScript de código y paso de cada mutación nueva.

8. Tests

  1. ibe.test.ts. Los vectores de Go de la sección 4 (GT, H2, H3, H4, las file keys de los fixtures y el cifrado con sigma fijo), y como contraste el de tlock-js (gtToHash de 16 bytes cb87319f24560b5231579a09ad79f12e, igual al de kyber). Los cinco fixtures: la file key obtenida con la firma del sidecar abre la cabecera age. Control negativo: serializar GT con Fp12.toBytes de noble da otra clave. U con c0 + p, U identidad, U negado y firma de otra ronda: rechazados con el motivo esperado. Ida y vuelta encryptOnG2RFC9380 → decryptOnG2 con una clave sintética. Cobertura del 100 %.
  2. release.test.ts. Ronda 1000 con la firma real: válida; 999: inválida; codificaciones alternativas de la misma firma (sin comprimir, x + p, flag de signo alterado): ERR_RELEASE_INVALID; longitud 47 y 96; identidad. Cobertura del 100 %.
  3. open.test.ts. Los cinco fixtures se abren con el release del sidecar y el SHA-256 del plaintext coincide con el sidecar; los .dkk de time_and_key abren con sus identidades. El corpus de mutaciones exportado, incluidas las nuevas de la sección 7, reproduce código y paso.
  4. Interoperabilidad TS → Go a nivel IBE. scripts/ibe-go-roundtrip.go, como scripts/bls12381-go-verdicts.go: TypeScript cifra 16 bytes para la ronda 1000 con la clave de Quicknet y escribe U ‖ V ‖ W; Go los abre con tlock.BytesToCiphertext y tlock.TimeUnlock con la firma real y compara. Se ejecuta a mano y su resultado se congela como vector.
  5. Interoperabilidad TS → Go a nivel de fichero age. Un fichero age con un stanza tlock escrito por age-encryption con el Recipient propio, abierto por agewrap de Go con la firma real. Se ejecuta a mano y se congela.
  6. Guardas de la sección 3.
  7. Rendimiento, informativo: tiempo de decryptOnG2 en frío y en caliente en Node y en el navegador de la página.

9. Página

La ruta /inspect gana una acción "abrir": con un fixture o un .dkc arrastrado, un release pegado o el del sidecar, y las identidades .dkk cuando la política las exige. Muestra el resultado de cada paso 9 a 18 como hoy muestra 1 a 8, y el SHA-256 del plaintext; para los fixtures, además el plaintext. Sin red: la CSP no cambia y check-build lo comprueba. El plaintext nunca se envía.

Precisión aprobada por el autor el 28-09-2026, tras revisar lo que dice el protocolo:

  • El plaintext de un fichero propio se guarda solo en un fichero temporal del almacenamiento privado del navegador (OPFS), hasta la descarga. Es el "fichero temporal" que recomienda §56, y ninguna regla del protocolo prohíbe guardarlo en el cliente. Se borra al pedirlo, al abrir o cargar otra cápsula, al salir de la página y, si quedó, en la siguiente visita.
  • El release lo suministra quien abre (§63 paso 10): pegado de la respuesta de drand, que la persona abre en otra pestaña desde un enlace de la página, o el del registro de un fixture. Solo se leen su ronda y su firma (§11, §13).
  • §48 y §49 siguen pendientes. Con solo releases suministrados, la librería no cumple aún el SHOULD de §48 (varios relays) y no resuelve por sí misma el de §49 (obtener el release directamente del proveedor, sin la API DateKeys). Los cubrirá la fuente drand opcional del SDK, nunca activa por defecto en la página (decisión 4, sección 12).

10. Orden de trabajo y criterios de aceptación

Paso Contenido Hecho cuando
0 Confirmar las decisiones de la sección 2 y aprobar las dependencias de la sección 3 hecho el 26-09-2026
1 Precondición: main verde con testdata sincronizado al último commit de datekeys-go cumplida en d5e3236 (692cf87) y de nuevo en 71ab8fb, con testdata en 9ac9cd9 (spec-v0.8.2)
2 Dependencias y guardas instaladas con versiones exactas; npm audit --omit=dev sin avisos; guardas en verde; en el lockfile, un solo noble 2.4.0 en la raíz, la copia 2.0.1 solo bajo @noble/post-quantum y ningún 1.x. Hecho el 28-09-2026: el README de App recoge las guardas, la medida del bundle y el resultado de npm audit
3 ibe.ts de descifrado desde la semilla, roundIdentity, escritura del stanza en age.ts, ibe.test.ts sin la parte de cifrado los cinco fixtures dan la file key correcta; U no canónico e identidad rechazados; cobertura 100 %. Hecho el 28-09-2026: vectores de scripts/ibe-go-vectors.go en src/lib/dkc/testing/ibe-vectors.json; ibe.ts al 100 %, fijado como umbral. ibe.ts también pasa el cuerpo U ‖ V ‖ W a bytes; los argumentos del stanza y su paso al Stanza de age-encryption, que guarda el tipo en args[0], van al paso 7, con el Recipient que los usa
4 release.ts y release.test.ts ronda real válida, alias rechazados. Hecho el 28-09-2026:
- verifyRelease con el orden y los textos de provider.Verify;
- ReleaseSource, con el contrato de la sección 5, y suppliedRelease;
- los casos de TestVerifyRejects de Go y los 7 del corpus de mutaciones que fallan en el paso 10;
- las firmas publicadas de las rondas 1000, 1001, 2000 y 1004, esta última obtenida restando p a la codificación x + p del corpus;
- cobertura del 100 %, fijada como umbral
5 open.ts con la Identity propia, pasos 9 a 18, open.test.ts los cinco fixtures se abren y el plaintext coincide con el sidecar; el corpus de mutaciones existente reproduce código y paso.
5a hecho el 28-09-2026, con el texto en claro en memoria:
- los 65 casos del corpus pasan por open con el código y el paso de Go;
- los cinco fixtures se abren con cada credencial;
- los textos siguen a capsule.Open y agewrap.
El paso 13 exige probar cada identity contra cada stanza X25519, y age-encryption no expone su X25519Identity. Por eso x25519.ts abre los stanzas de uno en uno, con ChaCha20-Poly1305 de @noble/ciphers 2.4.0, dependencia aprobada el 28-09-2026 porque es la copia que ya usa age-encryption. bech32.ts lee las identidades AGE-SECRET-KEY-1….
index.ts aún no reexporta la apertura, que metería noble en /inspect: el paso 8 la cargará bajo demanda.
5b hecho el 28-09-2026: open acepta un Blob, del que lee solo el prefijo de los pasos 1 a 8 (prefix.ts, movido de la página a la librería), calcula el capsule_digest en streaming (digest.ts) y descifra PAYLOAD_AGE en streaming. La salida puede ser un WritableStream, que se cierra tras el paso 18 y se aborta ante cualquier fallo. Los 65 casos del corpus pasan también así.
Comprobado en el navegador con un fichero OPFS (createWritable): time_only se abre con el SHA-256 del sidecar, y un fallo de STREAM deja intacto el contenido anterior del fichero.
La consulta de cuota (navigator.storage.estimate()), el fichero temporal y la descarga van con la página, en el paso 8
6 Spec, mutaciones en Go, testdata:sync, reproducción en TypeScript (sección 7) texto aprobado; vectores congelados en ambos repositorios; commits en Gitea. Hecho: la enmienda de canonicidad entró en la v0.8.2 (f6f2e9f, tag spec-v0.8.2), y sus 10 mutaciones pasan por open en TypeScript desde el paso 5a
7 encryptOnG2RFC9380, Recipient propio, ida y vuelta, interoperabilidad TS → Go a nivel IBE y de fichero age (sección 8, puntos 4 y 5) Go abre lo que TypeScript cifra; vectores congelados.
Hecho el 28-09-2026:
- ibe.ts gana encryptOnG2RFC9380 y encryptOnG2WithSigma (solo para tests);
- tlock.ts gana timeRecipient, con las comprobaciones y textos de NewTimeRecipient;
- scripts/tlock-go-vectors.go reescribe EncryptCCAonG2 con sigma fijo, lo comprueba con kyber y tlock, y abre las muestras de scripts/tlock-ts-samples.mjs: Go abrió los cuerpos IBE y los ficheros age de las rondas 1000 y 1001 con la misma file key y el mismo texto;
- todo congelado en src/lib/dkc/testing/tlock-vectors.json;
- cobertura del 100 % de ibe.ts y tlock.ts
8 Página (sección 9) un fixture time_and_key se abre en el navegador sin red; check-build en verde; tamaño del bundle anotado en el README.
Hecho el 28-09-2026:
- OpenPanel.svelte, con release-input.ts (release pegado o del registro del fixture), opener.ts (cargado con import()), opening.ts (pasos 9 a 18 como los registra la referencia) y tempfile.ts (fichero temporal de OPFS, un directorio y un Web Lock por pestaña, cuota libre y limpieza);
- readAccessKey en prefix.ts;
- licenses.txt con los avisos de tlock-js y age y los de cada paquete del bundle, escrito por vite.config.ts;
- nuevas guardas de check-build: ninguna página carga noble, @scure/base ni age-encryption en la primera carga, y licenses.txt está completo;
- cobertura del 100 % de los módulos nuevos.
Comprobado en el navegador con la compilación de producción:
- se abren los fixtures time_only, time_and_key_portable (con su .dkk) y time_and_key_recipients (con una identidad pegada), cada uno con el SHA-256 de su registro;
- una firma alterada da ERR_RELEASE_INVALID en el paso 10, y un fragmento STREAM alterado da ERR_INTEGRITY en el paso 17, sin descarga ni fichero temporal;
- un fichero propio se abre al fichero temporal, se descarga sin violar la CSP y se borra con su lock;
- lo que dejó una pestaña terminada se borra en la visita siguiente;
- ninguna petición sale del origen, y la página no se desborda a 375 px de ancho.
Revisión adversarial, en cuatro dimensiones (protocolo, seguridad, estado de la interfaz, tests y guardas), con cada hallazgo contrastado por un revisor que intentaba refutarlo: se confirmaron 15 hallazgos, algunos repetidos, y se refutaron 5. Todos se corrigieron:
- la fecha se vuelve a mirar cuando llega y cuando la pestaña vuelve a verse;
- el fichero de una apertura en curso es de la página: se borra con ella, y la apertura se detiene si se carga otra cápsula (cancellable);
- se vuelve a comprobar tras cada espera si la apertura sigue vigente;
- si el navegador rechaza OPFS, la apertura se hace en memoria;
- el nombre ofrecido para descargar no lleva caracteres invisibles;
- las glosas de ERR_INVALID_MAGIC y ERR_EXTENSION_CRITICAL_UNKNOWN valen también para la .dkk;
- el foco va al campo o al mensaje del error;
- tras volver de la caché de atrás y adelante no se ofrece un enlace muerto;
- un nombre largo no desborda la página;
- licenses.txt incluye el código de Vite y rolldown que entra en el bundle;
- la guarda de la primera carga resuelve los scripts respecto a cada página y falla si no encuentra ninguno

Cada paso termina con npm run verify en verde y un commit en Gitea. El paso 6 puede ir en paralelo con el 4 y el 5.


11. Riesgos

  • API de age-encryption 0.3.1 para Identity, Recipient y Stanza. Mitigación: comprobarla en el paquete instalado antes del paso 5 y fijar la versión exacta.
  • Cambios de API de noble 2.x. Ya ocurrió entre 1.x y 2.x. Mitigación: versión exacta, guardas y el test del vector de GT, que detecta cualquier cambio de orden o de Fp.toBytes.
  • Orden de bytes de GT. Es el único punto donde una implementación puede coincidir en todo lo demás y fallar aquí. Mitigación: el vector de tlock-js y el control negativo.
  • Doble noble en el bundle. @noble/post-quantum 0.5.4 ya trae su propia copia 2.0.x; si alguna dependencia futura arrastra 1.x, sería peor. Mitigación: las guardas de la sección 3 (ningún 1.x, copias 2.x distintas solo anidadas bajo post-quantum, el código BLS en la 2.4.0 exacta) y la medida del bundle en el paso 2.
  • Diagnósticos con material interno. Mitigación: la regla de errores de la sección 4 y un test que busca en los mensajes de error los bytes de sigma, msg y r.
  • H3 devuelve 0, con probabilidad 2⁻²⁵⁵: multiply(0) lanza RangeError. Mitigación: tratar cualquier excepción del cálculo como proof y cubrirlo con un test que inyecte r = 0.
  • Rendimiento en el navegador. Un pairing y una multiplicación escalar en G2 rondan los 40 a 300 ms en Node; la puerta añade 11 ms por punto. Mitigación: medir en el paso 8 y, si hace falta, descifrar en un worker. Medido en el paso 8, en Chromium, sin contar la descarga del código: 0,34 s los pasos 9 a 18 de time_only, la primera apertura, y 0,11 s los de time_and_key_portable. No hace falta un worker, que la CSP (worker-src 'none') tampoco permite.
  • Rama wip/align-3820066. Si no se fusiona antes, el paso 3 partiría de un árbol distinto del que verifican los tests actuales. Mitigación: el paso 1.
  • Aprobación de la spec. El paso 6 depende del autor; los pasos 3 a 5 no.

12. Fuera de alcance y decisiones aplazadas

  • Writer completo de .dkc y .dkk en TypeScript y la prueba TS → Go a nivel de cápsula: fase 3.
  • Los schemes pedersen-bls-unchained y bls-unchained-on-g1: sin uso previsto; si se añaden, el módulo crece con encryptOnG1 y decryptOnG1 y la puerta cambia de grupo.
  • Obtención de releases por red desde el SDK: fuera de esta fase; el servidor y la CLI Go siguen siendo la vía. Es lo que cubrirá los SHOULD de §48 (varios relays) y §49 (release directo del proveedor): una fuente drand opcional del SDK, que verifica cada respuesta y descarta las que fallan (corrección 6 de §76), nunca activa por defecto en la página.
  • Reutilización de la implementación age de tlock-js: descartada; age-encryption es la implementación oficial.

Powered by TurnKey Linux.