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.

72 KiB

DateKeys, formato de cápsula 3 (spec v0.10)

Diseño, todavía sin implementar. 30 de septiembre de 2026. Contrastado con el spec v0.9 (datekeys-go 7e2d83c, tag spec-v0.9) y con datekeys-ts da26862. Incorpora la revisión adversarial interna, la revisión de seguridad de Fable (revision_fable.md), las decisiones del autor sobre ella y una revisión adversarial de esta versión. Es la única fuente del diseño: sustituye a formato3_revision_intro.md y a formato3_para_revision.md, que repetían su texto.

Cómo leer este documento

  • La parte 0 da el contexto a quien llega de fuera: el protocolo v0.9 en una página, un glosario, las decisiones del autor y el modelo de amenazas.

  • Las partes 1 a 3 son las tres entregas. El formato se congela en la primera. Las entregas 2 y 3 no cambian ningún byte de la trama ni del head: definen qué va dentro del área security y cómo se comprueba. Un lector de una entrega anterior muestra «no soportado» para lo nuevo y abre igual.

    • Entrega 1: contenedor, varios ficheros y rutas, con security presente y vacío.
    • Entrega 2: firma de autor Ed25519 y claves de autor.
    • Entrega 3: sello de tiempo, cuando exista el documento «Servicio de sellado DateKeys v1».

    Cada entrega es una versión del spec: la 1 es la v0.10, y la 2 y la 3 abrirán las versiones siguientes, porque una versión cerrada no cambia.

  • Los anexos recogen las revisiones: dónde se resuelve cada hallazgo de Fable, las 24 objeciones de la revisión interna y los hallazgos de la revisión adversarial de esta versión.

Los § son del spec v0.9; los apartados de este documento se citan como «apartado 3». Las referencias a código (framing.go:56, tempfile.ts:198…) apuntan a las implementaciones actuales y no hace falta abrirlas.

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 que ya pasó tres revisiones. 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.

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 claves, el abuso y la verificación a décadas vista.
  • 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;
  • el apartado afectado;
  • una propuesta de arreglo.

Ya revisado. Los anexos A, B y C recogen las revisiones anteriores y cómo se resolvió cada punto. No hace falta repetirlos, pero sí comprobar que los arreglos son correctos.

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? Entregas 2 y 3.
  2. ¿Es correcto el perfil «Ed25519 estricto» (apartado 11)? ¿Da el mismo resultado en Go 1.26 y en noble 2.4? Entrega 2.
  3. ¿Filtran algo antes de la fecha la trama del contenido, el área de tamaño fijo o la cota de ficheros que se deduce de P? Entrega 1.
  4. ¿Cierran R1 a R10 el escape de carpeta, las colisiones de nombres y los trucos de Windows, de macOS y de Linux (best-fit, alias 8.3, nombres reservados, caracteres ignorables, casefold) al extraer a un ZIP y a una carpeta? Entrega 1.
  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? Entrega 1.
  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. Entrega 1.
  7. ¿Pueden inducir a error los textos de los veredictos? ¿Pueden suplantarlos el comentario o el autor declarado? Entregas 1 a 3.
  8. ¿Son seguras las claves de autor, con su formato dkauthor1…, el scrypt de logN 16 y la regla para guardar una clave (apartado 14)? Entrega 2.
  9. ¿Resiste el servicio de sellado la retrodatación y el robo de claves, con la prueba dentro de la cápsula, la raíz offline, las claves anuales, el anclaje en OpenTimestamps y un max_anchor_delay de 26 h? ¿Qué aprende, qué vincula después de la apertura, y basta con no guardar registros de acceso? Entrega 3.
  10. ¿Hay alguna ambigüedad de codificación que dos implementaciones resolverían de forma distinta?

Parte 0. Contexto

0.1 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.

0.2 Glosario

  • L y P. L es la longitud real del texto en claro de PAYLOAD_AGE, que va en la clave 6 de CONTROL_CBOR. P = regla(L) es la longitud con relleno, la que se cifra. En el formato 3, L es la longitud de BODY (apartado 2).
  • L_MAX = 2⁵³ − 2⁴⁶, el mayor L admitido (§29.1). Es múltiplo de 2⁴⁶, el mayor redondeo de Padmé por debajo de 2⁵³, así que P nunca pasa de L_MAX y todo cabe en un entero exacto de JavaScript.
  • Capas de validación (§69.1). Cada objeto CBOR se valida en cuatro capas, en este orden, y se informa el código de la primera que falla:
    1. trama: las longitudes y los límites del contenedor;
    2. tipo y versión: el type tag de la clave 0 y la versión de la clave 1;
    3. codificación y schema: el perfil CBOR canónico (§58) y el CDDL, con tipos, tamaños y rangos, que dan ERR_NON_CANONICAL_CBOR;
    4. campos con código propio, en orden ascendente de clave.
  • Trama del contenido: los 12 primeros bytes de BODY, con AREA_LEN, SECURITY_LEN y HEAD_LEN (apartado 2).
  • Área: el espacio reservado para SECURITY_CBOR, completado con ceros. Su tamaño lo fija la versión del spec del escritor, no lo que lleva, así que P no revela si se eligió firmar o sellar. La única excepción es la fuga del área de 512 (apartado 0.4).
  • Sal del head: 32 bytes de un CSPRNG en la clave 2 del head, nuevos en cada cápsula. Hacen de head_digest un compromiso que oculta. Quien ve head_digest sin el head, como un dispositivo de firma externo que recibe AUTHOR_MESSAGE en claro antes de la fecha, no puede confirmar una conjetura sobre nombres, tamaños o hashes previsibles. Los SHA-256 de cada fichero van sin sal: quien recibe el head los ve y puede compararlos con sha256sum.
  • round_time: el instante en que drand publica la ronda de la cápsula. Antes, nadie puede abrirla.
  • Release: la firma BLS de esa ronda, que abre la capa tlock.
  • Veredicto: el resultado de comprobar la firma o el sello. Es siempre un texto fijo y nunca impide abrir (apartado 4).
  • Lista blanca de R4: ZWNJ (U+200C), ZWJ (U+200D), VS15 (U+FE0E) y VS16 (U+FE0F), los únicos puntos Default_Ignorable_Code_Point que admite una ruta (apartado 3).

0.3 Decisiones del autor

Decisiones iniciales. 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 y firma con claves fijadas en el protocolo. Lleva un registro de solo añadir, anclado periódicamente en OpenTimestamps. Sellar es opcional y pide consentimiento. La decisión inicial lo ponía en el mismo origen que la página; la segunda decisión sobre la revisión lo cambia.

  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.

