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.

21 KiB

Plan de implementación: datekeys-go

Implementación de referencia en Go de la DateKeys Protocol Specification v0.8.1.

Estado: borrador para revisión. Fecha: 25 de septiembre de 2026.


1. Objetivo y principios

Construir desde cero una librería Go que implemente el protocolo base tal como lo fija la v0.8.1, con estas reglas:

  1. La especificación manda. El código no añade semántica. Si el código descubre un problema en el spec, se abre un caso reproducible según la política de cambios de la sección 76.
  2. Cero criptografía propia. Solo age, tlock y la verificación BLS de drand. La librería aporta framing, CBOR, bindings, reglas de verificación y flujo.
  3. Validar todo lo verificable localmente antes de tocar red o secretos. Sección 63.
  4. Fallar cerrado. Ningún plaintext parcial, ningún error enmascarado, ningún "verified: true" ajeno.
  5. Superficie pequeña y auditable. Pocas dependencias, todas pinneadas, builds reproducibles, y un mapa de trazabilidad spec ↔ código para la revisión externa.
  6. Nueva librería, no refactor. Lo aprovechable del prototipo se copia y se adapta; el repositorio actual no se modifica.

2. Alcance de la primera versión de la librería (v0.1.0)

Implementa:

  • DateKey: resolución fecha → ronda, dk1_ canónico, vectores (secciones 14 a 19).
  • Provider Profile Quicknet: struct, CBOR canónico, profile_hash, registro pinneado (secciones 10 a 13).
  • Adaptador drand: obtención de releases por relays, verificación BLS local, modo estricto (secciones 35, 45 a 52).
  • .dkc: prelude, PUBLIC_HEADER, CONTROL_CBOR, header_binding, time_only y time_and_key, flujo de cifrado y descifrado completo con SHOULD y MUST (secciones 20 a 39, 61 a 63).
  • .dkk: framing, body, identity portable de un solo uso (secciones 40 a 44).
  • Extensiones genéricas con reglas de duplicados y orden canónico (sección 54).
  • Catálogo de errores (sección 69). Límites de parser (57). Canonicidad CBOR y omisión de opcionales (58, 58.1).
  • Fixtures, mutaciones, fuzzing y CLI mínima.

No implementa, a propósito:

  • Servidor de Release API ni Release Queue. La librería define la interfaz de fuente de releases; el servidor actual podrá consumirla más adelante.
  • Almacenamiento, entrega de .dkk, servicios, extensiones concretas.
  • Cliente TypeScript. Vendrá después con la misma suite de fixtures.

3. Decisiones de diseño

3.1 Módulo y layout

Repositorio: Gitea propio, https://g.activething.com/go/DateKeys.git. Módulo: g.activething.com/go/DateKeys, la ruta que el propio Gitea anuncia en su etiqueta go-import. Si más adelante se quiere una ruta en datekeys.com que no dependa del servidor, es un cambio de una línea en go.mod y un sed en las importaciones, siempre antes de publicar. Go 1.26, con go.mod fijando la última versión de parche de la serie. No se usa GitHub para nada del proyecto; las dependencias alojadas allí se obtienen como módulos Go, igual que cualquier otra.

datekeys-go/
  go.mod
  LICENSE                      Apache-2.0 (código)
  README.md  SECURITY.md  CONTRIBUTING.md  TRADEMARKS.md  CHANGELOG.md
  spec/
    DateKeys_Protocol_Specification_v0.8.1.md   copia congelada
    datekeys.cddl                                schema normativo de todos los CBOR
  datekey/         DateKey, resolución, dk1_ canónico
  profile/         Provider Profile, CBOR canónico, hash, registro pinneado, quicknet.go
  provider/        interfaces Condition, Release, ReleaseSource, Verify
  provider/drand/  relays HTTP, verificación BLS, modo estricto
  codec/           CBOR determinista: Encode, Decode con comprobación por reencodificación, límites
  codec/bech32/    codificación Bech32 para la forma humana de identities (ver 4.3)
  agewrap/         identities y recipients envolventes de age: cardinalidad, sonda de inspección, stanza tlock
  extension/       tipos y reglas de extensiones
  capsule/         .dkc: framing, cabecera, control, binding, Encrypt, Inspect, Open
  accesskey/       .dkk: framing, body, identity
  errors.go        sentinel errors del catálogo de la sección 69
  internal/testkit/    generación de fixtures, escritor de cabeceras age malformadas, mutadores
  testdata/fixtures/   fixtures oficiales y sus valores esperados
  docs/traceability.md   mapa sección del spec → paquete, función, test
  cmd/datekeys/    CLI: encrypt, decrypt, inspect, datekey, profile

