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.

28 KiB

Formato de cápsula 3 (spec v0.10): security | head | content

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

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

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.

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

Powered by TurnKey Linux.