Decisiones sobre la revisión de Fable, del 30-09:

  1. Tres entregas, con el formato congelado desde la primera y security presente pero vacío:

    • la 1, el contenedor, varios ficheros y las rutas;
    • la 2, la firma y las claves de autor;
    • la 3, el sello, cuando exista el documento «Servicio de sellado DateKeys v1».

    Las entregas 2 y 3 no cambian el formato: un lector anterior muestra «no soportado» y abre igual.

  2. Origen del servicio (hallazgo 1). El autor no tenía preferencia y se tomó la recomendación: un subdominio aparte, seal.datekeys.com, que solo sirve la API y nunca HTML, con nosniff y CSP: sandbox de todos modos. La CSP de /create añade ese origen a connect-src, solo en esa página. Es de la entrega 3 y se puede revisar entonces.

  3. Vida del sello (hallazgo 3). La prueba va dentro de la cápsula. El token lleva la prueba de inclusión y el checkpoint firmado, y una raíz offline, fijada desde el primer perfil, certifica las claves anuales. Así el sello se verifica sin red para siempre, y la auditoría del anclaje queda como comprobación online opcional. Esto agranda el área (apartado 2).

  4. Abuso (hallazgo 4). Prueba de trabajo SHA-256 en cada petición, de un segundo en el navegador, sin guardar estado sobre quién pide.

0.4 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, autor o comentario, ni si hay firma o sello, salvo lo que delatan el tráfico y un área de 512 (fugas aceptadas)
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 Sus claves anuales 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

Fugas aceptadas:

  • P acota el número de ficheros y la longitud de los nombres y del comentario (apartado 6; objeción 20 del anexo B).
  • Un área de 512 bytes, la de los escritores anteriores a la entrega 3, no admite un sello. Cuando P es pequeña, delata esa área y, con ella, que la cápsula no lleva sello (apartado 2).
  • El registro del servicio publica la t de cada sello. Con poco tráfico, quien sepa cuándo se creó una cápsula puede sospechar que lleva sello.
  • Con sello, el tráfico hacia seal.datekeys.com delata que la cápsula lleva sello y, más o menos, cuándo se creó (hallazgo 8; apartado 20).
  • Tras la apertura, el sello vincula la cápsula con su petición al servicio (objeción 9).
  • La auditoría del anclaje acota la retrodatación a max_anchor_delay, 26 h, y no a minutos, hasta que haya testigos (objeción 2).

Parte 1. Entrega 1: contenedor, varios ficheros y rutas

La entrega 1 congela el formato. Define la trama, el contenedor security con sus huecos para la firma y el sello, el head, las rutas, los veredictos y su presentación, el lector y el escritor. Sus escritores dejan security vacío, y sus lectores muestran como no soportada cualquier firma o sello que encuentren.

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. Texto en claro de PAYLOAD_AGE = BODY || 0x00^(P − L), con BODY = AREA_LEN || SECURITY_LEN || HEAD_LEN || SECURITY_CBOR || ceros del área || HEAD_CBOR || CONTENT y 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: siempre presente, en un área cuyo tamaño declara la trama y no depende del contenido.
  5. SECURITY nunca decide la apertura (objetivo 6 de §4, recuperación independiente): solo da veredictos de texto fijo.
  6. Rutas estrictas, con tablas propias de Unicode 17.0.0 y las proyecciones best-fit de Windows.
  7. En el paso 17 prevalece cualquier fallo de age (ERR_INTEGRITY); si no lo hay, el orden del texto en claro. Código nuevo ERR_HEAD_INVALID.
  8. Entrega del contenido: un fichero, un ZIP propio o un árbol en un directorio nuevo. Un lector v0.9 rechaza el formato 3 en el paso 2.

Desacuerdos resueltos durante el diseño:

  • 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 inicial 4); HEAD con versión propia (decisión inicial 2), con el precedente del paso 14; área de tamaño fijo.
  • 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

BODY = AREA_LEN (uint32 BE) || SECURITY_LEN (uint32 BE) || HEAD_LEN (uint32 BE)
     || SECURITY_CBOR || 0x00^(AREA_LEN − SECURITY_LEN)
     || HEAD_CBOR || CONTENT

AREA_LEN = 512·k, con 1 ≤ k ≤ 128       1 ≤ SECURITY_LEN ≤ AREA_LEN
1 ≤ HEAD_LEN ≤ 2^24                     12 + AREA_LEN + HEAD_LEN ≤ L ≤ L_MAX
C = L − 12 − AREA_LEN − HEAD_LEN;       CONTENT = los ficheros concatenados en el orden del head

El área (hallazgo 5).

  • Su tamaño va en la trama y depende solo de la versión del spec con la que se escribe: lo usa todo escritor de esa versión, sepa sellar o no, y nunca depende de la invocación, de -seal ni de que se pase un sealer. Los escritores de las entregas 1 y 2 MUST escribir AREA_LEN = 512, y todos MUST escribir siempre SECURITY_CBOR, aunque vaya vacío. Antes, el área medía 512·⌈SECURITY_LEN/512⌉, y un sello de más de 512 bytes habría cambiado L y delatado su presencia a través de P.
  • El lector acepta cualquier AREA_LEN que cumpla la trama y no lo compara con la versión del spec. Así lee sin cambios el área mayor de un escritor posterior.
  • La entrega 3 fijará un área mayor para su versión del spec, porque el sello lleva su prueba dentro. Sus escritores la usarán siempre, sellen o no, así que P seguirá sin revelar si se eligió sellar. P sí puede revelar la versión del escritor, igual que VERSION revela el formato, y con un área de 512, que no hay sello (apartado 0.4).
  • El mapa security sigue en su versión 1 en las tres entregas. Un lector de la entrega 2 sigue comprobando la firma en una cápsula de la entrega 3.

Cada CBOR ocupa exactamente sus bytes (unmarshal actual). Los buffers solo crecen con bytes autenticados, nunca según L, P o los size (§57).

security = { 0: "datekeys-security", 1: 1,
             ? 2: bstr .size (1..65536),        ; author-signature, codificado aparte
             ? 3: bstr .size (1..65536) }       ; seal, codificado aparte
author-signature = { 0: alg (uint 1..2^32−1; 1 = Ed25519 estricto, entrega 2),
                     1: bstr,                   ; clave pública
                     2: bstr }                  ; firma
seal = { 0: seal_type (uint 1..2^32−1; 1 = DateKeys, entrega 3;
                       2 = RFC 3161 y 3 = OpenTimestamps, reservados),
         1: bstr }                              ; token
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

Las claves 2 y 3 de security son cadenas de bytes con su propio CBOR dentro: el mapa author-signature y el mapa seal. La capa 3 de security solo comprueba el mapa exterior. El lector decodifica cada uno aparte en el paso 17.6, con el perfil de §58 y su CDDL, y un fallo solo cambia el veredicto de ese elemento (apartado 4). Así, los lectores de las tres entregas validan igual el mapa exterior, una firma o un sello mal formados nunca arrastran al otro, y un alg o un seal_type futuros pueden tener el tamaño que quepa en el área. Los tamaños propios de cada alg, 32 y 64 bytes en Ed25519, también se comprueban al evaluar el veredicto.