3.2 API pública, deliberadamente pequeña

// datekey
type DateKey struct { ProfileID string; Round uint64 }
func Resolve(p *profile.Profile, at time.Time) (DateKey, error)   // §15, precisión completa
func Parse(s string) (DateKey, error)                             // §19, solo forma canónica
func (d DateKey) Compact() string                                 // dk1_...
func (d DateKey) UnlockAt(p *profile.Profile) time.Time

// profile
type Profile struct { ID, Provider, Network string; ChainHash [32]byte; PublicKey []byte;
                      Period time.Duration; GenesisTime int64; Scheme string; GenesisSeed [32]byte }
func (p *Profile) CanonicalCBOR() []byte
func (p *Profile) Hash() [32]byte                                 // §11
var Quicknet *Profile                                             // §12, pinneado en el binario
type Registry interface { Lookup(id string) (*Profile, bool) }

// provider
type Condition struct { Round uint64 }
type Release struct { Round uint64; Signature []byte }
type ReleaseSource interface { Fetch(ctx context.Context, p *profile.Profile, c Condition) (Release, error) }
func Verify(p *profile.Profile, c Condition, r Release) error    // §51, BLS local siempre

// capsule
type Policy uint8                                                 // TimeOnly = 0, TimeAndKey = 1
type EncryptOptions struct {
    Profile        *profile.Profile
    UnlockAt       time.Time
    Policy         Policy
    Recipients     []age.Recipient        // X25519 de destinatarios conocidos
    NewPortableKey bool                   // genera I_ACCESS y devuelve la .dkk
    Critical, Noncritical []extension.Extension
}
type Result struct { DateKey datekey.DateKey; CapsuleID [16]byte; PortableKey *accesskey.AccessKey }
func Encrypt(dst io.Writer, src io.Reader, opts EncryptOptions) (*Result, error)

type Inspection struct { /* prelude, cabecera, política, ronda, unlock, resultado de cada comprobación */ }
func Inspect(r io.ReadSeeker, reg profile.Registry) (*Inspection, error)   // pasos 1 a 8, sin red ni secretos

type OpenOptions struct {
    Registry   profile.Registry
    Source     provider.ReleaseSource
    Identities []age.Identity          // X25519 propias del destinatario
    AccessKey  *accesskey.AccessKey    // .dkk portable
    Now        func() time.Time        // inyectable para tests
}
func Open(dst io.Writer, r io.ReadSeeker, opts OpenOptions) error         // pasos 9 a 18

// accesskey
type AccessKey struct { CredentialID, CapsuleID [16]byte; Type string; Material []byte;
                        Verification *Verification; Critical, Noncritical []extension.Extension }
func Encode(w io.Writer, k *AccessKey) error
func Decode(r io.Reader) (*AccessKey, error)
func (k *AccessKey) Identity() (age.Identity, error)

Reglas de la API:

  • io.Reader y io.Writer en todo; el payload nunca se carga entero en memoria.
  • Open recibe io.ReadSeeker para poder inspeccionar la cabecera de PAYLOAD_AGE en el offset 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN antes de pedir el release. Si el lector no permite seek, se omite el SHOULD y se aplica el MUST al abrir.
  • El reloj se inyecta. Ningún paquete llama a time.Now directamente salvo la CLI.
  • Ningún tipo imprime secretos en String(). Las identities se pasan como tipos de age, no como bytes sueltos, salvo en accesskey.

