# datekeys-go Implementación de referencia en Go de la **DateKeys Protocol Specification v0.14** ([`spec/`](spec/DateKeys_Protocol_Specification_v0.14.md)), etiquetada `spec-v0.14`, que no cambia ningún formato de la v0.11, la v0.12 ni la v0.13. [English version](README.md). DateKeys cifra datos de forma que solo puedan abrirse a partir de un instante elegido. La condición temporal procede del beacon de aleatoriedad **Quicknet** de drand: los datos se sellan con cifrado timelock hacia una ronda futura, y la firma BLS de esa ronda, que drand publica cuando llega, es la llave. Todo lo que se puede verificar localmente se verifica localmente; relays, cachés y APIs son transportes no confiables. > **Estado: v0.x, pre-estándar.** La especificación es un borrador y la API > puede cambiar antes de v1.0.0. El código aún no ha pasado una revisión > criptográfica externa (spec §75). No lo uses para secretos de alto valor. ## Qué implementa | Objeto | Spec | Paquete | |---|---|---| | DateKey: fecha → ronda, cadena canónica `dk1_…` | §14–§19 | [`datekey`](datekey) | | Provider Profile, perfil Quicknet pinneado, `profile_hash` | §10–§13 | [`profile`](profile) | | Fuentes de releases, verificación BLS local, relays drand | §45–§52 | [`provider`](provider), [`provider/drand`](provider/drand) | | DateKeyCap `.dkc`: `time_only` y `time_and_key`, los ficheros del formato 3, el área de seguridad y sus veredictos | §20–§39, §61–§63 | [`capsule`](capsule) | | Rutas y textos del head, con las tablas de Unicode 18.0.0 y best-fit | §29.5, §29.6 | [`internal/pathrule`](internal/pathrule) | | Firma de autor: qué se firma, `alg` 1 y 2, el sello de `seal_type` 2 | §29.8–§29.11 | [`capsule`](capsule) | | Claves de autor `dkauthor1…` y el Ed25519 estricto de `alg` 1 | §29.9, §29.12 | [`authorkey`](authorkey), [`internal/ed25519strict`](internal/ed25519strict) | | Firma con certificados (`alg` 2, CMS) y sello de tiempo (RFC 3161): DER, el perfil del certificado, la tabla cerrada de algoritmos | §29.10, §29.11 | [`internal/cms`](internal/cms), [`internal/der`](internal/der) | | La nota pública `datekeys.note` | §24.1 | [`extension`](extension), [`capsule`](capsule) | | Llave de palabras | §38.1 | [`wordkey`](wordkey) | | DateKeys Access Key `.dkk` | §40–§44 | [`accesskey`](accesskey) | | La extensión `datekeys.capsule`: localizador, sobre y direcciones | §44.1 | [`locator`](locator) | | Extensiones y su registro | §54, §72 | [`extension`](extension) | | CBOR determinista | §58 | [`codec`](codec) | | Errores normativos | §69 | [`errors.go`](errors.go) | | CLI | — | [`cmd/datekeys`](cmd/datekeys) | Aquí no se implementa criptografía. El cifrado es [age](https://age-encryption.org) (`filippo.io/age`); el timelock es [tlock](https://github.com/drand/tlock) (solo su núcleo exportado); la verificación BLS es la de drand; las firmas de autor y los sellos se verifican con la biblioteca estándar de Go, con el perfil estricto de Ed25519 comprobado alrededor de `crypto/ed25519`. Este módulo aporta framing, CBOR, DER, bindings, reglas de verificación y flujo, y aplica las reglas de stanzas del protocolo dentro de las identities de age, para que un fichero nunca se acepte solo porque age haya podido desenvolver una clave. No implementa, a propósito: el servidor y la cola de la Release API, el almacenamiento y la entrega, la descarga del resto de un sobre ni el cliente TypeScript (plan §2). El paquete `locator` comprueba las direcciones de un localizador y lo que un lector trae de ellas, y no descarga nada (spec §44.1). La firma con certificados y el sello comprueban la criptografía, nunca quién emitió un certificado o un sello ni si se revocó: eso lo hace un validador oficial (spec §29.10). Las curvas brainpool y los algoritmos nacionales como GOST o SM2 quedan fuera de la tabla. ## Versiones Hay tres números de versión, cada uno con su significado: | Versión | Dónde | Cambia cuando | |---|---|---| | Formato | Dentro de los objetos: el formato de la cápsula, el `VERSION` del prelude de DKC1, 3 al escribir y de 1 a 3 al leer, que es también la versión de schema de CONTROL_CBOR; y 1 en la trama de DKK1 y en el schema de los demás objetos, incluidos el head y `security` del formato 3 | Cambia el formato. Un lector rechaza una versión que no conoce (spec §22, §70) | | Especificación | `datekeys.SpecVersion`, hoy `0.14`, y el tag `spec-v0.14`. Un borrador, como lo fue la v0.14, no tiene tag ni la cambia | Cambia el texto normativo. §76 del spec recoge cada cambio con su caso | | Módulo | Los tags de este módulo Go, `vX.Y.Z`, y `datekeys.Version()` | Cambia la API o el comportamiento. Versionado semántico, sin promesa de estabilidad antes de v1.0.0 | `datekeys version` imprime la versión del módulo, la del spec y la del toolchain de Go. Un binario compilado en un checkout muestra la pseudo-versión de su commit, por ejemplo `v0.0.0-20260928105528-9ac9cd952f04`. Cada release dice qué cubre, aquí y en el [CHANGELOG](CHANGELOG.md). El código de esta rama, aún sin publicar, cubre: - la especificación 0.14: escribe el formato 3 de cápsula y lee los formatos 1, 2 y 3; - el perfil Quicknet pinneado, y cualquier perfil de los tres schemes de drand que soporta tlock; - cifrado, inspección y apertura, firmas de autor y sellos, la nota pública, la llave de palabras y el localizador, y la CLI; - todos los vectores y fixtures compartidos de [`testdata/`](testdata). El primer tag, v0.1.0, llegará cuando `go get` funcione desde una máquina limpia. ## Una cápsula, en un dibujo ```text .dkc = PRELUDE (16 B) || PUBLIC_HEADER (CBOR) || SEALED_CONTROL (age) || PAYLOAD_AGE (age, hasta EOF) time_only: SEALED_CONTROL = age(tlock ronda R → CONTROL_CBOR) time_and_key: SEALED_CONTROL = age(tlock ronda R → age(16 stanzas X25519 → CONTROL_CBOR)) CONTROL_CBOR = { header_binding = SHA-256(PRELUDE || PUBLIC_HEADER), I_PAYLOAD, extensiones, L, regla de relleno } PAYLOAD_AGE = age(X25519 R_PAYLOAD → BODY || ceros hasta P = regla(L)), en streaming BODY = AREA_LEN || SECURITY_LEN || HEAD_LEN (3 × uint32) || SECURITY_CBOR, ceros hasta AREA_LEN (32 KiB) firma de autor y sello, o vacío || HEAD_CBOR sal, comentario, autor declarado, los ficheros || los ficheros, uno tras otro ``` Es el formato 3, el que escribe `encrypt` (spec §22, §29.2). Sus 16 stanzas llevan de 1 a 16 credenciales y un señuelo en cada hueco libre, en orden aleatorio, y su payload se rellena hasta P: hasta la fecha nadie sabe cuántas credenciales hay, ni la longitud exacta L, ni los nombres de los ficheros ni cuántos son, más allá de lo que acota P (spec §29.1, §39, §55.2). El área de seguridad mide 32 KiB, lleve o no firma, para que P no diga si la cápsula va firmada, y 64 KiB solo cuando quien la crea lo pide porque una firma no cabe (spec §29.2). Los escritores de la v0.10 escribían 512 bytes, que un lector sigue aceptando. Ya abierta, da ficheros con sus rutas, tamaños, SHA-256 y fechas de modificación, un comentario y un autor declarado, y los veredictos de su área de seguridad. El formato 2, el de la v0.9, lleva un solo contenido; el formato 1, el de la v0.8.2, lleva un stanza por credencial y no rellena. Los lectores siguen abriendo los dos. ## CLI ```bash go install g.activething.com/go/DateKeys/cmd/datekeys@latest ``` El módulo se sirve desde el Gitea del proyecto, cuyo certificado Go no reconoce por defecto. Define `GOPRIVATE=g.activething.com` para que el proxy y la base de datos de sumas de Go no intervengan, e instala el certificado del servidor o, en una red de confianza, define `GOINSECURE=g.activething.com`. ```bash datekeys datekey resolve -at 2030-01-01T00:00:00Z datekeys encrypt -at 2030-01-01T00:00:00Z -in carta.txt -out carta.dkc datekeys encrypt -at 2030-01-01T00:00:00Z -in fotos -in carta.txt -comment "Para Ana" -author "Juan" -out regalo.dkc datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk regalo.dkk -in fotos -out regalo.dkc datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -words-file palabras.txt -in carta.txt -out carta.dkc datekeys encrypt -at 2030-01-01T00:00:00Z -padding bloque256 -no-mtime -in carta.txt -out carta.dkc datekeys author keygen -out autor.key -pass-file clave.txt datekeys encrypt -at 2030-01-01T00:00:00Z -in carta.txt -note "Cartas de Lisboa" -sign autor.key -sign-pass-file clave.txt -out carta.dkc datekeys decrypt -in carta.dkc -out carta -expect-author dkauthor1... datekeys inspect -in regalo.dkc datekeys decrypt -in regalo.dkc -out regalo -dkk regalo.dkk datekeys profile hash datekeys version ``` `encrypt` nunca usa la red. Cada `-in` es un fichero o una carpeta; una carpeta da su nombre como primer segmento de sus rutas, como hace un navegador, y se recorre sin seguir enlaces, tomando solo ficheros regulares y dejando fuera `.DS_Store`, `Thumbs.db`, `desktop.ini`, `._*` y `__MACOSX`. Una ruta o un texto que incumple una regla de spec §29.5 o §29.6 se rechaza, nombrando la regla y el carácter. Los ficheros conservan su fecha de modificación salvo con `-no-mtime`. El contenido se rellena con la regla reforzado, o con bloque256 si se pide (spec §29.1). `inspect` ejecuta solo las comprobaciones previas al desbloqueo (spec §63, pasos 1 a 8): nunca pide un release ni usa secretos. `decrypt` obtiene el release de relays públicos de drand y lo verifica localmente. Los ficheros de una cápsula de formato 3 van a la carpeta nueva `-out`, preparados dentro de ella y movidos a su sitio solo cuando todas las comprobaciones han pasado (spec §56); una cápsula con solo un comentario no crea la carpeta. Muestra los veredictos al principio y al final, y el autor declarado, el comentario y las rutas como texto del creador que nadie ha comprobado, cada línea tras el prefijo `│ ` y nunca más ancha que el terminal (spec §29.7). Una línea de los veredictos se parte por el último espacio que cabe, y cada fila tras la primera empieza por ` ↳ ` (dos espacios, U+21B3 y un espacio): el terminal nunca parte una, y ningún nombre de un certificado puede empezar una fila y pasar por un veredicto. Los formatos 1 y 2 siguen dando un fichero. Nunca se sobrescriben ficheros de salida. `author keygen` crea una clave de autor Ed25519 (spec §29.12) en un fichero cifrado con una contraseña, o en texto con `-plain`, e imprime su clave pública, `dkauthor1…`; `author public` la vuelve a imprimir. La contraseña llega de un fichero, o de la entrada estándar con `-`, nunca de la línea de órdenes ni del entorno. `encrypt -sign` firma la cápsula con ella (`alg` 1): antes de firmar muestra la clave y el código de `AUTHOR_MESSAGE` (spec §62.1, regla 20), y comprueba la firma antes de escribir nada. `decrypt` muestra la firma; con `-expect-author` falla, sin escribir nada, salvo que la cápsula lleve una firma válida de esa clave pública, el veredicto F4: compara la clave antes del paso 18, cuando aún no se ha publicado ningún fichero. `encrypt -note` pone una nota pública en claro en la cápsula (spec §24.1), y avisa de que es pública. `inspect` y `decrypt` la muestran como texto del creador que nadie ha comprobado; una nota que incumple las reglas de texto no se muestra, y los dos lo dicen (`public_note_unusable` en `inspect -json`). `-words` y `-words-file` dan a una cápsula `time_and_key` una llave de palabras: al menos seis palabras distintas de tres letras o más, que la abren con `decrypt -words-file` en lugar de una `.dkk` (spec §38.1). ## Librería ```go reg, err := profile.Default() // perfil Quicknet pinneado, comprobado contra su profile_hash // EncryptFiles: sin red, la ronda se resuelve localmente. Lee cada fichero // dos veces, y falla si un fichero cambia entre las dos lecturas. res, err := capsule.EncryptFiles(dst, []capsule.Source{{ Path: "fotos/playa.jpg", Size: info.Size(), ModTime: info.ModTime(), Open: func() (io.ReadCloser, error) { return os.Open(nombre) }, }}, capsule.EncryptOptions{ Profile: profile.Quicknet(), UnlockAt: time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC), Policy: capsule.TimeAndKey, NewPortableKey: true, // res.PortableKey es la .dkk; se codifica con accesskey.Encode Comment: "Para Ana", Now: time.Now, }) // Inspeccionar: pasos 1 a 8, sin red ni secretos. in, err := capsule.Inspect(f, capsule.InspectOptions{Registry: reg}) // Abrir: pasos 1 a 18; el release se verifica localmente. Una cápsula de // formato 3 entrega sus ficheros a un Sink, que los publica en Commit, en el // paso 18. opened, err := capsule.Open(ctx, nil, f, capsule.OpenOptions{ Registry: reg, Source: drand.New(), AccessKey: key, // o Identities: []age.Identity{...} Sink: sink, Now: time.Now, }) if errors.Is(err, datekeys.ErrReleaseUnavailable) { /* todavía no */ } ``` Un `Sink` recibe `Begin` con el head, `Create` para cada fichero y `Commit` solo cuando el paso 17 ha pasado; tras cualquier fallo recibe `Abort`, y nada de lo que recibió puede presentarse como válido (spec §56). `opened.Head` lleva las rutas, los tamaños, los SHA-256 y las fechas de modificación, el comentario y el autor declarado, y `opened.Verdicts.Lines()` los veredictos que se muestran antes (spec §29.7). `EncryptOptions.AuthorKey` o `CMSSigner` firman la cápsula, y `Sealer` la sella (spec §29.9 a §29.11); `OpenOptions.AuthorKeys` son las claves que la persona guardó, que dan F3, y `OpenOptions.Accept` ve los veredictos antes del paso 18 y puede negarse a publicar los ficheros. `Open` sin `Sink` se detiene justo tras el paso 2 con `capsule.ErrSinkRequired`, un error del llamador sin código. Las cápsulas de los formatos 1 y 2 siguen escribiendo su contenido en streaming en `dst`, nunca el relleno. `opened.Format` es el formato de la cápsula: el formato 1 no oculta el número de credenciales ni la longitud exacta del contenido, así que muéstralo (spec §70). Todo fallo del protocolo envuelve uno de los 18 errores normativos del §69, así que `errors.Is` y `datekeys.Code(err)` lo identifican. ## Propiedades de seguridad y límites - **Confidencialidad temporal** bajo el supuesto de umbral de drand. El timelock de Quicknet **no es post-cuántico**: los ciphertexts guardados durante años quedan expuestos a *harvest now, decrypt later* (spec §7.7, §53). - **Sin confianza en servidores**: el perfil va pinneado en el binario, la ronda se calcula localmente, los releases se verifican con BLS localmente y una firma válida de otra ronda se rechaza (spec §13, §17, §51). - **Integridad**: framing, cabecera, control y payload están autenticados frente a quien no conoce las file keys: un cambio de un tercero hace fallar la apertura (fixtures y corpus de mutaciones). Publicada la ronda, cualquiera puede calcular la file key temporal, y cualquiera puede sellar un control nuevo para una cabecera pública; quién puede escribir cada parte y desde qué paso queda vinculada es el modelo de confianza de spec §27 y §55.1. - **Privacidad de metadatos**: hasta la fecha quedan ocultos la longitud exacta del contenido, el número de credenciales, y los nombres, tamaños y número de los ficheros, más allá de lo que acota el tamaño con relleno P, y también si la cápsula va firmada, salvo que se haya ampliado su área; no la fecha, la política de acceso, `capsule_id` ni las extensiones de la cabecera, entre ellas la nota pública (spec §55.2). - **Extracción segura**: las rutas del head no pueden salir de la carpeta, colisionar en Windows, macOS o Linux, esconder caracteres ni cambiar la dirección del texto; las reglas usan tablas fijas de Unicode 18.0.0, nunca las de la plataforma (spec §29.5, §29.6). - **Autoría solo por una firma**: el autor declarado, el comentario, la nota pública y las fechas de modificación son texto del creador que no prueba nada (spec §24.1, §29.7, §36.1). Una firma de `alg` 1 prueba que firmó quien tenía la clave secreta, no quién la tiene; de una firma con certificados y de un sello, DateKeys comprueba la criptografía y las fechas, nunca quién los emitió ni si se revocaron (spec §29.9 a §29.11). Los veredictos nunca deciden la apertura (spec §29.3). `time_and_key` añade una barrera de acceso, no una firma. - **Recuperar años después** exige el release histórico: de un relay drand que aún lo sirva o de cualquier caché, verificado de nuevo localmente (spec §50). Ver [SECURITY.md](SECURITY.md). ## Conformidad y tests ```bash go test ./... # unitarios, vectores golden, fixtures, mutaciones go test -race -cover ./... go test -fuzz=FuzzInspect ./capsule # un objetivo de fuzzing cada vez go test -tags interop ./capsule # las CLI oficiales age y tle abren nuestros ficheros go test -tags integration ./capsule ./provider/drand # Quicknet en vivo ``` - `testdata/vectors`: vectores de `profile_hash`, fecha→ronda y `dk1_` (spec §65, §66), vectores del perfil CBOR y de cada schema, vectores del relleno (spec §29.1), las rutas, las claves de R7, los heads y las áreas de seguridad del formato 3 (spec §67), el Ed25519 estricto de `alg` 1, las firmas con certificados y los sellos, la nota pública y la extensión `datekeys.capsule`, el corpus de mutaciones exportado y un corpus diferencial de las comprobaciones previas al desbloqueo; formatos en [`testdata/README.md`](testdata/README.md). - `testdata/fixtures`: fixtures oficiales `.dkc`/`.dkk` sobre rondas ya publicadas, con la firma BLS embebida y todos los valores intermedios (spec §67, §68); se descifran sin red. Catorce son de formato 3, cinco de ellas con el área de 32 KiB de la v0.11: sin firma, firmada con `alg` 1, sellada, firmada con certificados y con una nota pública. Las siete de formato 2, de la v0.9, y las cinco de formato 1, de la v0.8.2, se conservan por compatibilidad. Cada `.dkc` tiene congelada su salida de `datekeys inspect -json`. - `internal/testkit.Mutations`: las mutaciones del §64, las 33 de sus dos primeras listas en cada formato, las 23 de la lista del formato 2, las 48 de la del formato 3 y 8 de la de la v0.11, y 40 más, cada una con su error y su paso exactos, o sus veredictos cuando se abre, comprobando además que los fallos previos al desbloqueo nunca provocan una petición de release; exportadas a `testdata/vectors/mutations.json`. - [`docs/traceability.md`](docs/traceability.md): sección del spec → código → test. - [`spec/datekeys.cddl`](spec/datekeys.cddl): schemas CBOR. ## Licencia Código: Apache-2.0 ([LICENSE](LICENSE)). Especificación: CC-BY-4.0 ([spec/README.md](spec/README.md)). `codec/bech32` se copia de age bajo su propia licencia. "DateKeys" es un nombre reservado: ver [TRADEMARKS.md](TRADEMARKS.md).