Tamaños.

  • SECURITY mide 22 bytes vacía y 130 con una firma Ed25519. El sello de la entrega 3 no cabe en 512 bytes.
  • HEAD mínimo: 53 bytes; cada entrada, al menos 45.
  • «nota.txt» de 1000 bytes, con mtime: HEAD 117, L = 1641, P = 1792.
  • Cápsula mínima que acepta el lector, sin ficheros ni comentario: L = 577, P = 768.

Maquetación: start₀ = 0, startᵢ = endᵢ₋₁ y endᵢ − startᵢ = sizeᵢ, comprobado con restas: en JavaScript 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 los 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.

mtime (hallazgo 9). El escritor la toma al cargar cada fichero: ⌊lastModified/1000⌋ en la página y ModTime().Unix() en la CLI. La omite si no la conoce o si cae fuera de 0..253402300799, por ejemplo una fecha anterior a 1970, y nunca la recorta. Una mtime futura se guarda tal cual: es informativa.

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 y 4 → ERR_HEAD_INVALID: las reglas de texto del comentario y del autor;
    • clave 5 → ERR_HEAD_INVALID: cada entrada con R2 a R6c, R10 y su maquetación, y tras el bucle, R7 y R9;
    • clave 6 → ERR_EXTENSION_CRITICAL_UNKNOWN, luego ERR_EXTENSION_DATA_INVALID.

En el formato 3 el head va siempre en su versión 1. Una versión nueva del head exige un formato nuevo (§22), que un lector v0.10 rechaza en el paso 2, sin red; la capa 2 queda como defensa, igual que la versión de CONTROL_CBOR en el paso 14.

Capas de SECURITY: las mismas capas 2 y 3 sobre el mapa exterior, sin código. Un fallo da el veredicto X (apartado 4).

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. De 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 «..». Tampoco lo es, ni queda vacío, al quitarle los puntos de la lista blanca de R4: HFS+ ignora ZWNJ y ZWJ al comparar nombres, así que allí un «.» o un «..» seguido de ZWJ se vería y se compararía como ellos; VS15 y VS16 se quitan también, por prudencia. Además, el NFD del segmento, con las tablas de R7, mide como mucho 255 unidades UTF-16, el límite de HFS+: 127 veces «ΐ» (U+0390) son 254 bytes, pero 381 unidades tras NFD.

  • R4. Puntos de código prohibidos (hallazgo 2):

    • C0 y U+007F–009F;
    • " * : < > ? \ |;
    • U+2028 y U+2029;
    • todo punto con la propiedad Default_Ignorable_Code_Point de Unicode 17.0.0, salvo la lista blanca: ZWNJ, ZWJ, VS15 y VS16, que hacen falta en emoji y en varias escrituras. La propiedad cubre los controles bidi (U+061C, U+200E, U+200F, U+202A–202E, U+2066–2069), U+00AD, U+034F, U+200B, U+2060–206F, U+3164, U+FEFF, los demás selectores de variante y las etiquetas U+E0000–E007F, que permiten nombres visualmente idénticos;
    • los no-caracteres y todo Cn (sin asignar) de Unicode 17.0.0, que una versión futura podría plegar o volver ignorable;
    • U+F000–U+F0FF, del área de uso privado: Cygwin, WSL y el SMB de macOS representan con ellos los caracteres prohibidos en Windows, así que «informe», U+F03A y «anexo» se verían allí como «informe:anexo».
  • 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 (hallazgo 9). Se rechaza un segmento que, contando puntos de código, cumple las tres condiciones:

    • tiene como mucho un '.';
    • la parte anterior al '.', o todo el segmento si no lo tiene, mide de 1 a 8 y cumple ^[^.]*~[0-9]{1,6}$, es decir, acaba en '~' y de 1 a 6 dígitos ASCII;
    • la parte posterior mide de 0 a 3.

    La expresión regular no usa búsquedas anticipadas, que el paquete regexp de Go no admite; en JavaScript va con la bandera u. «~1» a secas también se rechaza. 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 s′ el segmento sin los puntos de la lista blanca de R4 y el pliegue C+F de CaseFolding 17.0.0 más ı → i (NTFS). Se quitan antes de normalizar porque los cuatro son starters (ccc 0): entre dos marcas combinantes, cambiarían su orden canónico. Los directorios con casefold de ext4 y f2fs también ignoraban los puntos ignorables al comparar, hasta 2025; no está comprobado aquí.
    • Se rechazan dos hermanos con la misma clave (A.txt/a.txt, NFC/NFD, «Fotos/a» y «fotos/b», Straße/STRASSE, Kelvin/k, «ab» con y sin ZWNJ) y una ruta que es a la vez fichero y carpeta («A» y «a/b»).
  • R8. Orden estrictamente ascendente de los bytes UTF-8 (§54). TypeScript MUST comparar los bytes, no los strings: < ordena por unidades UTF-16, y U+FF5E y U+1F600 salen al revés que en Go (hallazgo 6); localeCompare ordena por colación. Ninguno de los dos sirve.

  • R9. Como mucho 65535 carpetas implícitas.

  • R10. Ningún primer segmento cuya clave de R7 empiece por «.datekeys-». La CLI escribe el árbol en DIR/.datekeys-* antes de moverlo, y una entrada con ese nombre chocaría con él (hallazgo 10).

Tablas. Un generador en datekeys-go las produce desde UCD 17.0.0 (UnicodeData, DerivedCoreProperties, CaseFolding y las descomposiciones) y desde WindowsBestFit, y emite código para los dos repos; sus suites comprueban el digest. MUST NOT usarse normalize, toLowerCase, las clases \p{…} de las expresiones regulares, el paquete unicode ni x/text: dependen de la versión de Unicode de cada motor.

Escritor.

  • Guarda los nombres tal como llegan.
  • Rechaza un UTF-16 mal formado y, con un mensaje que nombra el carácter, los puntos de R4 y los 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. Los avisos comparan la clave de R7 del segmento y de su extensión, como R10, así que «.GIT», «Informe.LNK» o «.git» con un ZWJ en medio también avisan.

4. Veredictos y presentación

Evaluación (hallazgo 9). El lector evalúa security en el paso 17.6, y el resultado solo se muestra tras el paso 18. La firma y el sello se evalúan por separado:

Situación Firma Sello
El mapa exterior de security falla la capa 2 o la 3, o su versión no es 1 X X
Sin clave 2 F0 no cambia
La clave 2 no decodifica o incumple el CDDL de author-signature F1 no cambia
alg que el lector no implementa F1 no cambia
alg conocido, con una clave o una firma de otra longitud F1 no cambia
Firma que no verifica F2 no cambia
Firma válida F3 o F4 no cambia
Sin clave 3 no cambia S0
El sello no se puede evaluar, según el orden de abajo no cambia S1 o S2
Sello que no verifica no cambia S3
Sello válido no cambia S4 o S5; S6 en su lugar si la clave anual está comprometida