3.3 Cómo se usan age y tlock

  • PAYLOAD_AGE e INNER_ACCESS_AGE: age.Encrypt y age.Decrypt con recipients e identities X25519 de la API pública de age.
  • OUTER_TIME_AGE: age.Encrypt con un recipient propio en agewrap que llama a tlock.TimeLock y tlock.CiphertextToBytes, ambos exportados, y emite el stanza tlock <ronda> <chainhash> con el mismo formato que tlock. Para abrir, una identity propia que comprueba el conjunto completo de stanzas, la ronda y el chain hash contra la DateKey y el perfil, y llama a tlock.TimeUnlock, que verifica el beacon antes de descifrar.
    • Motivo: tlock.New(...).Decrypt usa una identity no exportada en la que no podemos imponer la cardinalidad ni evitar que todo error se convierta en ErrTooEarly. Usando su núcleo exportado se mantiene la compatibilidad de stanza con tle y se gana control del flujo.
  • Cardinalidad MUST: en las tres identities envolventes, dentro de Unwrap, que recibe todos los stanzas del fichero. Sin APIs internas.
  • Inspección SHOULD: age.ExtractHeader lee únicamente la cabecera del fichero, y age.DecryptHeader con una identity sonda, cuyo Unwrap registra los stanzas recibidos y devuelve un error centinela distinto de ErrIncorrectIdentity, entrega los stanzas parseados por age sin descifrar nada ni usar secretos. Ambas funciones son API pública de age 1.3. Para PAYLOAD_AGE se aplica en el offset del prelude. No hace falta parser propio.

4. Dependencias

Dependencia Versión Uso Nota
filippo.io/age v1.3.2 ficheros age, X25519, Stanza, Recipient, Identity única implementación de STREAM; no se reimplementa
github.com/drand/tlock v1.2.0 TimeLock, TimeUnlock, CiphertextToBytes, BytesToCiphertext no se usa tlock.New, Encrypt ni Decrypt
github.com/drand/drand/v2 v2.1.7 crypto.Scheme, VerifyBeacon, chain.Info arrastra gRPC y otros; govulncheck ya confirmó que no son alcanzables
github.com/drand/kyber y kyber-bls12381 transitivas pairing BLS12-381 sobre kilic/bls12-381, archivado; vigilar y documentar en SECURITY.md
github.com/fxamacker/cbor/v2 v2.9.4 CBOR determinista CoreDetEncOptions para codificar; límites en decodificación

Sin otras dependencias en la librería. Tests con la biblioteca estándar. CLI con flag. HTTP con net/http.

4.1 Política de versiones

go.mod pinneado, go.sum verificado, go mod verify en CI, govulncheck en cada PR, Renovate (compatible con Gitea) solo para parches, con revisión manual de cualquier cambio en age, tlock, drand o kyber.

4.2 Canonicidad CBOR

Codificar con Core Deterministic Encoding. Al decodificar, además de los límites de fxamacker (sin longitudes indefinidas, sin tags, sin claves duplicadas, profundidad y tamaños acotados), se reencodifica y se compara byte a byte con la entrada. Cualquier diferencia es ErrNonCanonicalCBOR. Es el mismo principio que dk1_ y no depende de que una librería prometa rechazar todas las formas no canónicas.

4.3 Bech32

age solo acepta identities y recipients X25519 en Bech32 por su API pública, y su paquete Bech32 es internal. El .dkk guarda 32 bytes crudos por spec. Se necesita un codificador Bech32 de unas cien líneas para pasar de bytes a AGE-SECRET-KEY-1... y de age1... a bytes. Es codificación, no criptografía. Opciones: copiar el paquete interno de age con su licencia BSD y atribución, o reimplementar BIP-173 con vectores de prueba. Propuesta: copiar, para no divergir.


5. Qué se copia del prototipo

Origen Destino Cambios
pkg/datekeys/key.go: RoundForTime, TimeForRound, ParseDateKey, FromRound datekey/ quitar campos de API y el alias EncryptionKey; el struct de respuesta HTTP se queda en el servidor
pkg/datekeys/key_test.go, FuzzRecipient datekey/ renombrar, ampliar con vectores de la sección 65
internal/quicknet/trust.go profile/quicknet.go añadir CBOR canónico y hash; conservar la autocomprobación de chain hash
internal/drand/client.go y tests provider/drand/ carrera entre relays, respuestas malformadas, cancelación; adaptar a ReleaseSource
pkg/datekeys/crypto.go no se copia sustituido por agewrap sobre el núcleo exportado de tlock; se conserva la doble verificación BLS
tests/integration/quicknet_test.go provider/drand/, build tag integration prueba en vivo contra Quicknet
cmd/datekeys reescrito misma ergonomía, nueva API
internal/api, web-client no se tocan consumirán la librería más adelante

