11 KiB
datekeys-go
Implementación de referencia en Go de la DateKeys Protocol Specification
v0.9 (spec/).
English version.
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 |
Provider Profile, perfil Quicknet pinneado, profile_hash |
§10–§13 | profile |
| Fuentes de releases, verificación BLS local, relays drand | §45–§52 | provider, provider/drand |
DateKeyCap .dkc: time_only y time_and_key |
§20–§39, §61–§63 | capsule |
DateKeys Access Key .dkk |
§40–§44 | accesskey |
| Extensiones | §54 | extension |
| CBOR determinista | §58 | codec |
| Errores normativos | §69 | errors.go |
| CLI | — | cmd/datekeys |
Aquí no se implementa criptografía. El cifrado es age
(filippo.io/age); el timelock es tlock
(solo su núcleo exportado); la verificación BLS es la de drand. Este módulo
aporta framing, CBOR, 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, extensiones concretas ni el cliente TypeScript (plan §2).
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, 2 al escribir y 1 o 2 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 |
Cambia el formato. Un lector rechaza una versión que no conoce (spec §22, §70) |
| Especificación | datekeys.SpecVersion, hoy 0.9, y el tag spec-v0.9 |
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. El código de main, aún sin publicar, cubre:
- la especificación 0.9: escribe el formato 2 de cápsula y lee los formatos 1 y 2;
- el perfil Quicknet pinneado, y cualquier perfil de los tres schemes de drand que soporta tlock;
- cifrado, inspección y apertura, y la CLI;
- todos los vectores y fixtures compartidos de
testdata/.
El primer tag, v0.1.0, llegará cuando go get funcione desde una máquina limpia.
Una cápsula, en un dibujo
.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 → tus datos || ceros hasta P = regla(L)), en streaming
Es el formato 2, el que escribe encrypt (spec §22). 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 (spec §29.1, §39, §55.2). El formato 1, el de la
v0.8.2, lleva un stanza por credencial y no rellena; los lectores lo siguen
abriendo.
CLI
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.
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 -policy time_and_key -dkk carta.dkk -in carta.txt -out carta.dkc
datekeys encrypt -at 2030-01-01T00:00:00Z -padding bloque256 -in carta.txt -out carta.dkc
datekeys inspect -in carta.dkc
datekeys decrypt -in carta.dkc -out carta.txt -dkk carta.dkk
datekeys profile hash
datekeys version
encrypt nunca usa la red. Rellena el contenido 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, lo verifica
localmente y publica el plaintext solo cuando age lo ha autenticado entero.
Nunca se sobrescriben ficheros de salida.
Librería
reg, err := profile.Default() // perfil Quicknet pinneado, comprobado contra su profile_hash
// Cifrar: sin red, la ronda se resuelve localmente. El formato 2 sella la
// longitud del contenido antes que el contenido: src entrega exactamente
// Length bytes.
res, err := capsule.Encrypt(dst, src, 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
Length: size,
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.
opened, err := capsule.Open(ctx, tmp, f, capsule.OpenOptions{
Registry: reg,
Source: drand.New(),
AccessKey: key, // o Identities: []age.Identity{...}
Now: time.Now,
})
if errors.Is(err, datekeys.ErrReleaseUnavailable) { /* todavía no */ }
Open escribe el contenido en streaming, nunca el relleno; si falla, descarta
lo escrito y no lo presentes como válido (spec §56). 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 17 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 (formato 2): hasta la fecha quedan ocultos la
longitud exacta del contenido y el número de credenciales; no la fecha, la
política de acceso,
capsule_id, las extensiones de la cabecera ni el tamaño con relleno P (spec §55.2). - Sin autoría:
time_onlyda coherencia interna, no prueba de quién creó la cápsula, ni antes ni después de madurar;time_and_keyañade una barrera de acceso, no una firma (spec §36.1). - 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.
Conformidad y tests
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 deprofile_hash, fecha→ronda ydk1_(spec §65, §66), vectores del perfil CBOR y de cada schema, vectores del relleno (spec §29.1), el corpus de mutaciones exportado y un corpus diferencial de las comprobaciones previas al desbloqueo; formatos entestdata/README.md.testdata/fixtures: fixtures oficiales.dkc/.dkksobre rondas ya publicadas, con la firma BLS embebida y todos los valores intermedios (spec §67, §68); se descifran sin red. Siete son de formato 2; los cinco de formato 1, de la v0.8.2, se conservan por compatibilidad. Cada.dkctiene congelada su salida dedatekeys inspect -json.internal/testkit.Mutations: las mutaciones del §64, las 33 de sus dos primeras listas en cada formato y las 22 de la tercera, y 37 más, cada una con su error y su paso exactos, comprobando además que los fallos previos al desbloqueo nunca provocan una petición de release; exportadas atestdata/vectors/mutations.json.docs/traceability.md: sección del spec → código → test.spec/datekeys.cddl: schemas CBOR.
Licencia
Código: Apache-2.0 (LICENSE). Especificación: CC-BY-4.0
(spec/README.md). codec/bech32 se copia de age bajo su
propia licencia. "DateKeys" es un nombre reservado: ver TRADEMARKS.md.