Orden del sello: decide la primera condición que se cumple (anexo C, hallazgo 8).

  1. La clave 3 no decodifica o incumple el CDDL de seal → S2.
  2. seal_type desconocido → S1.
  3. El token tiene otro type tag o no llega a su versión → S2.
  4. Versión del token desconocida → S1.
  5. El token incumple su CDDL, que trata el certificado como una cadena de bytes → S2.
  6. El certificado tiene otro type tag o no llega a su versión → S2; su versión es desconocida → S1; incumple su CDDL → S2.
  7. Raíz desconocida → S1.
  8. Falla cualquier comprobación de firma, de rango o de inclusión → S3.
  • X es una sola línea, en lugar de la de la firma y la del sello.
  • SIG_PART (apartado 17) se calcula sobre el contenido exacto de la cadena de bytes de la clave 2, sin su cabecera CBOR, sea cual sea su veredicto. Una firma ilegible, no soportada o inválida no cambia el veredicto del sello, y un sello válido prueba que esos bytes existían en t.
  • Solo «guardar esta clave» combina los dos veredictos (apartado 14).
  • En la entrega 1 el lector no implementa ningún alg ni ningún seal_type, así que solo llega a X, F0, F1, S0, S1 y S2.

Textos (normativos):

Veredicto Texto Desde
X «No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada.» 1
F0, firma ausente «Sin firma de autor.» 1
F1, firma ilegible o no soportada «No se ha comprobado ninguna firma: trátala como no firmada.» 1
F2, firma inválida «La firma no corresponde a este contenido.» 2
F3, válida con una clave guardada «Firmado con la clave que guardaste como ‹etiqueta›.» 2
F4, válida con otra clave «Firmado con la clave dkauthor1… (completa). No prueba quién la tiene.» 2
S0, sello ausente nada sobre la fecha 1
S1, sello no soportado «Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada.» 1
S2, sello ilegible «El sello de tiempo es ilegible: no prueba nada.» 1
S3, sello inválido «El sello no corresponde a este contenido.» 3
S4, 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.» 3
S5, válido, t ≥ round_time «Sellado después de la fecha de apertura: no prueba nada anterior.» 3
S6, clave del servicio comprometida «Firma del servicio válida, pero su clave está comprometida: requiere auditoría.» 3

Con un sello válido, una mtime posterior a t se muestra como incoherencia.

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 (hallazgo 7):
    • expande cada TAB del comentario a espacios, hasta la siguiente columna múltiplo de 8;
    • parte cada línea del autor y del comentario en trozos de ancho visual como mucho W − 3, con W el ancho del terminal, u 80 si la salida no es un terminal, y nunca menos de 20; cada trozo lleva al menos un punto de código;
    • cuenta el ancho por lo alto: 1 por carácter ASCII imprimible y 2 por cualquier otro punto de código. El prefijo │ cuenta 3, porque U+2502 tiene anchura ambigua y ocupa 2 columnas en un terminal CJK;
    • pone │ delante de cada trozo. Así el terminal nunca parte una línea por su cuenta, y ninguna queda sin prefijo;
    • después del comentario repite los veredictos.

5. Escritor y lector