6. Estilo de código

  • Identificadores, comentarios de código, mensajes de error y commits en inglés. Documentación de usuario en inglés con versión en español. Motivo: la revisión externa y la comunidad de age y drand.
  • gofmt, goimports, go vet, staticcheck, golangci-lint con configuración corta: errcheck, govet, staticcheck, ineffassign, unparam, gosec en modo aviso.
  • Errores: un sentinel por entrada del catálogo de la sección 69, envueltos con %w y contexto; nunca se sustituye un error por otro más cómodo. Los tests de mutación comprueban errors.Is.
  • Parsers: límites antes de reservar memoria, io.LimitReader, ningún panic alcanzable desde la entrada. Todo parser tiene un objetivo de fuzzing.
  • Sin estado global mutable. El registro de perfiles se pasa explícitamente; el registro por defecto contiene solo Quicknet.
  • Comentarios de paquete y de función citan la sección del spec que implementan, por ejemplo // Spec §26.
  • Secretos: no se registran en logs, no aparecen en String(), se borran de buffers propios al terminar, sin prometer más de lo que Go garantiza.
  • Salida atómica en la CLI: fichero temporal en el mismo directorio y renombrado al final; nunca sobrescribir destinos existentes.
  • Versionado semántico. v0.x hasta que el spec sea v1.0. Sin promesa de estabilidad de API antes de v1.0.0.

7. Estrategia de tests

  1. Unitarios por paquete, tabla-driven, biblioteca estándar.
  2. Vectores de spec, generados por el código y guardados como golden files versionados: ronda (frontera, un segundo antes y después, fracción de segundo tras frontera, 2030-01-01, cercanos al genesis), dk1_ (objeto, JSON canónico, base64url, cadena), profile_hash de Quicknet.
  3. Fixtures .dkc y .dkk (secciones 67 y 68): generados una vez con internal/testkit sobre una ronda ya publicada, con la firma BLS embebida en el fixture. Los tests verifican la firma contra la clave pinneada y descifran sin red. Se comparan todos los valores intermedios: cabecera, prelude, binding, control, I_PAYLOAD, plaintext y el error esperado en cada etapa.
  4. Corpus de mutaciones (sección 64): las veinte mutaciones como funciones sobre un fixture válido; cada una afirma su sentinel error concreto.
  5. Escritor de cabeceras age malformadas en testkit: añade stanzas o cambia tipos manteniendo el MAC válido, porque en el test se conoce la file key. Demuestra que la comprobación estructural rechaza lo que el MAC aceptaría. Es la prueba directa de la amenaza del creador. Implementación: según la especificación C2SP, el MAC de cabecera es HMAC-SHA-256 con clave HKDF-SHA-256 de la file key e info header, sobre el texto de la cabecera; testkit lo recalcula con golang.org/x/crypto/hkdf, ya presente en el grafo por age. Para abrir ficheros de test con una file key conocida se usa age.NewInjectedFileKeyIdentity, también API pública.
  6. Propiedades: Encode(Decode(x)) == x para todo fixture; toda reordenación de claves o ensanchamiento de enteros se rechaza como no canónico; Parse(Compact(d)) == d.
  7. Fuzzing nativo de Go: prelude, PUBLIC_HEADER, CONTROL_CBOR, .dkk, dk1_, cabecera age vía sonda. Corpus sembrado con fixtures y mutaciones. Presupuesto corto en cada PR, largo en nightly.
  8. Integración en vivo con build tag: cifrar hacia ahora más treinta segundos, esperar, abrir por relays reales. Nightly, no en cada PR.
  9. Interoperabilidad con herramientas ajenas: extraer SEALED_CONTROL de un fixture time_only y abrirlo con la CLI tle oficial; extraer PAYLOAD_AGE y abrirlo con la CLI age usando I_PAYLOAD. Si ambas pasan, la afirmación "son ficheros age estándar" queda demostrada por terceros.
  10. Cobertura: mínimo 90 % en codec, capsule, accesskey, datekey, agewrap; -race siempre.
  11. CDDL: spec/datekeys.cddl valida los fixtures en un job opcional de CI con la herramienta cddl; si no se quiere otra toolchain, se mantiene como documento normativo y se comprueba a mano en cada cambio de schema.

8. CI, releases y cadena de suministro

  • Gitea Actions, con la misma sintaxis que GitHub Actions: test en Linux y Windows con la versión estable de Go y la anterior, macOS cuando haya runner; lint; vuln con govulncheck; fuzz-short; integration nightly por cron; sbom con cyclonedx-gomod.
  • Releases con goreleaser, que publica en Gitea: -trimpath, CGO_ENABLED=0, checksums publicados, firma con cosign con clave propia, ya que la firma sin clave depende de un proveedor OIDC que Gitea no ofrece.
  • SECURITY.md con canal de reporte, versiones soportadas y la dependencia de kilic/bls12-381 declarada como riesgo conocido y vigilado.
  • TRADEMARKS.md: "DateKeys" reservado; "implements the DateKey protocol" permitido.

