|
|
|
|
@ -1,564 +0,0 @@
|
|
|
|
|
# DateKeys, formato de cápsula 3: propuesta para revisión de seguridad
|
|
|
|
|
|
|
|
|
|
*30 de septiembre de 2026. Propuesta de ampliación del protocolo DateKeys v0.9, todavía sin implementar.*
|
|
|
|
|
|
|
|
|
|
## Para quien revisa
|
|
|
|
|
|
|
|
|
|
**Qué es.** Una ampliación del protocolo DateKeys: el formato de cápsula 3. Hasta la v0.9, una cápsula guarda un solo fichero, sin nombre ni fecha. El formato 3 guarda varios ficheros y carpetas, con sus nombres, tamaños, hashes y fechas de modificación. Añade además una firma de autor opcional y un sello de tiempo opcional, emitido por un servicio de DateKeys.
|
|
|
|
|
|
|
|
|
|
**Estado.** Es un diseño. El spec v0.9 existe, y sus dos implementaciones, la de referencia en Go y la de TypeScript para el navegador, están hechas y probadas byte a byte entre sí. Nada del formato 3 está programado todavía.
|
|
|
|
|
|
|
|
|
|
**Qué se pide.** Encontrar vulnerabilidades en el diseño, en particular:
|
|
|
|
|
- **Criptografía:** los enunciados que se firman y se sellan, los compromisos, el perfil Ed25519 estricto y los trasplantes entre cápsulas o formatos.
|
|
|
|
|
- **Privacidad:** qué se filtra antes de la fecha de apertura, qué ve el servicio de sellado y qué queda vinculado después.
|
|
|
|
|
- **El lector ante una cápsula maliciosa:** el análisis del CBOR, las reglas de rutas, la extracción a un ZIP o a una carpeta, y el consumo de memoria y de CPU.
|
|
|
|
|
- **El servicio de sellado:** la retrodatación, el robo de la clave y el registro auditable.
|
|
|
|
|
- **Ambigüedades** que dos implementaciones independientes podrían resolver de forma distinta.
|
|
|
|
|
|
|
|
|
|
**Cómo informar.** Para cada hallazgo:
|
|
|
|
|
- la gravedad: bloqueante, mayor o menor;
|
|
|
|
|
- un escenario concreto: qué entrada produce qué resultado;
|
|
|
|
|
- la sección afectada;
|
|
|
|
|
- una propuesta de arreglo.
|
|
|
|
|
|
|
|
|
|
**Ya revisado.** Una revisión adversarial interna encontró 28 problemas, todos corregidos; la lista va al final del documento. No hace falta repetirlos, pero sí comprobar que los arreglos son correctos.
|
|
|
|
|
|
|
|
|
|
Las referencias a ficheros y líneas (`framing.go:56`, `tempfile.ts:198`…) apuntan al código actual de las implementaciones. No hace falta abrirlas: el documento se entiende sin ellas.
|
|
|
|
|
|
|
|
|
|
## El protocolo base (v0.9) en una página
|
|
|
|
|
|
|
|
|
|
**Objetivo.** Cifrar hoy un fichero que nadie pueda abrir antes de una fecha, sin confiar en ningún servidor. La clave depende de una firma BLS que la red drand publica en esa fecha. drand Quicknet publica una ronda cada 3 segundos, y el cifrado usa tlock, un IBE sobre BLS12-381. Nadie tiene esa firma antes de la ronda, tampoco quien cifra.
|
|
|
|
|
|
|
|
|
|
**Estructura de una cápsula (`.dkc`)**, en este orden:
|
|
|
|
|
1. **PRELUDE**, 16 bytes en claro: la marca `DKC1`, `VERSION` (el formato de la cápsula), `PUBLIC_HEADER_LEN` y `SEALED_CONTROL_LEN`.
|
|
|
|
|
2. **PUBLIC_HEADER**, CBOR en claro: `capsule_id` (16 bytes aleatorios), la DateKey (el perfil del proveedor y la ronda), la política de acceso y extensiones públicas.
|
|
|
|
|
3. **SEALED_CONTROL = OUTER_TIME_AGE**, un fichero `age` con un único stanza `tlock` para la ronda. Dentro va una de dos cosas:
|
|
|
|
|
- con la política `time_only`, directamente `CONTROL_CBOR`;
|
|
|
|
|
- con `time_and_key`, `INNER_ACCESS_AGE`: un fichero `age` con exactamente 16 stanzas X25519, que contiene `CONTROL_CBOR`. Esos 16 stanzas son las credenciales (destinatarios `age1…` y, opcionalmente, una clave portable `.dkk`) más señuelos, en orden aleatorio.
|
|
|
|
|
4. **PAYLOAD_AGE**, un fichero `age` con un stanza X25519 para `R_PAYLOAD`. Su texto en claro es el contenido, L bytes, seguido de ceros hasta P = regla(L). Las reglas son `bloque256` y `reforzado`, que es max(bloque256, Padmé).
|
|
|
|
|
|
|
|
|
|
**`CONTROL_CBOR`** contiene:
|
|
|
|
|
- `header_binding`, el SHA-256 de PRELUDE ‖ PUBLIC_HEADER;
|
|
|
|
|
- `payload_identity`, que es `I_PAYLOAD`, la identidad X25519 cuya pública es `R_PAYLOAD`;
|
|
|
|
|
- las extensiones de control;
|
|
|
|
|
- `payload_length` (L);
|
|
|
|
|
- el código de la regla de relleno.
|
|
|
|
|
|
|
|
|
|
**Apertura** (§63, pasos 1 a 18):
|
|
|
|
|
- **Pasos 1 a 8:** estructura, sin red y sin secretos.
|
|
|
|
|
- **Paso 9:** la firma de la ronda (el release), que se da a mano o se descarga, y en `time_and_key`, la credencial.
|
|
|
|
|
- **Paso 10:** la firma BLS se verifica contra la clave pública del perfil, que va fijada en el software.
|
|
|
|
|
- **Paso 11:** se abre `OUTER_TIME_AGE`.
|
|
|
|
|
- **Paso 12:** estructura de la política.
|
|
|
|
|
- **Paso 13:** la credencial abre exactamente uno de los 16 stanzas de `INNER_ACCESS_AGE`.
|
|
|
|
|
- **Paso 14:** se decodifica `CONTROL_CBOR`.
|
|
|
|
|
- **Paso 15:** se comprueba `header_binding`.
|
|
|
|
|
- **Paso 16:** `I_PAYLOAD` y P.
|
|
|
|
|
- **Paso 17:** se abre `PAYLOAD_AGE` y se comprueba el relleno.
|
|
|
|
|
- **Paso 18:** se entrega.
|
|
|
|
|
|
|
|
|
|
Nada se presenta antes de autenticar el contenido entero (§56), y solo se entregan L bytes, nunca el relleno.
|
|
|
|
|
|
|
|
|
|
**Qué se ve antes de la fecha** (formato 2): la DateKey (la fecha), la política, `capsule_id` y el tamaño relleno. No se ve la longitud exacta ni el número de credenciales.
|
|
|
|
|
|
|
|
|
|
**La `.dkk`**, la clave portable, contiene:
|
|
|
|
|
- la identidad X25519 de acceso;
|
|
|
|
|
- `capsule_id`;
|
|
|
|
|
- el SHA-256 del `.dkc` exacto (`capsule_digest`).
|
|
|
|
|
|
|
|
|
|
No lleva MAC ni firma.
|
|
|
|
|
|
|
|
|
|
**Lo que v0.9 no garantiza** (§5, §36.1, §55.1):
|
|
|
|
|
- autoría: cualquiera puede cifrar hacia una fecha futura con la clave pública del perfil;
|
|
|
|
|
- fecha probatoria de creación;
|
|
|
|
|
- integridad frente a quien ya abrió la cápsula: puede cifrar otro `PAYLOAD_AGE` para `R_PAYLOAD` y volver a sellar el control;
|
|
|
|
|
- resistencia poscuántica.
|
|
|
|
|
|
|
|
|
|
**Implementaciones.** La de Go es la de referencia, con la CLI `datekeys`. La de TypeScript funciona en el navegador sin red: la página `/create` cifra y `/inspect` abre. Las dos son iguales byte a byte, con fixtures, vectores y mutaciones compartidas. Evitan dependencias nuevas: tienen su propio códec CBOR determinista. En criptografía usan noble (curves, hashes, ciphers) y `age-encryption` en TypeScript, y la biblioteca estándar, `filippo.io/age` y drand/tlock en Go.
|
|
|
|
|
|
|
|
|
|
## Decisiones del autor para el formato 3
|
|
|
|
|
|
|
|
|
|
Estas decisiones son el punto de partida del diseño, no algo a revisar. Un hallazgo puede, eso sí, mostrar que alguna es insegura.
|
|
|
|
|
|
|
|
|
|
1. **Formato 3.** El texto en claro de `PAYLOAD_AGE`, cifrado y bajo el relleno, pasa a ser tres bloques seguidos: `security | head | content`. Nada va en PUBLIC_HEADER, en `CONTROL_CBOR` ni en el nombre del fichero. Los formatos 1 y 2 siguen abriéndose, y un lector v0.9 rechaza el formato 3 en el paso 2.
|
|
|
|
|
2. **`head`** lleva:
|
|
|
|
|
- su propia versión;
|
|
|
|
|
- una sal aleatoria;
|
|
|
|
|
- un comentario opcional por cápsula;
|
|
|
|
|
- un autor declarado opcional, que es texto y nunca prueba nada;
|
|
|
|
|
- una entrada por fichero, con la ruta relativa (con carpetas), `size`, `start`, `end`, el SHA-256 y la fecha de modificación. `start`, `end` y `size` deben cuadrar entre sí.
|
|
|
|
|
|
|
|
|
|
No lleva tipo de contenido, hash global, permisos ni fecha de creación.
|
|
|
|
|
3. **La fecha de modificación** se toma sola al cargar cada fichero. Se incluye por defecto, con una casilla para quitarla, y es informativa.
|
|
|
|
|
4. **`security`** lleva una firma de autor Ed25519 y un sello de tiempo, los dos opcionales, sobre `head` y el contexto de la cápsula. Si van los dos, primero se firma y el sello cubre también la firma. Ninguno de los dos hace falta para abrir.
|
|
|
|
|
5. **El servicio de sellado** es de DateKeys, en el mismo origen que la página. Firma con una clave Ed25519 fijada en el protocolo. Lleva un registro de solo añadir, anclado periódicamente en OpenTimestamps. Sellar es opcional y pide consentimiento.
|
|
|
|
|
6. **Varios ficheros:** la página entrega un ZIP sin compresión que construye con código propio, y la CLI escribe el árbol en un directorio nuevo, nunca fuera de él.
|
|
|
|
|
|
|
|
|
|
## Modelo de amenazas
|
|
|
|
|
|
|
|
|
|
| Adversario | Qué tiene | Qué no debe conseguir |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| A1, observador antes de la fecha | El `.dkc`; quizá, el tráfico hacia el servicio de sellado | Nombres, número de ficheros, tamaños, hashes, fechas, si hay firma o sello, autor o comentario |
|
|
|
|
|
| A2, quien abre | Todo el contenido, después de la fecha | Hacer pasar una cápsula manipulada por la original, con firma o sello que lo respalden |
|
|
|
|
|
| A3, escritor malicioso | La libertad de fabricar cualquier cápsula | Que el lector escriba fuera de su carpeta o sobrescriba ficheros, agotarlo, o engañar con veredictos falsos, nombres que se confunden o un ZIP malicioso |
|
|
|
|
|
| A4, servicio de sellado deshonesto o comprometido | Su clave y su registro | Retrodatar sin que se note, o vincular las peticiones con quien crea la cápsula antes de la fecha |
|
|
|
|
|
| A5, red | El camino entre la página y el servicio | Alterar o suplantar el sello sin que se detecte |
|
|
|
|
|
| A6, tercero que verifica | El paquete de verificación, sin `I_PAYLOAD` | Obtener `I_PAYLOAD`, o un contenido que no está en el paquete |
|
|
|
|
|
|
|
|
|
|
## Preguntas concretas
|
|
|
|
|
|
|
|
|
|
1. ¿Atan `control_commit` y `head_digest` la firma y el sello a esta cápsula y solo a ella? ¿Se pueden trasplantar entre cápsulas, entre formatos o a un paquete de verificación?
|
|
|
|
|
2. ¿Es correcto el perfil «Ed25519 estricto» (§4 del diseño)? ¿Da el mismo resultado en Go 1.26 y en noble 2.4?
|
|
|
|
|
3. ¿Filtran algo antes de la fecha la trama `SECURITY_LEN ‖ HEAD_LEN`, el área de 512 bytes o la cota de ficheros que se deduce de P?
|
|
|
|
|
4. ¿Cierran R1 a R9 el escape de carpeta, las colisiones de nombres y los trucos de Windows (best-fit, alias 8.3, nombres reservados) al extraer a un ZIP y a una carpeta, en Windows, macOS y Linux?
|
|
|
|
|
5. ¿Garantizan la precedencia del paso 17 y la entrega atómica que nada se presenta antes de autenticarlo todo, y que Go y TypeScript dan el mismo código de error?
|
|
|
|
|
6. ¿Puede un escritor malicioso agotar memoria o CPU? Por ejemplo, con un head de 16 MiB, 65 535 ficheros, rutas largas o el plegado Unicode.
|
|
|
|
|
7. ¿Resiste el servicio de sellado la retrodatación como afirma el diseño, con ventanas de clave de un año, registro Merkle, anclaje horario en OpenTimestamps y `max_anchor_delay` de 26 h? ¿Qué pasa si roban la clave?
|
|
|
|
|
8. ¿Qué aprende el servicio, qué vincula después de la apertura, y basta con no guardar registros de acceso?
|
|
|
|
|
9. ¿Pueden inducir a error los textos de los veredictos? ¿Pueden suplantarlos el comentario o el autor declarado?
|
|
|
|
|
10. ¿Son seguras las claves de autor, con su formato `dkauthor1…`, el scrypt de logN 16 y la confianza en el primer uso condicionada al sello?
|
|
|
|
|
11. ¿Hay alguna ambigüedad de codificación que dos implementaciones resolverían de forma distinta?
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## El diseño del formato 3
|
|
|
|
|
|
|
|
|
|
*Base: la propuesta «crypto», con injertos de las otras cuatro y la revisión adversarial. Contrastado con la spec v0.9 (`datekeys-go` 7e2d83c) y App da26862 el 30-09-2026.*
|
|
|
|
|
|
|
|
|
|
### 1. Resumen
|
|
|
|
|
|
|
|
|
|
1. `VERSION` = 3; `CONTROL_CBOR` en schema 3 con las claves de la 2 (103 bytes: `SEALED_CONTROL_LEN` no cambia). No cambian `PUBLIC_HEADER`, la `.dkk`, los 16 huecos, L_MAX ni los códigos de relleno.
|
|
|
|
|
2. Plaintext de `PAYLOAD_AGE` = `BODY || 0x00^(P − L)`, `BODY = SECURITY_LEN || HEAD_LEN || SECURITY_CBOR || área || HEAD_CBOR || CONTENT`, L = |BODY| (clave 6).
|
|
|
|
|
3. HEAD: versión propia, sal, comentario, autor declarado y, por fichero, ruta, size, start, end, SHA-256 y mtime.
|
|
|
|
|
4. SECURITY: firma Ed25519 y sello, opcionales, en un área fija de 512 bytes que no cambia L ni P.
|
|
|
|
|
5. Se firma y se sella `control_commit || head_digest`. `control_commit` lleva un compromiso de `I_PAYLOAD`, no la clave, para que un tercero pueda verificar. El autor firma primero; el sello cubre la firma.
|
|
|
|
|
6. SECURITY nunca decide la apertura (objetivo 6): solo da veredictos de texto fijo.
|
|
|
|
|
7. Ed25519 estricto sin cofactor, igual en Go y TypeScript.
|
|
|
|
|
8. Rutas estrictas, con plegado Unicode 17.0.0 y proyecciones best-fit de Windows en tablas propias.
|
|
|
|
|
9. En el paso 17 prevalece cualquier fallo de `age` (`ERR_INTEGRITY`); si no lo hay, el orden del plaintext. Código nuevo `ERR_HEAD_INVALID`.
|
|
|
|
|
10. Entrega: fichero, ZIP propio o árbol en un directorio nuevo. Un lector v0.9 rechaza el formato 3 en el paso 2.
|
|
|
|
|
|
|
|
|
|
**Desacuerdos resueltos:**
|
|
|
|
|
|
|
|
|
|
- **Paso 17: primero STREAM.** `filippo.io/age` v1.3.2 (`stream.go:117-156`) entrega un chunk completo como no final antes de `ErrUnexpectedEOF`; `age-encryption` 0.3.1 solo lo prueba como final en `flush`. Si decidiera la posición, Go daría `ERR_HEAD_INVALID` y TypeScript `ERR_INTEGRITY`.
|
|
|
|
|
- **SECURITY sin código** (decisión 4); **HEAD con versión propia** (decisión 2), con el precedente del paso 14; **área de 512 bytes**.
|
|
|
|
|
- **Tablas Unicode propias:** Go 1.26.8 trae Unicode 15.0.0 y Node 24.9 el 16.0; un repertorio fijo rechazaría ¿, ° y emoji.
|
|
|
|
|
|
|
|
|
|
### 2. Estructura del contenido
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
BODY = SECURITY_LEN (uint32 BE) || HEAD_LEN (uint32 BE)
|
|
|
|
|
|| SECURITY_CBOR || 0x00^(A − SECURITY_LEN) A = 512·⌈SECURITY_LEN / 512⌉
|
|
|
|
|
|| HEAD_CBOR || CONTENT
|
|
|
|
|
1 ≤ SECURITY_LEN ≤ 65536 1 ≤ HEAD_LEN ≤ 2^24 8 + A + HEAD_LEN ≤ L ≤ L_MAX
|
|
|
|
|
C = L − 8 − A − HEAD_LEN; CONTENT = ficheros concatenados en el orden del head
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Cada CBOR ocupa exactamente sus bytes (`unmarshal` actual). Los buffers solo crecen con bytes autenticados, nunca según L, P o los size (§57).
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
security = { 0: "datekeys-security", 1: 1, ? 2: author-signature, ? 3: seal }
|
|
|
|
|
author-signature = { 0: alg (uint 1..2^32−1; 1 = Ed25519 puro),
|
|
|
|
|
1: bstr .size (1..8192), ; clave; alg 1: .size 32
|
|
|
|
|
2: bstr .size (1..8192) } ; firma; alg 1: .size 64
|
|
|
|
|
seal = { 0: seal_type (uint 1..2^32−1; 1 DateKeys; 2 RFC 3161 y 3 OTS reservados),
|
|
|
|
|
1: bstr .size (1..16384) }
|
|
|
|
|
head = { 0: "datekeys-head", 1: 1, 2: bstr .size 32, ; sal nueva de un CSPRNG
|
|
|
|
|
? 3: tstr .size (1..16384), ; comentario
|
|
|
|
|
? 4: tstr .size (1..256), ; autor declarado
|
|
|
|
|
? 5: [1*65535 file], ; orden ascendente estricto de bytes de ruta
|
|
|
|
|
? 6: [1*64 extension], ? 7: [1*64 extension] } ; críticas, no críticas
|
|
|
|
|
file = { 0: tstr .size (1..1024), ; ruta
|
|
|
|
|
1: uint, 2: uint, 3: uint, ; size, start, end exclusivo; ≤ L_MAX
|
|
|
|
|
4: bstr .size 32, ; SHA-256
|
|
|
|
|
? 5: uint .le 253402300799 } ; mtime, segundos UTC, informativa
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Los tamaños por `alg` son capa 3: con alg 1, una clave o una firma de otra longitud da «ilegible». Sin tipo de contenido, hash global, permisos ni fecha de creación.
|
|
|
|
|
|
|
|
|
|
**Tamaños:** SECURITY mide 22 bytes vacía, 128 con firma, 175 con sello y 281 con ambas. HEAD mínimo: 53 bytes; cada entrada, al menos 45. «nota.txt» de 1000 bytes: HEAD 117, L = 1637, P = 1792. Cápsula mínima: L = 573, P = 768.
|
|
|
|
|
|
|
|
|
|
**Maquetación:** start₀ = 0, startᵢ = endᵢ₋₁, endᵢ − startᵢ = sizeᵢ, comprobado con restas (en JS nunca se suman valores sin comprobar); end_último = C, o C = 0 sin ficheros.
|
|
|
|
|
|
|
|
|
|
**Texto.** «Espacio» es U+0020; MUST NOT usarse `trim`, `\s`, `TrimSpace` ni `unicode.IsSpace`.
|
|
|
|
|
|
|
|
|
|
- **Comentario:** prohibidos U+0000–0008, U+000B–001F (el escritor convierte CR LF y CR en LF), U+007F–009F, U+202A–202E, U+2066–2069, U+2028, U+2029, U+FEFF y no-caracteres.
|
|
|
|
|
- **Autor:** lo mismo, más TAB, LF, U+061C, U+200E y U+200F, sin U+0020 en los extremos.
|
|
|
|
|
|
|
|
|
|
Ningún texto ni ruta puede llevar ESC o C1 a un terminal.
|
|
|
|
|
|
|
|
|
|
**Capas de HEAD** (§69.1):
|
|
|
|
|
|
|
|
|
|
- **Capa 2:** un type tag ajeno da `ERR_NON_CANONICAL_CBOR`; una versión ≠ 1, `ERR_UNSUPPORTED_VERSION`.
|
|
|
|
|
- **Capa 3,** sobre todo el objeto (`ERR_NON_CANONICAL_CBOR`): §58, el CDDL (con R1), el orden de rutas (R8) y las extensiones.
|
|
|
|
|
- **Capa 4,** por clave:
|
|
|
|
|
- claves 3, 4 y 5 → `ERR_HEAD_INVALID`: cada entrada con R2–R6c y su maquetación, y tras el bucle R7 y R9;
|
|
|
|
|
- clave 6 → `ERR_EXTENSION_CRITICAL_UNKNOWN`, luego `ERR_EXTENSION_DATA_INVALID`.
|
|
|
|
|
|
|
|
|
|
### 3. Rutas y nombres
|
|
|
|
|
|
|
|
|
|
R1 y R8 son capa 3 sobre todo el array; las demás, capa 4. Así, [«b/..», «a»] da `ERR_NON_CANONICAL_CBOR`.
|
|
|
|
|
|
|
|
|
|
- **R1.** 1 a 1024 bytes UTF-8.
|
|
|
|
|
- **R2.** Separada por '/', de 1 a 32 segmentos no vacíos: sin rutas absolutas ni '//'.
|
|
|
|
|
- **R3.** Cada segmento mide de 1 a 255 bytes y no es «.» ni «..».
|
|
|
|
|
- **R4.** Puntos de código prohibidos:
|
|
|
|
|
- C0 y U+007F–009F;
|
|
|
|
|
- `" * : < > ? \ |`;
|
|
|
|
|
- los controles bidi: U+061C, U+200E, U+200F, U+202A–202E y U+2066–2069;
|
|
|
|
|
- U+2028, U+2029, U+FEFF, U+00AD, U+200B y U+2060–2064;
|
|
|
|
|
- los no-caracteres y todo Cn (sin asignar) de Unicode 17.0.0, que una versión futura podría plegar.
|
|
|
|
|
|
|
|
|
|
ZWJ, ZWNJ y VS16 se permiten.
|
|
|
|
|
- **R5.** Ningún segmento empieza por U+0020 ni termina en U+0020 o '.'.
|
|
|
|
|
- **R6.** El segmento hasta el primer '.', sin U+0020 finales y sin distinguir mayúsculas ASCII, no puede ser CON, PRN, AUX, NUL, CONIN$, CONOUT$, COM0–9, LPT0–9, COM¹²³ ni LPT¹²³.
|
|
|
|
|
- **R6b.** Alias 8.3. Se rechaza un segmento que:
|
|
|
|
|
- tiene como mucho un '.';
|
|
|
|
|
- antes de él, tiene de 1 a 8 caracteres, acabados en '~' seguido de 1 a 6 dígitos;
|
|
|
|
|
- después, tiene como mucho 3.
|
|
|
|
|
|
|
|
|
|
En NTFS «ABCDEF~1» resuelve a «ABCDEFGHIJ» (comprobado), así que `Root.MkdirAll` y el Explorador fusionarían dos carpetas distintas.
|
|
|
|
|
- **R6c.** Best-fit. Para cada `bestfit*.txt` de WindowsBestFit (fijadas por digest), se sustituye cada punto no ASCII que la tabla lleva a ASCII por ese byte. Se rechaza si alguna proyección contiene '/' o un carácter ASCII de R4, o incumple R3, R5, R6 o R6b. Cubre ∖, ∶, ¥ (cp932), ₩ (cp949), ´ (cp1253), las formas de ancho completo y «CON.txt».
|
|
|
|
|
- **R7.** Árbol plegado.
|
|
|
|
|
- Nodos: las rutas y sus prefijos.
|
|
|
|
|
- Clave de cada segmento: NFD(pliegue(NFD(s))), con el pliegue C+F de CaseFolding 17.0.0 más ı → i (NTFS).
|
|
|
|
|
- Se rechazan dos hermanos con la misma clave (A.txt/a.txt, NFC/NFD, «Fotos/a» y «fotos/b», Straße/STRASSE, Kelvin/k) y una ruta que es a la vez fichero y carpeta («A» y «a/b»).
|
|
|
|
|
- **R8.** Orden estrictamente ascendente de bytes (§54).
|
|
|
|
|
- **R9.** Como mucho 65535 carpetas implícitas.
|
|
|
|
|
|
|
|
|
|
**Tablas.** Un generador en `datekeys-go` las produce desde UCD 17.0.0 y WindowsBestFit, y emite código para los dos repos; sus suites comprueban el digest. MUST NOT usarse `normalize`, `toLowerCase`, el paquete `unicode` ni x/text.
|
|
|
|
|
|
|
|
|
|
**Escritor.**
|
|
|
|
|
|
|
|
|
|
- Guarda los nombres tal como llegan.
|
|
|
|
|
- Rechaza un UTF-16 mal formado y, con un mensaje claro, puntos de código posteriores a Unicode 17.
|
|
|
|
|
- No conserva carpetas vacías y deja editar las rutas.
|
|
|
|
|
- Excluye por defecto `.DS_Store`, `Thumbs.db`, `desktop.ini`, `._*` y `__MACOSX/`, que se muestran tachados con un conmutador.
|
|
|
|
|
|
|
|
|
|
**Nombres de salida.**
|
|
|
|
|
|
|
|
|
|
- El `.dkc` y la `.dkk` siguen llamándose `capsula-<fecha de apertura en UTC>`.
|
|
|
|
|
- Un fichero único se descarga con su nombre, pasado por `safeFileName`. En OPFS conserva el nombre fijo `TEMP_FILE` (`tempfile.ts:24`): sin Web Locks, `isStale` busca ese nombre y borraría el directorio de otra pestaña.
|
|
|
|
|
- El ZIP se llama `<primer segmento común>.zip` o `<nombre del .dkc>.zip`.
|
|
|
|
|
- Las rutas se muestran solo como texto, en un `<bdi dir=auto>`.
|
|
|
|
|
- Se avisa de `.lnk`, `.url`, `.library-ms`, `.searchConnector-ms`, `desktop.ini`, `.git`, los ejecutables y un '-' inicial.
|
|
|
|
|
|
|
|
|
|
### 4. Firma y sello
|
|
|
|
|
|
|
|
|
|
**Entradas.**
|
|
|
|
|
|
|
|
|
|
- `payload_commit = SHA-256("datekeys:dkc3:payload:v1" || 0x00 || I_PAYLOAD)`.
|
|
|
|
|
- `CONTROL_PUB` son los bytes de `CONTROL_CBOR` del paso 14, con los 32 de la clave 3 sustituidos por `payload_commit`; `control_commit = SHA-256(CONTROL_PUB)`. Cubre, sin revelar `I_PAYLOAD`:
|
|
|
|
|
- `header_binding`: PRELUDE, `capsule_id`, DateKey, política y extensiones públicas;
|
|
|
|
|
- L, el relleno y las extensiones de control;
|
|
|
|
|
- `I_PAYLOAD`, a través del compromiso.
|
|
|
|
|
- `head_digest` = SHA-256 de `HEAD_CBOR`; fija `CONTENT` byte a byte.
|
|
|
|
|
|
|
|
|
|
**Verificación por un tercero.** Recibe un paquete (`datekeys statement export|verify`) con PRELUDE, `PUBLIC_HEADER`, `CONTROL_PUB`, HEAD, SECURITY y los ficheros elegidos, nunca `I_PAYLOAD`. Revelar HEAD muestra los nombres, tamaños, mtimes y SHA-256 sin sal de todos los ficheros.
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
AUTHOR_MESSAGE = "datekeys:dkc3:author-signature:v1" || 0x00 || control_commit || head_digest ; 98 B
|
|
|
|
|
SIG_PART = 0x00 sin firma | 0x01 || SHA-256(mapa author-signature exacto)
|
|
|
|
|
SEAL_SUBJECT = SHA-256("datekeys:dkc3:seal-subject:v1" || 0x00 || control_commit || head_digest || SIG_PART)
|
|
|
|
|
SEAL_MESSAGE = "datekeys:seal:v1" || 0x00 || seal_profile_hash || SHA-256(SEAL_SUBJECT) || u64(t) || u64(índice) ; 97 B
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Solo `SEAL_SUBJECT` sale del dispositivo, y el registro publica su hash, nunca los 32 bytes recibidos. No se cubren los recipients (§55.2), los ciphertexts ni la `.dkk`.
|
|
|
|
|
|
|
|
|
|
**Ed25519 estricto** (§29.8; vale también para el token y los perfiles). Una firma es válida si y solo si:
|
|
|
|
|
|
|
|
|
|
1. |A| = 32 y |sig| = 64.
|
|
|
|
|
2. A es canónica (y < p, y el signo es 0 si y ∈ {1, p−1}), decodifica y no es de orden pequeño.
|
|
|
|
|
3. `sig[63] & 0xE0 = 0` y S < ℓ.
|
|
|
|
|
4. [S]B − [k]A codifica exactamente R, con k = SHA-512(R‖A‖M) mod ℓ.
|
|
|
|
|
|
|
|
|
|
En las dos librerías:
|
|
|
|
|
|
|
|
|
|
- Go 1.26.8 `ed25519.Verify` acepta A = 01 00…00, R = identidad y S = 0. Por eso Go comprueba A con código propio antes de llamarla.
|
|
|
|
|
- noble 2.4.0 `ed25519.verify` usa el cofactor y acepta ese mismo caso con `zip215: true`. MUST NOT llamarse, y una prueba de guarda lo impide. Se usa `Point.fromBytes(A, false)`, `isSmallOrder()`, `multiplyUnsafe` y `sha512`.
|
|
|
|
|
|
|
|
|
|
**Qué prueban.**
|
|
|
|
|
|
|
|
|
|
- **Firma válida(K):** quien tiene la clave privada de K firmó este head para este control. No prueba quién tiene K, ni cuándo firmó, ni la autoría, ni el autor declarado, ni que no haya otra cápsula con el mismo `capsule_id` (equivocación).
|
|
|
|
|
- **Sello válido:** según el servicio, todo existía en t, y si t < round_time, antes de que la cápsula pudiera leerse. Depende del operador; la auditoría acota esa dependencia a la ventana de anclaje.
|
|
|
|
|
- **Ataques:** tras la fecha, quien vuelve a sellar el control puede quitar la firma o firmar con K′, pero un sello nuevo tendría t ≥ round_time. Un trasplante a otro control invalida las dos. Contra la equivocación, se publica `capsule_digest` antes de la fecha.
|
|
|
|
|
|
|
|
|
|
**Presentación** (normativa):
|
|
|
|
|
|
|
|
|
|
- Primero van los veredictos.
|
|
|
|
|
- Después, «autor declarado (texto del creador, sin comprobar)» y el comentario, en un recuadro titulado «Comentario del creador (sin comprobar)».
|
|
|
|
|
- En la CLI, cada línea del autor y del comentario lleva el prefijo `│ `.
|
|
|
|
|
|
|
|
|
|
| Veredicto | Texto |
|
|
|
|
|
|---|---|
|
|
|
|
|
| firma ausente | «Sin firma de autor.» |
|
|
|
|
|
| firma ilegible o no soportada | «No se ha comprobado ninguna firma: trátala como no firmada.» |
|
|
|
|
|
| firma inválida | «La firma no corresponde a este contenido.» |
|
|
|
|
|
| válida, clave guardada | «Firmado con la clave que guardaste como ‹etiqueta›.» |
|
|
|
|
|
| válida, otra clave | «Firmado con la clave dkauthor1… (completa). No prueba quién la tiene.» |
|
|
|
|
|
| sello ausente, ilegible o no soportado | nada sobre la fecha |
|
|
|
|
|
| sello inválido | «El sello no corresponde a este contenido.» |
|
|
|
|
|
| válido, t < round_time | «Según el servicio de sellado de DateKeys, existía el ‹t›, antes de que la cápsula pudiera abrirse.» |
|
|
|
|
|
| válido, t ≥ round_time | «Sellado después de la fecha de apertura: no prueba nada anterior.» |
|
|
|
|
|
| perfil comprometido | «Firma del servicio válida, pero su clave está comprometida: requiere auditoría.» |
|
|
|
|
|
|
|
|
|
|
Una mtime posterior a t se muestra como incoherencia.
|
|
|
|
|
|
|
|
|
|
### 5. El servicio de sellado
|
|
|
|
|
|
|
|
|
|
**API.**
|
|
|
|
|
|
|
|
|
|
- `POST /seal/v1` en el mismo origen, con 32 bytes `application/octet-stream`.
|
|
|
|
|
- `fetch` con `credentials:'omit'`, `referrerPolicy:'no-referrer'`, `cache:'no-store'` y `redirect:'error'`, en un único módulo con prueba de guarda.
|
|
|
|
|
- Respuestas: 200 con el token de 146 bytes; 400, 429 o 503.
|
|
|
|
|
- La librería no usa la red: recibe un `sealer(subject)`.
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
seal-token-1 = { 0: "datekeys-seal-token", 1: 1, 2: bstr .size 32 (seal_profile_hash),
|
|
|
|
|
3: bstr .size 8 (t), 4: bstr .size 8 (índice), 5: bstr .size 64 }
|
|
|
|
|
seal-profile = { 0: "datekeys-seal-profile", 1: 1, 2: "datekeys:seal:v1",
|
|
|
|
|
3: tstr .size (1..256) (log_origin), 4: bstr .size 32 (clave de token),
|
|
|
|
|
5: bstr .size 32 (clave de checkpoints), 6: bstr .size 8 (not_before),
|
|
|
|
|
7: bstr .size 8 (not_after), 8: bstr .size 8 (max_anchor_delay) }
|
|
|
|
|
; u64 big-endian en segundos, ≤ 2^53−1
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Servicio.** Calcula t = 60·⌈ahora/60⌉ con un reloj disciplinado, añade la hoja `SEAL_MESSAGE` de forma duradera y replicada, y después firma y responde.
|
|
|
|
|
|
|
|
|
|
**Perfil.**
|
|
|
|
|
|
|
|
|
|
- `seal_profile_hash` se fija como Quicknet; la spec imprime los bytes y el hash.
|
|
|
|
|
- La ventana es de un año, y la clave de tokens se destruye de forma documentada en not_after. Los perfiles viejos se conservan.
|
|
|
|
|
- El estado vive en el registro de §71 fijado en el software. Un compromiso marca **todos** los tokens del perfil, sea cual sea su t, porque el ladrón elige t; la fecha T solo sirve a la auditoría.
|
|
|
|
|
|
|
|
|
|
**Verificación sin red** (17.6):
|
|
|
|
|
|
|
|
|
|
- `seal_type`, versión o perfil desconocidos → no soportado.
|
|
|
|
|
- Un token que no decodifica → ilegible.
|
|
|
|
|
- t fuera de [not_before, not_after], índice > 2⁵³ − 1 o firma estricta que falla → inválido.
|
|
|
|
|
- Si no, válido, con antes_de_apertura = t < round_time y la marca de compromiso que tenga el registro en esa versión del lector.
|
|
|
|
|
|
|
|
|
|
**Registro y anclaje.**
|
|
|
|
|
|
|
|
|
|
- Árbol Merkle RFC 9162, checkpoints C2SP al menos cada 60 s con su propia clave, y tiles estáticos replicables.
|
|
|
|
|
- Cada hora, el último checkpoint va a al menos dos calendarios de OpenTimestamps; la prueba se publica en `/seal/v1/anchor/<tamaño>.ots`.
|
|
|
|
|
- Invariantes que vigilan los monitores: los tiempos no decrecen, y t − 2 h ≤ t_bloque ≤ t + max_anchor_delay (26 h). La tolerancia inferior absorbe la holgura de los tiempos de bloque de Bitcoin.
|
|
|
|
|
- El anclaje **acota** la retrodatación a max_anchor_delay, pero no la impide. Por eso el veredicto auditado «antes de la apertura» exige t_bloque + 2 h < round_time. En una fase 2, testigos tlog-witness la acotarán a minutos.
|
|
|
|
|
- La auditoría usa la red, es opcional y queda fuera de §63 (`datekeys seal audit`). La página no puede verificar Bitcoin bajo su CSP, y lo dice.
|
|
|
|
|
|
|
|
|
|
**Privacidad.**
|
|
|
|
|
|
|
|
|
|
- Antes de la apertura, el servicio ve la IP, el User-Agent, la hora y 32 bytes que no puede vincular.
|
|
|
|
|
- Después, quien abre recalcula el subject y encuentra la hoja: el sello vincula la cápsula con ese POST.
|
|
|
|
|
- Por eso `/seal/v1` MUST NOT guardar registros de acceso, tampoco en el hosting ni en el CDN, ni hacer analítica. Lo dicen el texto de consentimiento y §55.2.
|
|
|
|
|
|
|
|
|
|
**Alternativas descartadas.**
|
|
|
|
|
|
|
|
|
|
- **RFC 3161 dentro de la cápsula:** exigiría DER, CMS y X.509 con código propio, tokens de 2 a 6 KB y revocación durante décadas. Queda reservado `seal_type` 2; eIDAS puede llegar después, sobre los checkpoints.
|
|
|
|
|
- **OpenTimestamps dentro de la cápsula:** al crear, la prueba está pendiente. Queda reservado `seal_type` 3.
|
|
|
|
|
|
|
|
|
|
### 6. Claves de autor
|
|
|
|
|
|
|
|
|
|
**Formato.**
|
|
|
|
|
|
|
|
|
|
- Semilla Ed25519 de 32 bytes.
|
|
|
|
|
- Clave pública `dkauthor1…` (bech32, 67 caracteres); clave secreta `DKAUTHOR-SECRET-KEY-1…` (79 caracteres).
|
|
|
|
|
- El fichero imita una identidad de `age` y por defecto va envuelto en `age` scrypt con logN = 16 (64 MiB). El `scrypt` de noble es síncrono (`recipients.js:358`), y el 18 por defecto (256 MiB) bloquea o agota un móvil. La página avisa de la pausa.
|
|
|
|
|
|
|
|
|
|
**Custodia.**
|
|
|
|
|
|
|
|
|
|
- **Página:** `crypto.getRandomValues` y `ed25519.getPublicKey`. El fichero se ofrece una sola vez y nunca se guarda en el navegador; la semilla se borra tras firmar.
|
|
|
|
|
- **CLI:** `author keygen -out FICHERO` (0600, nunca sobrescribe), `author public` y `encrypt -sign`. La passphrase se lee de `-passphrase-file`, o sin eco con código propio sobre `golang.org/x/sys`, que ya está en `go.mod` como indirecta: ningún módulo nuevo.
|
|
|
|
|
- La API expone `AUTHOR_MESSAGE` para que firme un dispositivo externo.
|
|
|
|
|
|
|
|
|
|
**Cómo se conoce la clave.** Por un canal externo o con `-expect-author`. La página ofrece «guardar esta clave como…» solo si:
|
|
|
|
|
|
|
|
|
|
- la firma está cubierta por un sello válido, no comprometido y con t < round_time, lo que excluye a quien volvió a firmar tras la fecha;
|
|
|
|
|
- la etiqueta no se rellena con el autor declarado;
|
|
|
|
|
- la persona confirma la clave completa por otro canal.
|
|
|
|
|
|
|
|
|
|
Ninguna decisión se basa en una huella truncada.
|
|
|
|
|
|
|
|
|
|
### 7. Escritor y lector
|
|
|
|
|
|
|
|
|
|
**Escritor**, en dos fases (`prepareFiles` y `write`):
|
|
|
|
|
|
|
|
|
|
- **Fase A, sin red ni secretos:**
|
|
|
|
|
- Rutas y mtime (⌊lastModified/1000⌋ o `ModTime().Unix()`, con la casilla marcada por defecto).
|
|
|
|
|
- La CLI recorre con `Lstat` y acepta solo ficheros regulares. Cada directorio de `-in` aporta su nombre como primer segmento, igual que `webkitRelativePath`.
|
|
|
|
|
- Offsets y |HEAD| con los hashes a cero. Se rechaza |HEAD| > 16 MiB o L > L_MAX. El tamaño es exacto (`capsuleLength3`) y se comprueba la cuota.
|
|
|
|
|
- Pasada 1: SHA-256 en piezas de 64 KiB.
|
|
|
|
|
- **Fase B:**
|
|
|
|
|
1. §61 y §62 con `VERSION` 3.
|
|
|
|
|
2. La sal y `HEAD_CBOR`.
|
|
|
|
|
3. `CONTROL_CBOR` v3, `control_commit` y `head_digest`.
|
|
|
|
|
4. La firma, verificada (MUST).
|
|
|
|
|
5. Con consentimiento, el sello. MUST verificarse el token y MUST cumplirse t < round_time; si no, se aborta. Se avisa si |t − reloj| > 10 min. Si el sello falla, se sigue sin él o se cancela, y `plan.size` no cambia.
|
|
|
|
|
6. `SECURITY_CBOR` y la autodecodificación de los tres CBOR (MUST).
|
|
|
|
|
7. `INNER_ACCESS_AGE` y `OUTER_TIME_AGE`.
|
|
|
|
|
8. La escritura, con una pasada 2 que aborta si un tamaño o un hash difieren.
|
|
|
|
|
9. La `.dkk` con `capsule_digest`.
|
|
|
|
|
|
|
|
|
|
**Lector.**
|
|
|
|
|
|
|
|
|
|
- El paso 2 acepta los formatos 1 a 3; los pasos 12 y 13 esperan 16 stanzas; en el paso 14, la versión de schema debe coincidir con `VERSION`.
|
|
|
|
|
- Paso 17:
|
|
|
|
|
- **17.1** `PAYLOAD_AGE`.
|
|
|
|
|
- **17.2** Trama y área → `ERR_INTEGRITY`.
|
|
|
|
|
- **17.3** SECURITY, capas 2 y 3, sin código.
|
|
|
|
|
- **17.4** HEAD, capas 2 a 4.
|
|
|
|
|
- **17.5** end_último = C → si no, `ERR_INTEGRITY`.
|
|
|
|
|
- **17.6** Veredictos.
|
|
|
|
|
- **17.7** SHA-256 de cada fichero → `ERR_INTEGRITY`.
|
|
|
|
|
- **17.8** Relleno → `ERR_INTEGRITY`.
|
|
|
|
|
|
|
|
|
|
**Precedencia** (§69.1 y final del paso 17).
|
|
|
|
|
|
|
|
|
|
- Un fallo de `age` después de su cabecera, o un plaintext de longitud ≠ P, es `ERR_INTEGRITY` y prevalece.
|
|
|
|
|
- Si no hay ninguno, decide el primer fallo de 17.2 a 17.8 y, dentro de él, la primera capa.
|
|
|
|
|
- Tras un fallo de bloque, el lector deja de entregar y sigue autenticando hasta EOF.
|
|
|
|
|
- Solo MAY detenerse antes con `ERR_INTEGRITY`. Los demás códigos del paso 17 MUST NOT informarse antes de autenticar hasta EOF con P bytes, así que el momento de la detección sigue sin cambiar el código.
|
|
|
|
|
|
|
|
|
|
Ejemplos:
|
|
|
|
|
|
|
|
|
|
- «..» y un chunk posterior corrupto → `ERR_INTEGRITY`.
|
|
|
|
|
- «..» y un byte de relleno ≠ 0 → `ERR_HEAD_INVALID`.
|
|
|
|
|
- Corte exacto tras el chunk que termina el head → `ERR_INTEGRITY` en los dos lenguajes.
|
|
|
|
|
|
|
|
|
|
**Entrega** (§56). Nada se presenta antes del paso 18. El sumidero tiene `Begin(head)`, `Create(i)`, `Commit` y `Abort`.
|
|
|
|
|
|
|
|
|
|
`Open` con solo `dst` y `open` sin sumidero terminan tras el paso 2 con un error del llamador sin código normativo (`ErrSinkRequired` con `Code() == ""`; `TypeError`), nunca con `ERR_UNSUPPORTED_VERSION`. Una prueba compartida lo fija con `format3_single`.
|
|
|
|
|
|
|
|
|
|
- **Página.**
|
|
|
|
|
- Un fichero de un solo segmento va al fichero fijo de OPFS; sin ficheros, solo se muestra el comentario.
|
|
|
|
|
- En los demás casos, un ZIP propio: entradas almacenadas, bit 11, sin descriptores de datos ni entradas de carpeta.
|
|
|
|
|
- El CRC-32 se parchea con escrituras posicionadas (`TempFileHandle` pasa a `FileSystemWritableFileStream`).
|
|
|
|
|
- ZIP64 cuando hay 0xFFFF entradas o más, o algún tamaño u offset ≥ 0xFFFFFFFF.
|
|
|
|
|
- Tiempos: hora DOS en UTC, recortada a 1980–2107. El extra 0x000A va siempre y es el que manda; después, 0x5455 solo si 0 ≤ t ≤ 2³¹ − 1. Una entrada sin mtime recibe round_time.
|
|
|
|
|
- **CLI, `decrypt -out DIR`:**
|
|
|
|
|
1. `os.Mkdir(DIR, 0700)` reclama DIR y falla si ya existe. Un `os.Rename` final no serviría: en POSIX sustituiría un DIR vacío.
|
|
|
|
|
2. El árbol se escribe en `DIR/.datekeys-*` con `os.OpenRoot`, `O_EXCL`, permisos 0600 y `Root.Chtimes`.
|
|
|
|
|
3. Tras el paso 18, `Root.Rename` lleva cada entrada al primer nivel de DIR; ante cualquier fallo, `RemoveAll(DIR)`.
|
|
|
|
|
4. Una cápsula sin ficheros no crea DIR.
|
|
|
|
|
5. Los veredictos, el autor y el comentario van a stdout, en el orden de §4.
|
|
|
|
|
|
|
|
|
|
### 8. Qué ve cada uno
|
|
|
|
|
|
|
|
|
|
- **Quien tiene el `.dkc` antes de la fecha:**
|
|
|
|
|
- lo mismo que en el formato 2, más `VERSION` = 3;
|
|
|
|
|
- no ve si hay firma o sello;
|
|
|
|
|
- P acota el número de ficheros (n ≤ (P − 575)/45, es decir, 4 con P = 768) y la longitud de nombres y comentario.
|
|
|
|
|
- **El servicio:** sin sello, ninguna petición. Con sello, la IP, el User-Agent, la hora y 32 bytes que no puede vincular hasta la apertura.
|
|
|
|
|
- **Quien abre:**
|
|
|
|
|
- rutas, tamaños, SHA-256 y mtimes;
|
|
|
|
|
- el comentario y el autor declarado;
|
|
|
|
|
- la clave pública, que vincula entre sí las cápsulas firmadas con ella;
|
|
|
|
|
- t al minuto, que aproxima la fecha de creación (el texto de consentimiento lo dice);
|
|
|
|
|
- el índice y el perfil, que vinculan la cápsula con el POST.
|
|
|
|
|
|
|
|
|
|
### 9. Compatibilidad y versiones
|
|
|
|
|
|
|
|
|
|
- Un lector v0.9 rechaza `VERSION` 3 en el paso 2 (`framing.go:56`, `framing.ts:44`). Un lector v0.10 acepta 1, 2 y 3; los formatos 1 y 2 no cambian.
|
|
|
|
|
- Cambiar la `VERSION` entre 2 y 3 falla en el paso 14, o antes, en el 9.a. Volver a sellar bajo el formato 2 es una reescritura (§70).
|
|
|
|
|
- Los escritores v0.10 MUST escribir el formato 3. El escritor de un solo flujo (`Encrypt`, `encrypt()`) queda tras una opción solo para pruebas (§62.1), para los generadores.
|
|
|
|
|
- **Pruebas que cambian:**
|
|
|
|
|
- spec §64;
|
|
|
|
|
- `mutations.json` (`[4,1,"03"]` pasa a `"04"`);
|
|
|
|
|
- `framing.test.ts`, `inspect.test.ts`, `prefix.test.ts`, `format.ts:40` y `framing_test.go`;
|
|
|
|
|
- las del escritor que esperan el formato 2 (`encrypt.test.ts:116`, `encrypt_test.go`, `format2_test.go`).
|
|
|
|
|
- **Siguen igual:** `control.test.ts:125` y `framing_test.go:208`.
|
|
|
|
|
|
|
|
|
|
### 10. Cambios en la spec, plan y pruebas
|
|
|
|
|
|
|
|
|
|
**Spec v0.10:**
|
|
|
|
|
|
|
|
|
|
- §4, objetivo 9: autenticidad y fecha opcionales.
|
|
|
|
|
- §5 y §7.9–§7.10: equivocación, servicio comprometido y clave robada.
|
|
|
|
|
- §22, §23, §27 y §31.
|
|
|
|
|
- §29.2 a §29.10: trama, SECURITY, HEAD, rutas, texto, enunciados, Ed25519, sello y presentación.
|
|
|
|
|
- §55.1, §55.2, §56 y §57.
|
|
|
|
|
- §62.1, reglas 13 a 18.
|
|
|
|
|
- §63, paso 17.
|
|
|
|
|
- §69 y §69.1: precedencia y Alcance.
|
|
|
|
|
- §71: perfiles de sello.
|
|
|
|
|
- `datekeys.cddl`.
|
|
|
|
|
- Documento aparte: «Servicio de sellado DateKeys v1».
|
|
|
|
|
|
|
|
|
|
**Código:**
|
|
|
|
|
|
|
|
|
|
- **Go:** `capsule` (`Format3`, `head`, `security`, `pathkey`, `ed25519strict`, sumidero), `seal`, `authorkey` y la CLI (`-in` con directorios, `-comment`, `-author`, `-no-mtime`, `-sign`, `-seal`, `decrypt -out DIR`, `author` y `statement`).
|
|
|
|
|
- **TypeScript:** los mismos módulos, más `seal-verify`, `zip.ts`, `crc32.ts` y guardas contra `ed25519.verify` y `fetch`.
|
|
|
|
|
|
|
|
|
|
**Pruebas.**
|
|
|
|
|
|
|
|
|
|
- **Fixtures:** `format3_single`, `_tree`, `_comment_only`, `_signed`, `_signed_sealed`, `_sealed_after`, `_security_v2`, `_bloque256` y `_time_and_key_portable`.
|
|
|
|
|
- **Vectores:**
|
|
|
|
|
- `statements.json`, con alg 1 de 33 y 63 bytes;
|
|
|
|
|
- `ed25519_strict.json`, con los casos de «Taming the many EdDSAs»;
|
|
|
|
|
- `paths.json` y `head_schema.json`: U+00A0 y U+3000 en los extremos (se aceptan), best-fit, 8.3, Cn y [«b/..», «a»];
|
|
|
|
|
- `path_fold.json`;
|
|
|
|
|
- `seal_token.json`, con un token retrodatado bajo un perfil comprometido;
|
|
|
|
|
- `zip.json`: mtime 1, 2³¹, 2040 y 253402300799, y 65535 entradas.
|
|
|
|
|
- **Mutaciones:**
|
|
|
|
|
- `VERSION` 4, y de 3 a 2 y de 2 a 3;
|
|
|
|
|
- la trama y el área;
|
|
|
|
|
- cada regla de rutas y de texto;
|
|
|
|
|
- head v2 y end_último ≠ C;
|
|
|
|
|
- la precedencia, con el corte exacto;
|
|
|
|
|
- la firma alterada, quitada o rehecha, t cambiada y el trasplante.
|
|
|
|
|
- **Otras:**
|
|
|
|
|
- un HEAD de 16 MiB y otro de 16 MiB + 1;
|
|
|
|
|
- diferenciales entre Go y TypeScript;
|
|
|
|
|
- `archive/zip` y el extra 0x000A;
|
|
|
|
|
- la CLI nunca escribe fuera de DIR;
|
|
|
|
|
- un lector que informa `ERR_HEAD_INVALID` antes de EOF no es conforme.
|
|
|
|
|
|
|
|
|
|
### 11. Preguntas abiertas (con recomendación)
|
|
|
|
|
|
|
|
|
|
1. ¿Código nuevo `ERR_HEAD_INVALID`? Sí: separa un escritor defectuoso de un CBOR roto.
|
|
|
|
|
2. ¿Área de 512 bytes? Sí: cuesta unos 490 bytes y oculta la firma y el sello.
|
|
|
|
|
3. ¿Tablas propias? Sí: la regla ligera deja pasar colisiones NFC/NFD.
|
|
|
|
|
4. ¿Aceptar que un head nuevo falle tras la fecha? Sí.
|
|
|
|
|
5. ¿Clave con ventana? Sí, de un año y con destrucción documentada; la raíz offline, más adelante.
|
|
|
|
|
6. ¿Hora del sello al minuto? Sí.
|
|
|
|
|
7. ¿Sello RFC 3161 cualificado? En una segunda fase.
|
|
|
|
|
8. ¿Escribir solo el formato 3? Sí.
|
|
|
|
|
9. Límites: comentario 16 KiB, autor 256 B, ruta 1024 B, segmento 255 B, profundidad 32, 65535 ficheros y head 16 MiB. Falta confirmarlos.
|
|
|
|
|
10. ¿Un único fichero dentro de una carpeta? En ZIP.
|
|
|
|
|
11. ¿Fichero de clave cifrado por defecto? Sí, con logN 16.
|
|
|
|
|
12. ¿Cápsulas solo con comentario? El lector acepta C = 0; el escritor exige un fichero o un comentario.
|
|
|
|
|
13. ¿Rechazar un '-' inicial? No, solo avisar.
|
|
|
|
|
14. ¿Testigos de checkpoints? En la fase 2.
|
|
|
|
|
|
|
|
|
|
### Revisión adversarial
|
|
|
|
|
|
|
|
|
|
Las 28 objeciones son reales; no se descarta ninguna.
|
|
|
|
|
|
|
|
|
|
1. **`control_digest` revela `I_PAYLOAD` (mayor).** Corregido: se firma `control_commit`, con `payload_commit`, y un paquete de verificación (§4). Se usa un hash de `I_PAYLOAD` en vez de `R_PAYLOAD`: con `R_PAYLOAD`, quien recibe el paquete podría cifrar otro `PAYLOAD_AGE` que el control abriría.
|
|
|
|
|
2. **El anclaje deja retrodatar hasta 26 h (mayor).** Corregido: el anclaje «acota», el veredicto auditado exige t_bloque + 2 h < round_time y los testigos llegan en la fase 2. Refina la decisión 5, no la sustituye.
|
|
|
|
|
3. **«Comprometido desde T» (mayor).** Corregido: marca todos los tokens del perfil; la clave se destruye en not_after y la ventana es de un año.
|
|
|
|
|
4. **Lista best-fit incompleta (mayor).** Corregido con R6c, generada. Solo se proyecta lo que la tabla lleva a ASCII, para no confundir con sintaxis el '?' por defecto.
|
|
|
|
|
5. **TOFU.** Corregido (§6).
|
|
|
|
|
6. **Un comentario que imita veredictos.** Corregido: presentación normativa y texto nuevo para la firma no soportada.
|
|
|
|
|
7. **«Espacio» sin definir.** Corregido: es U+0020.
|
|
|
|
|
8. **Alias 8.3.** Corregido con R6b; comprobado en esta máquina. Git solo protege `.git`; aquí se generaliza.
|
|
|
|
|
9. **Vinculación tras la apertura.** Corregido (§5 y §8).
|
|
|
|
|
10. **Canal de difusión.** Corregido: se publica el hash del subject y `fetch` queda aislado.
|
|
|
|
|
11. **MAY de fallo rápido (mayor).** Corregido. Comprobado que contradecía el Alcance de §69.1 y el final del paso 17.
|
|
|
|
|
12. **Compromiso sin red (mayor).** Igual que 3, con un vector.
|
|
|
|
|
13. **Tiempos del ZIP (mayor).** Corregido byte a byte.
|
|
|
|
|
14. **`Open` sin sumidero.** Corregido: error del llamador, como ya hacen `DecodeControl` con el formato 3 (`framing_test.go:243`) y `open` (`open.ts:139-142`).
|
|
|
|
|
15. **Precedencia de R1 y R8.** Corregido.
|
|
|
|
|
16. **Best-fit (repetida).** Igual que 4.
|
|
|
|
|
17. **Tamaños con alg 1.** Corregido: capa 3, «ilegible».
|
|
|
|
|
18. **«Espacio» (repetida).** Igual que 7.
|
|
|
|
|
19. **HEAD de más de 16 MiB.** Corregido en la fase A.
|
|
|
|
|
20. **Carpetas y ZIP64.** Corregido.
|
|
|
|
|
21. **Nombre del fichero en OPFS.** Corregido; comprobado en `isStale` (`tempfile.ts:198`).
|
|
|
|
|
22. **Salida de la CLI.** Corregido: `Mkdir`, `Root.Rename` (existe en Go 1.26), stdout, cápsulas sin ficheros y `-in`.
|
|
|
|
|
23. **Número de ficheros visible.** Corregido: P da una cota (§8).
|
|
|
|
|
24. **Tolerancia inferior del ancla.** Igual que 2.
|
|
|
|
|
25. **Puntos de código sin asignar.** Corregido: Cn entra en R4.
|
|
|
|
|
26. **scrypt y passphrase.** Corregido: logN 16; `x/sys` ya es indirecta.
|
|
|
|
|
27. **Pruebas del escritor de formato 2.** Corregido: opción solo para pruebas (`encrypt.test.ts:116`).
|
|
|
|
|
28. **CDDL del perfil de sello.** Corregido.
|