Escritor, en dos fases (prepareFiles y write):

  • Fase A, sin red ni secretos:
    • Rutas y mtime, con la casilla marcada por defecto y la regla del apartado 2.
    • 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 de schema 3.
    4. Desde la entrega 2, la firma (apartado 10).
    5. Desde la entrega 3, el sello (apartado 19).
    6. SECURITY_CBOR, vacío en la entrega 1, en el área de su versión del spec (512 en las entregas 1 y 2), y la autodecodificación de CONTROL_CBOR, HEAD_CBOR, SECURITY_CBOR y los mapas de sus claves 2 y 3 (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: los rangos de AREA_LEN, SECURITY_LEN y HEAD_LEN, que quepan en L y que el área termine en ceros → 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 (apartado 4).
    • 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 texto en claro 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. R10 impide que una entrada tenga ese nombre.
    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, con la presentación del apartado 4.

6. 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; del sello, solo lo que delatan el tráfico y un área de 512 (apartado 0.4);
    • P acota el número de ficheros y la longitud de los nombres y del comentario. Con un área de 512 y pocos ficheros, n ≤ (P − 579)/45, es decir, 4 con P = 768.
  • Quien abre: las rutas, los tamaños, los SHA-256 y las mtimes, el comentario y el autor declarado. Con firma y sello, además, lo de los apartados 12 y 20.

7. 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.
  • Las entregas 2 y 3 no cambian la trama, el head ni el CDDL de security: añaden algoritmos, tipos de sello y veredictos, y la 3, un área mayor que todo lector v0.10 ya acepta.
  • 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.

8. Cambios en el spec, el código y las pruebas

Spec v0.10:

  • §4, objetivo 8, y §55.2: la privacidad de metadatos cubre el formato 3.
  • §6: los nombres, los tamaños y las fechas de los ficheros entran en el protocolo, cifrados.
  • §22, §23, §27 y §31: formato 3 y CONTROL_CBOR de schema 3.
  • §29.2 a §29.6 y §29.10: la trama y el área, el contenedor security, el head, las rutas, el texto, y los veredictos y su presentación.
  • §54 y §72: el head es un objeto nuevo con extensiones, y el registro lo recoge.
  • §55.1: el head y security en la tabla de confianza por sección.
  • §56 y §57: entrega atómica y límites.
  • §62.1: reglas nuevas del escritor (formato 3, área y security siempre, rutas, texto, mtime y autodecodificación de CONTROL_CBOR, HEAD_CBOR, SECURITY_CBOR y los mapas de sus claves 2 y 3).
  • §63, pasos 2, 12 a 14 y 16 a 18.
  • §69 y §69.1: ERR_HEAD_INVALID, la precedencia y el Alcance.
  • §70: VERSION de 1 a 3, y los escritores escriben el formato 3.
  • datekeys.cddl.

Código:

  • Go: capsule (Format3, head, security, pathkey, el sumidero) y la CLI (-in con directorios, -comment, -author, -no-mtime y decrypt -out DIR).
  • TypeScript: los mismos módulos, más zip.ts y crc32.ts.
  • El generador de tablas, en datekeys-go, que emite código para los dos repos.

Pruebas:

  • Fixtures: format3_single, _tree, _comment_only, _bloque256, _time_and_key_portable, _area_1024 (el lector acepta un área mayor), _security_v2 (veredicto X), _signature_unsupported, con alg 1, una clave de 32 bytes y una firma de 64, aleatorias (F1 en la entrega 1 y F2 desde la 2), y _seal_unsupported, con esa firma y un sello de seal_type 1 con un token aleatorio (F1 y S1 en la entrega 1; S2 desde la 3).
  • Vectores:
    • paths.json y head_schema.json: U+00A0 y U+3000 en los extremos, que se aceptan; best-fit; 8.3 con la expresión regular, «~1» incluido; Cn; [«b/..», «a»]; U+206A–206F, las etiquetas y otros ignorables fuera de la lista blanca; «.» seguido de ZWJ y un segmento hecho solo de ZWJ; 127 veces «ΐ»; U+F03A; «.datekeys-x» en el primer nivel y en otro; el orden de U+FF5E y U+1F600;
    • path_fold.json, con «ab» con y sin ZWNJ;
    • security.json: el mapa exterior con la clave 2 que no es una cadena de bytes, con una clave 4 o con un byte de más dentro de SECURITY_LEN (X); alg 0 o una clave vacía dentro de la clave 2 (F1, con el sello intacto);
    • 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: AREA_LEN 0, 511, 513 y 66048; SECURITY_LEN 0 y mayor que AREA_LEN; HEAD_LEN 0 y 2²⁴ + 1; 12 + AREA_LEN + HEAD_LEN = L + 1; L < 12; un byte ≠ 0 en el área;
    • cada regla de rutas y de texto;
    • head v2 y end_último ≠ C;
    • la precedencia, con el corte exacto.
  • 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;
    • el escritor omite una mtime negativa.

9. Preguntas abiertas de la entrega 1

Hay que cerrarlas antes de redactar el texto del spec. Cada una lleva su recomendación.

  1. ¿Código nuevo ERR_HEAD_INVALID? Sí: separa un escritor defectuoso de un CBOR roto.
  2. ¿AREA_LEN = 512 para los escritores de las entregas 1 y 2? Sí: cuesta 490 bytes por cápsula y oculta la firma. La alternativa es fijar ya el área final, por ejemplo 4096, para todo escritor: cuesta unos 3,5 KB por cápsula y quita la fuga del área de 512 (apartado 0.4), pero el tamaño del token aún no es definitivo.
  3. ¿El área la fija la versión del spec, y el mapa security sigue en su versión 1 cuando llegue el sello? Sí: así un lector de la entrega 2 sigue comprobando la firma en una cápsula de la entrega 3. La alternativa, una versión 2 de security con su propia área, haría que ese lector lo mostrara todo como no soportado.
  4. ¿Tablas propias, generadas de UCD 17.0.0? Sí: la regla ligera deja pasar colisiones NFC/NFD, y cada motor trae su versión de Unicode.
  5. ¿Aceptar que un head de versión nueva falle tras la fecha? No: §22 exige un formato nuevo para todo cambio que un lector anterior solo detectaría después de pedir el release. En el formato 3 el head va siempre en su versión 1 (apartado 2).
  6. ¿Escribir solo el formato 3? Sí.
  7. Límites: comentario 16 KiB, autor 256 B, ruta 1024 B, segmento 255 B, profundidad 32, 65 535 ficheros, head 16 MiB y área 64 KiB. Falta confirmarlos.
  8. ¿Un único fichero dentro de una carpeta? En ZIP.
  9. ¿Cápsulas solo con comentario? El lector acepta C = 0; el escritor exige un fichero o un comentario.
  10. ¿Rechazar un '-' inicial? No, solo avisar.

Parte 2. Entrega 2: firma y claves de autor

La entrega 2 define alg 1, Ed25519 estricto, y las claves de autor. No cambia ningún byte del formato: una cápsula firmada se abre en un lector de la entrega 1, que muestra F1.

10. Qué se firma

  • 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("datekeys:dkc3:control:v1" || 0x00 || 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("datekeys:dkc3:head:v1" || 0x00 || HEAD_CBOR). Fija CONTENT byte a byte, porque el head lleva el tamaño y el SHA-256 de cada fichero.
  • Los compromisos que se firman o se sellan, payload_commit, control_commit, head_digest y SEAL_SUBJECT, llevan cada uno su prefijo de dominio (hallazgo 10), así que ninguno sirve en otro contexto. Los hashes que van dentro de ellos, como el de cada fichero, no lo necesitan.
AUTHOR_MESSAGE = "datekeys:dkc3:author-signature:v1" || 0x00 || control_commit || head_digest   ; 98 B

La firma con alg 1 es la Ed25519 de AUTHOR_MESSAGE, y el mapa author-signature, codificado aparte en la clave 2 de security, lleva la clave pública de 32 bytes y la firma de 64. El escritor MUST verificarla, con el perfil estricto, antes de escribir la cápsula. No se cubren los recipients (§55.2), los ciphertexts ni la .dkk.

11. Ed25519 estricto

Vale para la firma de autor, y en la entrega 3, para el token, el checkpoint y los certificados del sello. 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.

12. Qué prueba una firma

  • 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).
  • Ataques: tras la fecha, quien vuelve a sellar el control (§36.1) puede quitar la firma o firmar con K′. Sin sello, una firma no prueba que existiera antes de la fecha. Un trasplante a otro control invalida la firma. Contra la equivocación, se publica capsule_digest antes de la fecha.
  • Quien abre ve la clave pública, que vincula entre sí las cápsulas firmadas con ella.
  • Los veredictos F2 a F4 están en el apartado 4.

13. Verificación por un tercero

Un tercero recibe un paquete (datekeys statement export|verify) con PRELUDE, PUBLIC_HEADER, CONTROL_PUB, HEAD, SECURITY y los ficheros elegidos, nunca I_PAYLOAD.

  • Recalcula header_binding y lo compara con el que va en CONTROL_PUB; después, control_commit y head_digest.
  • Comprueba la firma y, desde la entrega 3, el sello, con los veredictos del apartado 4.
  • Comprueba el SHA-256 de cada fichero del paquete contra el head.
  • Revelar HEAD muestra los nombres, los tamaños, las mtimes y los SHA-256 de todos los ficheros, también los que no van en el paquete.

14. 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. La sal del head evita que ese dispositivo aprenda algo del contenido (apartado 0.2).

Cómo se conoce la clave.

  • Por un canal externo: la persona pega la clave dkauthor1… completa que le dio el autor, o la CLI la recibe con -expect-author.

  • La página ofrece además «guardar esta clave como…» desde una cápsula 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.

    Como exige un sello, esta vía solo existe desde la entrega 3.

  • Ninguna decisión se basa en una huella truncada.

15. Cambios, pruebas y preguntas de la entrega 2

Spec:

  • §4, objetivo 9: autenticidad opcional.
  • §5 y un §7.9 nuevo: la equivocación y la clave de autor robada.
  • §29.7 y §29.8: los enunciados de la firma y Ed25519 estricto.
  • §62.1: la firma se verifica antes de escribir (MUST).
  • §27, §36.1, §55.1, §63 paso 15, §72, §73 y §74: la firma de autor aporta lo que antes solo aportaba una «extensión de firma».

Código: ed25519strict, authorkey, statement y la CLI (-sign, -expect-author, author keygen|public y statement export|verify); en TypeScript, además, la guarda contra ed25519.verify.

Pruebas:

  • Fixture: format3_signed.
  • Vectores: statements.json, con los prefijos de dominio y alg 1 con claves de 33 bytes y firmas de 63, que dan F1; ed25519_strict.json, con los casos de «Taming the many EdDSAs».
  • Mutaciones: la firma alterada, quitada o rehecha, y el trasplante a otro control.

Preguntas abiertas:

  1. ¿Fichero de clave cifrado por defecto? Sí, con logN 16.
  2. ¿Sin sello, la clave solo se guarda pegándola desde otro canal? Sí: una firma sin sello puede haberse rehecho tras la fecha.

Parte 3. Entrega 3: sello de tiempo

La entrega 3 depende del documento «Servicio de sellado DateKeys v1», que definirá la custodia de las claves, los monitores, el hosting y los detalles de esta parte. Aquí queda lo que ya está decidido y lo que ese documento tiene que cerrar. No cambia la trama ni el head: define seal_type 1 y un área mayor.

16. Decisiones que ya fijan el diseño

  1. Origen (hallazgo 1). El servicio vive en seal.datekeys.com, un origen distinto del de la página, que solo sirve la API y nunca HTML. Responde de todos modos con X-Content-Type-Options: nosniff y Content-Security-Policy: sandbox. La CSP de /create añade https://seal.datekeys.com a connect-src, solo en esa página; /inspect conserva la suya (apartado 19).
  2. La prueba va dentro de la cápsula (hallazgo 3). El token lleva la prueba de inclusión de su hoja y el checkpoint firmado que la contiene. Una raíz offline, fijada en el software desde el primer perfil, certifica cada clave anual. El sello se verifica sin red para siempre, aunque DateKeys desaparezca; la auditoría del anclaje queda como comprobación online opcional.
  3. Área mayor. El token con su prueba no cabe en 512 bytes. La entrega 3 fija un área mayor para su versión del spec, que todos sus escritores usan siempre, sellen o no, sin que dependa de -seal ni de que haya un sealer (apartado 2).
  4. Abuso (hallazgo 4). Cada petición lleva una prueba de trabajo SHA-256 de un segundo en el navegador. El servicio no guarda estado sobre quién pide.

17. Qué se sella

SIG_PART     = 0x00 sin firma | 0x01 || SHA-256(contenido de la cadena de bytes de la clave 2 de security, sin su cabecera CBOR)
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.
  • seal_profile_hash es el SHA-256 del contenido de la clave 2 del token, el certificado exacto de la clave anual (apartado 18).
  • Qué prueba un 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 la firma y el sello.

18. El token y su verificación sin red

La forma es orientativa; la exacta es del documento del servicio. El certificado va codificado aparte, en una cadena de bytes: así el token se valida sin él, y la raíz firma sus bytes exactos, con un prefijo de dominio, sin reserializar nada (§72).

seal-token = { 0: "datekeys-seal-token", 1: 1,
               2: bstr,                        ; seal-cert, codificado aparte
               3: bstr .size 64,               ; firma de la raíz sobre el contenido de la clave 2
               4: bstr .size 8 (t), 5: bstr .size 8 (índice),
               6: bstr .size 64,               ; firma de SEAL_MESSAGE con la clave anual
               7: [* bstr .size 32],           ; prueba de inclusión RFC 9162
               8: bstr }                       ; checkpoint C2SP firmado que incluye la hoja
seal-cert  = { 0: "datekeys-seal-cert", 1: 1, 2: "datekeys:seal:v1",
               3: tstr .size (1..256),         ; log_origin
               4: bstr .size 32,               ; clave de tokens
               5: bstr .size 32,               ; clave de checkpoints
               6: bstr .size 8, 7: bstr .size 8,   ; not_before, not_after
               8: bstr .size 8,                ; max_anchor_delay
               9: bstr .size 32 }              ; SHA-256 de la clave pública de la raíz
; u64 big-endian en segundos

Verificación en el paso 17.6, sin red, después del orden del apartado 4, que da S1 o S2 antes de comprobar nada:

  • Se comprueban la firma de la raíz sobre el contenido de la clave 2; que t y el índice no pasan de 2⁵³ − 1 y que t cae en [not_before, not_after]; la firma estricta de SEAL_MESSAGE; que el origen del checkpoint es log_origin; la prueba de inclusión de la hoja en el índice dado hasta la raíz del checkpoint, con 53 hashes como mucho; y la firma del checkpoint con la clave de checkpoints. Cualquier fallo → S3.
  • Si todo cuadra, el sello es válido: S4 si t < round_time y S5 si no. Es S6 si el registro de §71 que trae el lector marca como comprometida esa clave anual.

Claves.

  • La raíz es offline y se fija en el software desde el primer perfil. Solo firma certificados anuales.
  • Cada clave anual tiene una ventana de un año y se destruye de forma documentada en not_after.
  • Un compromiso marca todos los tokens de esa clave, sea cual sea su t, porque el ladrón elige t; la fecha del compromiso solo sirve a la auditoría. Un lector solo conoce los compromisos publicados antes de su versión.
  • Quien robara la raíz podría certificar claves falsas y retrodatar a voluntad. Su custodia es del documento del servicio.

19. El servicio

API.

  • POST https://seal.datekeys.com/v1/seal con application/octet-stream: los 32 bytes de SEAL_SUBJECT y la prueba de trabajo.
  • Es una petición entre orígenes, con preflight CORS; el servicio solo admite el origen de la página.
  • 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; 400, 429 o 503.
  • La librería no usa la red: recibe un sealer(subject).

Página. Hoy la CSP es global (kit.csp en svelte.config.js, emitida como <meta>), con connect-src 'self' y worker-src 'none', y scripts/check-build.mjs exige esos valores en todas las páginas. La entrega 3 da a /create, y solo a ella, connect-src 'self' https://seal.datekeys.com y worker-src 'self', reescribiendo su <meta> al prerenderizar o con cabeceras por ruta; check-build.mjs pasa a comprobar la política exacta de cada página. El worker de la prueba de trabajo no hereda el <meta> (CSP3): su política sería la de su propia respuesta, y el hosting estático no manda cabeceras. Por eso no importa el módulo de red, y una prueba de guarda lo comprueba.

Escritor. Con consentimiento, pide el sello después de firmar. MUST verificar el token y MUST cumplirse t < round_time. Si no, descarta el sello, y la persona elige seguir sin él o cancelar, igual que si el servicio no responde; plan.size no cambia, porque el área es fija. Avisa antes de pedirlo si faltan menos de 2 min para la fecha de apertura, porque t se redondea al minuto siguiente, y después si |t − reloj| > 10 min.

Prueba de trabajo.

  • El cliente busca un nonce tal que el SHA-256 de un prefijo de dominio, el subject, la ventana de t y el nonce tenga D bits a cero. D se ajusta para que tarde un segundo en el navegador, en un worker.
  • El servicio la comprueba con un solo hash y descarta en memoria las repeticiones de la ventana en curso. No guarda IPs ni nada sobre quién pide.
  • Encarece cada petición por igual, pero no frena a quien dedica varios núcleos o una GPU. El registro tiene que aguantar un crecimiento abusivo, con un límite global que no dependa de quién pide.

Servicio. Calcula t = 60·⌈ahora/60⌉ con un reloj disciplinado y añade la hoja SEAL_MESSAGE de forma duradera y replicada. Después espera al checkpoint que la incluye, firma y responde.

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 https://seal.datekeys.com/v1/anchor/<tamaño>.ots.
  • Invariantes que vigilan los monitores: hay un checkpoint al menos cada 60 s, 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). Parte del checkpoint que va en el token, y necesita el .ots de un checkpoint anclado posterior y la prueba de consistencia entre los dos. La página no puede verificar Bitcoin bajo su CSP, y lo dice.