9. Hitos y criterios de aceptación

M0. Cimientos. Esqueleto, go.mod pinneado, CI con lint y govulncheck, codec con canonicidad por reencodificación y límites, spec/datekeys.cddl inicial, profile con Quicknet, su CBOR y su hash como primer vector oficial. Hecho cuando: el hash del perfil está congelado en un golden file y CI está verde en tres sistemas.

M1. Objetos. datekey copiado y ampliado; framing DKC1 y DKK1; codecs de PUBLIC_HEADER, CONTROL_CBOR y .dkk; header_binding; extension; catálogo de errores; fuzzing de todos los parsers. Hecho cuando: los vectores de ronda y dk1_ están generados y versionados y el fuzzing lleva una noche sin fallos.

M2. time_only. agewrap con recipient e identity tlock estrictos, sonda de inspección, PAYLOAD_AGE, Encrypt, Inspect, Open con los 18 pasos; provider/drand copiado. Hecho cuando: un fixture time_only sobre ronda pasada se abre offline, la prueba con tle y age externas pasa, y las mutaciones aplicables fallan con su error exacto.

M3. time_and_key. INNER_ACCESS_AGE con varios recipients, .dkk portable de un solo uso, Identities y AccessKey en Open. Hecho cuando: fixtures time_and_key con uno y varios recipients se abren offline, la reutilización de I_ACCESS está prohibida en Encrypt y probada, y las veinte mutaciones están cubiertas.

M4. Conformidad. Suite de fixtures oficial, golden files, docs/traceability.md completo, integración nightly, SBOM y release firmada v0.1.0. Hecho cuando: cada sección normativa de la 14 a la 69 apunta a al menos un test, y v0.1.0 se construye de forma reproducible.

M5. CLI. datekeys encrypt, decrypt, inspect, datekey resolve, profile hash. inspect ejecuta solo los pasos 1 a 8 y nunca pide un release ni usa secretos. Hecho cuando: la CLI abre los fixtures y reproduce la ergonomía del prototipo con salida atómica.

Fuera de estos hitos: SDK TypeScript, migración del cliente web, servidor de Release API sobre la librería, segundo perfil evmnet.


10. Trazabilidad

docs/traceability.md mantiene una tabla sección → paquete → función → test. Se actualiza en el mismo PR que cualquier cambio de código normativo. Es el documento que se entrega al revisor externo junto con el spec, los fixtures y el corpus de mutaciones.


11. Decisiones a revisar

  1. Decidida: ruta g.activething.com/go/DateKeys. Queda abierto si se pasa a una ruta en datekeys.com antes de publicar. Nota: el servidor ejecuta Gitea 1.18.3, sin Actions; los workflows de .gitea/workflows quedan listos para Gitea 1.21 o superior con un act_runner, y hasta entonces scripts/check.sh es el control obligatorio antes de cada push.
  2. Inglés para código, errores y commits.
  3. Licencia Apache-2.0 para el código, CC-BY-4.0 para el spec.
  4. Copiar el Bech32 interno de age frente a reimplementarlo.
  5. Depender del módulo drand/v2 completo o extraer solo la verificación BLS a un paquete propio sobre kyber-bls12381. La primera opción es más simple; la segunda reduce el grafo y el ruido de govulncheck.
  6. Incluir en la librería un cliente de la Release API de DateKeys además del de relays drand, o dejarlo para el servidor.
  7. Validación CDDL en CI con toolchain externa, o solo como documento.
  8. Si Open exige io.ReadSeeker o acepta io.Reader y degrada el SHOULD.

12. Riesgos conocidos

  • kilic/bls12-381 archivado bajo drand y tlock. Mitigación: pinneado, vigilancia, plan de fork o de migración documentado en SECURITY.md.
  • tlock sin etiqueta desde agosto de 2024. Mitigación: pinneado por versión; solo se usa su núcleo exportado.
  • Fixtures sobre rondas pasadas dependen de que la firma embebida sea auténtica. Mitigación: se verifica BLS en cada ejecución contra la clave pinneada.
  • Desviación spec ↔ código. Mitigación: trazabilidad, política de cambios de la sección 76, y una segunda implementación en TypeScript como prueba de interoperabilidad.

Powered by TurnKey Linux.