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
VERSION= 3;CONTROL_CBORen schema 3 con las claves de la 2 (103 bytes:SEALED_CONTROL_LENno cambia). No cambianPUBLIC_HEADER, la.dkk, los 16 huecos, L_MAX ni los códigos de relleno.- Plaintext de
PAYLOAD_AGE=BODY || 0x00^(P − L),BODY = SECURITY_LEN || HEAD_LEN || SECURITY_CBOR || área || HEAD_CBOR || CONTENT, L = |BODY| (clave 6). - HEAD: versión propia, sal, comentario, autor declarado y, por fichero, ruta, size, start, end, SHA-256 y mtime.
- SECURITY: firma Ed25519 y sello, opcionales, en un área fija de 512 bytes que no cambia L ni P.
- Se firma y se sella
control_commit || head_digest.control_commitlleva un compromiso deI_PAYLOAD, no la clave, para que un tercero pueda verificar. El autor firma primero; el sello cubre la firma. - SECURITY nunca decide la apertura (objetivo 6): solo da veredictos de texto fijo.
- Ed25519 estricto sin cofactor, igual en Go y TypeScript.
- Rutas estrictas, con plegado Unicode 17.0.0 y proyecciones best-fit de Windows en tablas propias.
- En el paso 17 prevalece cualquier fallo de
age(ERR_INTEGRITY); si no lo hay, el orden del plaintext. Código nuevoERR_HEAD_INVALID. - 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/agev1.3.2 (stream.go:117-156) entrega un chunk completo como no final antes deErrUnexpectedEOF;age-encryption0.3.1 solo lo prueba como final enflush. Si decidiera la posición, Go daríaERR_HEAD_INVALIDy TypeScriptERR_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, luegoERR_EXTENSION_DATA_INVALID.
- claves 3, 4 y 5 →
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.MkdirAlly el Explorador fusionarían dos carpetas distintas. -
R6c. Best-fit. Para cada
bestfit*.txtde 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
.dkcy la.dkksiguen llamándosecapsula-<fecha de apertura en UTC>. - Un fichero único se descarga con su nombre, pasado por
safeFileName. En OPFS conserva el nombre fijoTEMP_FILE(tempfile.ts:24): sin Web Locks,isStalebusca ese nombre y borraría el directorio de otra pestaña. - El ZIP se llama
<primer segmento común>.zipo<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_PUBson los bytes deCONTROL_CBORdel paso 14, con los 32 de la clave 3 sustituidos porpayload_commit;control_commit = SHA-256(CONTROL_PUB). Cubre, sin revelarI_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 deHEAD_CBOR; fijaCONTENTbyte 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:
- |A| = 32 y |sig| = 64.
- A es canónica (y < p, y el signo es 0 si y ∈ {1, p−1}), decodifica y no es de orden pequeño.
sig[63] & 0xE0 = 0y S < ℓ.- [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.Verifyacepta 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.verifyusa el cofactor y acepta ese mismo caso conzip215: true. MUST NOT llamarse, y una prueba de guarda lo impide. Se usaPoint.fromBytes(A, false),isSmallOrder(),multiplyUnsafeysha512.
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_digestantes 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/v1en el mismo origen, con 32 bytesapplication/octet-stream.fetchconcredentials:'omit',referrerPolicy:'no-referrer',cache:'no-store'yredirect:'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_hashse 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/v1MUST 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_type2; eIDAS puede llegar después, sobre los checkpoints. - OpenTimestamps dentro de la cápsula: al crear, la prueba está pendiente. Queda reservado
seal_type3.
6. Claves de autor
Formato.
- Semilla Ed25519 de 32 bytes.
- Clave pública
dkauthor1…(bech32, 67 caracteres); clave secretaDKAUTHOR-SECRET-KEY-1…(79 caracteres). - El fichero imita una identidad de
agey por defecto va envuelto enagescrypt con logN = 16 (64 MiB). Elscryptde 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.getRandomValuesyed25519.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 publicyencrypt -sign. La passphrase se lee de-passphrase-file, o sin eco con código propio sobregolang.org/x/sys, que ya está engo.modcomo indirecta: ningún módulo nuevo. - La API expone
AUTHOR_MESSAGEpara 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
Lstaty acepta solo ficheros regulares. Cada directorio de-inaporta su nombre como primer segmento, igual quewebkitRelativePath. - 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.
- Rutas y mtime (⌊lastModified/1000⌋ o
- Fase B:
- §61 y §62 con
VERSION3. - La sal y
HEAD_CBOR. CONTROL_CBORv3,control_commityhead_digest.- La firma, verificada (MUST).
- 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.sizeno cambia. SECURITY_CBORy la autodecodificación de los tres CBOR (MUST).INNER_ACCESS_AGEyOUTER_TIME_AGE.- La escritura, con una pasada 2 que aborta si un tamaño o un hash difieren.
- La
.dkkconcapsule_digest.
- §61 y §62 con
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.
- 17.1
Precedencia (§69.1 y final del paso 17).
- Un fallo de
agedespués de su cabecera, o un plaintext de longitud ≠ P, esERR_INTEGRITYy 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_INTEGRITYen 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 (
TempFileHandlepasa aFileSystemWritableFileStream). - 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:os.Mkdir(DIR, 0700)reclama DIR y falla si ya existe. Unos.Renamefinal no serviría: en POSIX sustituiría un DIR vacío.- El árbol se escribe en
DIR/.datekeys-*conos.OpenRoot,O_EXCL, permisos 0600 yRoot.Chtimes. - Tras el paso 18,
Root.Renamelleva cada entrada al primer nivel de DIR; ante cualquier fallo,RemoveAll(DIR). - Una cápsula sin ficheros no crea DIR.
- Los veredictos, el autor y el comentario van a stdout, en el orden de §4.
8. Qué ve cada uno
- Quien tiene el
.dkcantes 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.
- lo mismo que en el formato 2, más
- 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
VERSION3 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
VERSIONentre 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:40yframing_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:125yframing_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,authorkeyy la CLI (-incon directorios,-comment,-author,-no-mtime,-sign,-seal,decrypt -out DIR,authorystatement). - TypeScript: los mismos módulos, más
seal-verify,zip.ts,crc32.tsy guardas contraed25519.verifyyfetch.
Pruebas.
- Fixtures:
format3_single,_tree,_comment_only,_signed,_signed_sealed,_sealed_after,_security_v2,_bloque256y_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.jsonyhead_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:
VERSION4, 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/zipy el extra 0x000A;- la CLI nunca escribe fuera de DIR;
- un lector que informa
ERR_HEAD_INVALIDantes de EOF no es conforme.
11. Preguntas abiertas (con recomendación)
- ¿Código nuevo
ERR_HEAD_INVALID? Sí: separa un escritor defectuoso de un CBOR roto. - ¿Área de 512 bytes? Sí: cuesta unos 490 bytes y oculta la firma y el sello.
- ¿Tablas propias? Sí: la regla ligera deja pasar colisiones NFC/NFD.
- ¿Aceptar que un head nuevo falle tras la fecha? Sí.
- ¿Clave con ventana? Sí, de un año y con destrucción documentada; la raíz offline, más adelante.
- ¿Hora del sello al minuto? Sí.
- ¿Sello RFC 3161 cualificado? En una segunda fase.
- ¿Escribir solo el formato 3? Sí.
- Límites: comentario 16 KiB, autor 256 B, ruta 1024 B, segmento 255 B, profundidad 32, 65535 ficheros y head 16 MiB. Falta confirmarlos.
- ¿Un único fichero dentro de una carpeta? En ZIP.
- ¿Fichero de clave cifrado por defecto? Sí, con logN 16.
- ¿Cápsulas solo con comentario? El lector acepta C = 0; el escritor exige un fichero o un comentario.
- ¿Rechazar un '-' inicial? No, solo avisar.
- ¿Testigos de checkpoints? En la fase 2.
Revisión adversarial
Las 28 objeciones son reales; no se descarta ninguna.
control_digestrevelaI_PAYLOAD(mayor). Corregido: se firmacontrol_commit, conpayload_commit, y un paquete de verificación (§4). Se usa un hash deI_PAYLOADen vez deR_PAYLOAD: conR_PAYLOAD, quien recibe el paquete podría cifrar otroPAYLOAD_AGEque el control abriría.- 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.
- «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.
- 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.
- TOFU. Corregido (§6).
- Un comentario que imita veredictos. Corregido: presentación normativa y texto nuevo para la firma no soportada.
- «Espacio» sin definir. Corregido: es U+0020.
- Alias 8.3. Corregido con R6b; comprobado en esta máquina. Git solo protege
.git; aquí se generaliza. - Vinculación tras la apertura. Corregido (§5 y §8).
- Canal de difusión. Corregido: se publica el hash del subject y
fetchqueda aislado. - MAY de fallo rápido (mayor). Corregido. Comprobado que contradecía el Alcance de §69.1 y el final del paso 17.
- Compromiso sin red (mayor). Igual que 3, con un vector.
- Tiempos del ZIP (mayor). Corregido byte a byte.
Opensin sumidero. Corregido: error del llamador, como ya hacenDecodeControlcon el formato 3 (framing_test.go:243) yopen(open.ts:139-142).- Precedencia de R1 y R8. Corregido.
- Best-fit (repetida). Igual que 4.
- Tamaños con alg 1. Corregido: capa 3, «ilegible».
- «Espacio» (repetida). Igual que 7.
- HEAD de más de 16 MiB. Corregido en la fase A.
- Carpetas y ZIP64. Corregido.
- Nombre del fichero en OPFS. Corregido; comprobado en
isStale(tempfile.ts:198). - Salida de la CLI. Corregido:
Mkdir,Root.Rename(existe en Go 1.26), stdout, cápsulas sin ficheros y-in. - Número de ficheros visible. Corregido: P da una cota (§8).
- Tolerancia inferior del ancla. Igual que 2.
- Puntos de código sin asignar. Corregido: Cn entra en R4.
- scrypt y passphrase. Corregido: logN 16;
x/sysya es indirecta. - Pruebas del escritor de formato 2. Corregido: opción solo para pruebas (
encrypt.test.ts:116). - CDDL del perfil de sello. Corregido.