20. Privacidad

  • Antes de la apertura, el servicio ve la IP, el User-Agent, la hora y 32 bytes que no puede vincular.
  • El tráfico hacia seal.datekeys.com delata a un observador de la red que la cápsula lleva sello y, más o menos, cuándo se creó. Es una fuga aceptada (hallazgo 8): sin sello, la página no hace ninguna petición.
  • Después, quien abre recalcula el subject y encuentra la hoja: el sello vincula la cápsula con ese POST. Por eso el servicio 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.
  • Quien abre ve además t al minuto, que aproxima la fecha de creación, como dice el texto de consentimiento, y el índice y el certificado, que vinculan la cápsula con el POST.

21. 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.

22. Cambios y preguntas de la entrega 3

Spec: §4, objetivo 9 (fecha opcional); un §7.10 nuevo (servicio de sellado comprometido, clave anual o raíz robadas); §29.9 (el sello); §62.1 (el sello se verifica antes de escribir, MUST); §71 (la raíz y el registro de claves comprometidas); y el documento «Servicio de sellado DateKeys v1».

Código: seal y seal-verify, la guarda contra fetch y la CLI (-seal y seal audit).

Pruebas: format3_signed_sealed y format3_sealed_after; seal_token.json, con un token retrodatado bajo una clave anual comprometida y un certificado con otra raíz; mutaciones de t y del trasplante.

Preguntas abiertas, para el documento del servicio:

  1. ¿Clave con ventana? Decidido: sí, de un año, certificada por una raíz offline desde el primer perfil (tercera decisión sobre la revisión).
  2. ¿Hora del sello al minuto? Sí, pero se revisa con la latencia: el token espera al checkpoint que incluye su hoja.
  3. ¿Checkpoints cada pocos segundos, o respuesta al cierre de cada minuto? Recomendación: cada pocos segundos, para que la página no espere un minuto.
  4. ¿Tamaño del área mayor? Probablemente 4096 bytes. El token, con su certificado, una prueba de inclusión de hasta 40 niveles y el checkpoint, ronda los 2 KB, y la firma ocupa 108 bytes.
  5. ¿Cómo se limita el crecimiento del registro sin saber quién pide?
  6. ¿Custodia de la raíz, y qué hacer si se compromete?
  7. ¿Dónde se conservan a largo plazo los .ots y las pruebas de consistencia, para que la auditoría online siga siendo posible sin DateKeys?
  8. ¿Cómo se cambia de clave anual? Con t redondeada hacia arriba, una petición cerca de not_after puede recibir una t posterior, y el checkpoint que incluye una hoja de los últimos segundos puede firmarse ya con la clave siguiente. Recomendación: elegir el certificado por t, solapar las ventanas y firmar cada checkpoint del solape con las dos claves, que un signed note admite.
  9. ¿Cómo se define la prueba de trabajo? Falta fijar D y de dónde lo obtiene el cliente; la tolerancia de ventana, porque una prueba que empieza en el segundo 59 termina en la ventana siguiente y los relojes se desvían; y un registro de repeticiones que cubra todas las ventanas aceptadas y todas las réplicas.
  10. ¿Sello RFC 3161 cualificado? En una segunda fase.
  11. ¿Testigos de checkpoints? En la fase 2 del servicio.

Anexo A. Revisión de Fable (30-09)

El texto completo está en revision_fable.md. Veredicto: la parte criptográfica aguanta, y los problemas están en los bordes. Fable comprobó la aritmética de tamaños de entonces, la necesidad del verificador Ed25519 propio y las versiones de Unicode de Go y de Node. No revisó el ZIP byte a byte ni probó HFS+ en una máquina: lo de HFS+ se apoya en la lista de core.protectHFS de git.

# Gravedad Hallazgo Resolución Dónde
1 Mayor Mismo origen para la página y el servicio seal.datekeys.com, solo API, con nosniff y CSP: sandbox (segunda decisión sobre la revisión) Apartado 16
2 Mayor Caracteres invisibles en las rutas R4 por Default_Ignorable_Code_Point con lista blanca; R3 y R7 sobre la forma sin la lista blanca; etiquetas prohibidas Apartado 3
3 Mayor El sello no sobrevive a DateKeys La prueba dentro de la cápsula y una raíz offline desde el primer perfil (tercera decisión) Apartados 16 y 18
4 Mayor Abuso frente a «sin registros» Prueba de trabajo, con estado solo en memoria (cuarta decisión) Apartado 19
5 Menor El área dependía de SECURITY_LEN AREA_LEN en la trama, fijo por versión del spec y siempre reservado Apartado 2
6 Menor Orden de rutas en TypeScript R8 compara bytes UTF-8; vector en paths.json Apartados 3 y 8
7 Menor Suplantación en la CLI Prefijo en cada línea visual, ancho contado por lo alto y veredictos repetidos al final Apartado 4
8 Menor El POST delata el sello Fuga aceptada y declarada Apartados 0.4 y 20
9 Menor Ambigüedades Tabla de verdad de firma y sello; expresión regular de R6b; mtime omitida fuera de rango Apartados 4, 3 y 2
10 Menor Sin prefijo de dominio; .datekeys-* Prefijos en control_commit y head_digest; R10 Apartados 10 y 3

Sobre el documento:

  • 28 objeciones que eran 24: el anexo B las funde y marca cada una como corregida o aceptada.
  • Faltaban definiciones: el apartado 0.2 explica las capas, L_MAX y la sal, y los apartados de este documento se distinguen de los § del spec.
  • Tres ficheros con el mismo texto: este documento es ahora la única fuente.
  • Un sello que el lector no sabe verificar no mostraba nada: ahora da S1.

Anexo B. Revisión adversarial interna: 24 objeciones

Todas eran reales. Cuatro de las 28 originales repetían otras (la 12 a la 3, la 16 a la 4, la 18 a la 7 y la 24 a la 2) y se han fundido. Tres se aceptan en lugar de corregirse: la 2, la 9 y la 20. Entre corchetes, el número original.

  1. [1] control_digest revelaba I_PAYLOAD (mayor). Corregida: se firma control_commit, con payload_commit, y hay un paquete de verificación (apartados 10 y 13). 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. [2 y 24] El anclaje deja retrodatar hasta 26 h (mayor). Aceptada y acotada: el anclaje «acota», el veredicto auditado exige t_bloque + 2 h < round_time y los testigos llegan en la fase 2. La tolerancia inferior del ancla es de 2 h.
  3. [3 y 12] «Comprometido desde T» (mayor). Corregida: un compromiso marca todos los tokens de la clave, y la clave anual se destruye en not_after. Vale también sin red, con un vector.
  4. [4 y 16] Lista best-fit incompleta (mayor). Corregida con R6c, generada. Solo se proyecta lo que la tabla lleva a ASCII, para no confundir con sintaxis el '?' por defecto.
  5. [5] TOFU. Corregida (apartado 14).
  6. [6] Un comentario que imita veredictos. Corregida: presentación normativa y texto nuevo para la firma no soportada. El hallazgo 7 de Fable la completa en la CLI.
  7. [7 y 18] «Espacio» sin definir. Corregida: es U+0020.
  8. [8] Alias 8.3. Corregida con R6b, comprobada en esta máquina y precisada con una expresión regular. Git solo protege .git; aquí se generaliza.
  9. [9] Vinculación tras la apertura. Aceptada y documentada (apartado 20): el servicio no guarda registros de acceso.
  10. [10] Canal de difusión. Corregida: se publica el hash del subject y fetch queda aislado.
  11. [11] MAY de fallo rápido (mayor). Corregida. Contradecía el Alcance de §69.1 y el final del paso 17.
  12. [13] Tiempos del ZIP (mayor). Corregida byte a byte.
  13. [14] Open sin sumidero. Corregida: error del llamador, como ya hacen DecodeControl con el formato 3 (framing_test.go:243) y open (open.ts:139-142).
  14. [15] Precedencia de R1 y R8. Corregida.
  15. [17] Tamaños con alg 1. Corregida: dan «ilegible». Desde la revisión de Fable se comprueban al evaluar la firma, y el mapa de la firma va codificado aparte en la clave 2 de security, así que el CDDL exterior es el mismo en las tres entregas y el sello no depende de la firma.
  16. [19] HEAD de más de 16 MiB. Corregida en la fase A.
  17. [20] Carpetas y ZIP64. Corregida.
  18. [21] Nombre del fichero en OPFS. Corregida; comprobado en isStale (tempfile.ts:198).
  19. [22] Salida de la CLI. Corregida: Mkdir, Root.Rename (existe en Go 1.26), stdout, cápsulas sin ficheros y -in.
  20. [23] Número de ficheros visible. Aceptada: P da una cota (apartado 6).
  21. [25] Puntos de código sin asignar. Corregida: Cn entra en R4, que desde la revisión de Fable prohíbe también los ignorables.
  22. [26] scrypt y passphrase. Corregida: logN 16; x/sys ya es indirecta.
  23. [27] Pruebas del escritor de formato 2. Corregida: opción solo para pruebas (encrypt.test.ts:116).
  24. [28] CDDL del perfil de sello. Corregida entonces. La tercera decisión sobre la revisión lo sustituye por un certificado de la raíz (apartado 18).

Anexo C. Revisión adversarial de esta versión (30-09)

Un revisor comprobó que los arreglos de Fable y las decisiones del autor están aplicados, buscó lo que se perdió respecto a la versión anterior y buscó problemas nuevos. No encontró nada bloqueante ni mayor: 15 hallazgos menores, todos resueltos. Una segunda pasada comprobó los arreglos y dejó seis retoques, también aplicados: la definición exacta de SIG_PART, el orden del sello con el certificado anidado, dos frases sobre la fuga del área de 512, el paso 6 del escritor, las longitudes de una fixture y un enlace del handoff.

# Hallazgo Resolución
1 Un head de versión 2 dentro del formato 3 solo fallaría tras pedir el release, contra §22 El head va siempre en su versión 1, y la pregunta 5 de la entrega 1 pasa a «No» (apartados 2 y 9)
2 El área de la entrega 3 dependía de que el escritor supiera sellar, y un área de 512 delata que no hay sello El área depende solo de la versión del spec; dos fugas nuevas declaradas; la alternativa de 4096 en la pregunta 2; la versión del spec de cada entrega (apartados 0.4, 2 y 9)
3 Faltaban §55.1 y otras secciones en los cambios del spec Añadidas (apartados 8 y 15)
4 Los avisos al extraer comparaban el nombre tal cual Comparan la clave de R7 (apartado 3)
5 HFS+ solo ignora ZWNJ y ZWJ, y su límite es de 255 unidades UTF-16 tras NFD R3 corregido y con ese límite (apartado 3)
6 Un mapa de firma mal formado llevaba a X y arrastraba al sello Las claves 2 y 3 de security van codificadas aparte (apartados 2 y 4)
7 Faltaban pruebas de S1, de X por la capa 3 y de una trama que no cabe en L Añadidas (apartado 8)
8 Una raíz desconocida no se distinguía de una firma de raíz inválida Identificador de la raíz en el certificado y un orden fijo de S1, S2 y S3 (apartados 4 y 18)
9 La CSP es global y prohíbe los workers Requisitos de CSP por página y del worker (apartado 19)
10 Un sello fallido abortaba la cápsula Se descarta el sello, y la persona elige seguir o cancelar; aviso si faltan menos de 2 min (apartado 19)
11 El ancho de línea de la CLI fallaba con TAB, con «│» en un terminal CJK y con terminales estrechos TAB expandido, trozos de W − 3 y W de al menos 20 (apartado 4)
12 Faltaban requisitos para el documento del servicio El origen del checkpoint y la longitud de la prueba en la verificación; el cambio de clave anual y la prueba de trabajo como preguntas (apartados 18, 19 y 22)
13 El área de uso privado U+F000–U+F0FF confunde en Cygwin, WSL y el SMB de macOS Prohibida en R4 (apartado 3)
14 Los ficheros sustituidos seguían en el repo Borrados; el paquete que revisó Fable sigue en git y en el artefacto publicado
15 Precisiones R8 y localeCompare, los prefijos de dominio, la cota con área de 512, los checkpoints cada 60 s y Linux en la pregunta 4

Comprobado por el revisor:

  • la aritmética de tamaños, con un codificador CBOR propio;
  • que las entregas 2 y 3 no cambian ningún byte de la trama, del head ni del control;
  • el paso 17 y sus ejemplos, con el comportamiento de stream.go y de flush;
  • la tabla de verdad, las versiones de Unicode y la lista blanca;
  • R6b en Go y en JavaScript, y en el NTFS de esta máquina;
  • el caso de Ed25519 en Go 1.26.8 y en noble 2.4.0;
  • las referencias al código y las cruzadas.

Quedan sin verificar Unicode 17.0, las tablas WindowsBestFit, HFS+ en una máquina y los mapeos de Cygwin, WSL y el SMB de macOS.

Powered by TurnKey Linux.