diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e21470..8a9a3ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,25 @@ semantic versioning; `v0.x` versions make no API stability promise. The draft v0.15 of the DateKeys Protocol Specification, on the branch `v0.15` and not approved yet: the long-term recovery of capsules, with the -eight decisions its author took on 6 October 2026. It changes no format of +eight decisions its author took on 6 October 2026 and the correction of +7 October, which removed the `.dkr` file. It changes no format of `.dkc` or `.dkk`, and one verdict: a valid release in the caller's hand opens a capsule even when the clock is before the round time. `SpecVersion` stays 0.14 until the author approves the draft. - **Specification.** `spec/DateKeys_Protocol_Specification_v0.15.md`, whose §76 lists six changes with their cases: the release object (§47.1, with - §20, §45, §47 and §57), its chain hash at step 10 (§63, §69.1), step 9.c + §45, §47 and §57), its chain hash at step 10 (§63, §69.1), step 9.c only before a network request (§49, §63, §70), the long-term recovery - (§50, §53, §62.1 rules 26 to 28, and the informative annex §79), the + (§50, on archives and cache services that keep the releases of all + rounds; §53; §62.1 rules 26 and 27; and the informative annex §79), the Release API (§45) and §74. `datekeys.cddl` adds the rule `release`. +- **No `.dkr` file.** The first draft kept the release of a capsule in a + `.dkr` file next to it; the author removed it on 7 October 2026: the + release does not exist when the capsule is made, and once the date comes + the capsule can be opened, so a saved release only opens it again and does + not help whoever opens it decades later. Gone with it: the extension, rule + 28 of §62.1, `decrypt -save-release` and the command `datekeys release`. - **Release object.** `provider.EncodeRelease`, `DecodeRelease` and `ParseRelease`, which also reads drand's JSON; `provider.Verify` checks the chain hash a release names, `ERR_PROFILE_MISMATCH`, before its round and its @@ -29,12 +37,10 @@ stays 0.14 until the author approves the draft. the round time, and `Opened.Release` carries the chain hash, ready for `provider.EncodeRelease`. A network source is still never asked before the round time. -- **CLI.** `datekeys decrypt -release FILE` takes a `.dkr`, drand's JSON or a - local archive, with no network request; `-save-release FILE.dkr` keeps the - release that opened the capsule; `datekeys release -in FILE.dkc -out - FILE.dkr` fetches, verifies and saves it without opening the capsule. +- **CLI.** `datekeys decrypt -release FILE` takes a release object, + drand's JSON or a local archive, with no network request. - **Test data.** `vectors/release.json`, new, from - `internal/testkit.ReleaseVectors`; `releases/.dkr` for rounds 1000, + `internal/testkit.ReleaseVectors`; `releases/.cbor` for rounds 1000, 1001, 1004 and 2000, and `releases/archive_1000_1004.bin`. In `mutations.json` every case has the field `source`, `supplied` or `network`; "round not reached yet", a release in hand with a clock 1 ns diff --git a/README.es.md b/README.es.md index 3205fb5..c7f35a8 100644 --- a/README.es.md +++ b/README.es.md @@ -5,9 +5,10 @@ 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. Esta rama, `v0.15`, lleva además el borrador v0.15 ([`spec/`](spec/DateKeys_Protocol_Specification_v0.15.md)), aún sin aprobar, -sobre la recuperación de cápsulas a largo plazo: el release de una ronda -guardado en un fichero `.dkr`, un release en la mano que el reloj no detiene, -y un anexo para abrir una cápsula sin software de DateKeys. +sobre la recuperación de cápsulas a largo plazo: el objeto release, un +release en la mano que el reloj no detiene, archivos y servicios de caché que +guardan los releases de todas las rondas, y un anexo para abrir una cápsula +sin software de DateKeys. [English version](README.md). DateKeys cifra datos de forma que solo puedan abrirse a partir de un instante @@ -133,9 +134,7 @@ datekeys encrypt -at 2030-01-01T00:00:00Z -in carta.txt -note "Cartas de Lisboa" 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 decrypt -in regalo.dkc -out regalo -dkk regalo.dkk -save-release regalo.dkr -datekeys decrypt -in regalo.dkc -out regalo -dkk regalo.dkk -release regalo.dkr -datekeys release -in regalo.dkc -out regalo.dkr +datekeys decrypt -in regalo.dkc -out regalo -dkk regalo.dkk -release ronda.cbor datekeys profile hash datekeys version ``` @@ -152,11 +151,9 @@ reforzado, o con bloque256 si se pide (spec §29.1). 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, nunca antes de la hora de la ronda; con `-release` toma en su -lugar el release en la mano, un `.dkr`, el JSON de drand o un archivo local -de releases, sin ninguna petición y diga lo que diga el reloj (borrador v0.15, -§47.1, §63 paso 9.c), y `-save-release` guarda como `.dkr` el release que -abrió la cápsula. `release` obtiene, verifica y guarda ese `.dkr` sin abrir -la cápsula. Los ficheros de una cápsula de formato 3 van a la carpeta nueva +lugar el release en la mano, un objeto release, el JSON de drand o un +archivo local de releases, venga de donde venga, sin ninguna petición y diga +lo que diga el reloj (borrador v0.15, §47.1, §63 paso 9.c). 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 @@ -276,9 +273,10 @@ normativos del §69, así que `errors.Is` y `datekeys.Code(err)` lo identifican. 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). - El borrador v0.15 lo guarda como `.dkr` junto a la cápsula, y su §79 dice - cómo abrir una cápsula sin software de DateKeys, lo que comprueba - `scripts/recovery_check.sh`. + El borrador v0.15 la basa en archivos y servicios de caché, de DateKeys o + de otros, que guardan los releases de todas las rondas, sin prometer + ningún alojamiento, y su §79 dice cómo abrir una cápsula sin software de + DateKeys, lo que comprueba `scripts/recovery_check.sh`. Ver [SECURITY.md](SECURITY.md). diff --git a/README.md b/README.md index 7cf9913..9c21fee 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ v0.14** ([`spec/`](spec/DateKeys_Protocol_Specification_v0.14.md)), tagged `spec-v0.14`, which changes no format of v0.11, v0.12 or v0.13. This branch, `v0.15`, also carries the draft v0.15 ([`spec/`](spec/DateKeys_Protocol_Specification_v0.15.md)), not approved yet, -on the long-term recovery of capsules: the release of a round saved as a -`.dkr` file, a release in hand that the clock does not stop, and an annex to -open a capsule without DateKeys software. +on the long-term recovery of capsules: the release object, a release in +hand that the clock does not stop, archives and cache services that keep the +releases of all rounds, and an annex to open a capsule without DateKeys +software. [Versión en español](README.es.md). DateKeys encrypts data so that it can only be opened after a chosen instant. @@ -133,9 +134,7 @@ datekeys encrypt -at 2030-01-01T00:00:00Z -in letter.txt -note "Letters from Lis datekeys decrypt -in letter.dkc -out letter -expect-author dkauthor1... datekeys inspect -in gift.dkc datekeys decrypt -in gift.dkc -out gift -dkk gift.dkk -datekeys decrypt -in gift.dkc -out gift -dkk gift.dkk -save-release gift.dkr -datekeys decrypt -in gift.dkc -out gift -dkk gift.dkk -release gift.dkr -datekeys release -in gift.dkc -out gift.dkr +datekeys decrypt -in gift.dkc -out gift -dkk gift.dkk -release round.cbor datekeys profile hash datekeys version ``` @@ -152,11 +151,9 @@ asked (spec §29.1). requests a release and never uses a secret. `decrypt` fetches the release from public drand relays and verifies it locally, never before the round time; with `-release` it takes the release -in hand instead, a `.dkr`, drand's JSON or a local release archive, without -any request and whatever the clock says (draft v0.15, §47.1, §63 step 9.c), -and `-save-release` keeps the release that opened the capsule as a `.dkr`. -`release` fetches, verifies and saves that `.dkr` without opening the -capsule. The files of a format 3 capsule go to the new folder `-out`, staged +in hand instead, a release object, drand's JSON or a local release +archive, from any source, without any request and whatever the clock says +(draft v0.15, §47.1, §63 step 9.c). The files of a format 3 capsule go to the new folder `-out`, staged inside it and moved into place only after every check passed (spec §56); a capsule with only a comment creates no folder. It shows the verdicts first and last, and the declared author, the comment and the paths as text of the @@ -273,9 +270,10 @@ one of the 19 normative errors of spec §69, so `errors.Is` and access barrier, not a signature. - **Recovery years later** needs the historical release: from a drand relay that still serves it or from any cache, re-verified locally (spec §50). - The draft v0.15 keeps it as a `.dkr` next to the capsule, and its §79 - says how to open a capsule without DateKeys software, which - `scripts/recovery_check.sh` checks. + The draft v0.15 rests it on archives and cache services, of DateKeys or + others, that keep the releases of all rounds, with no promise of hosting + one, and its §79 says how to open a capsule without DateKeys software, + which `scripts/recovery_check.sh` checks. See [SECURITY.md](SECURITY.md). diff --git a/capsule/inhand_test.go b/capsule/inhand_test.go index aa9756c..524e972 100644 --- a/capsule/inhand_test.go +++ b/capsule/inhand_test.go @@ -35,10 +35,11 @@ func readRelease(t *testing.T, round uint64) []byte { // Spec v0.15, §63 step 9.c: a release in hand is not compared with the // clock. The capsule opens with a clock before the round time, Opened says -// the clock is behind, and Opened.Release encodes to the official .dkr. +// the clock is behind, and Opened.Release encodes to the official release +// object of the round. func TestReleaseInHand(t *testing.T) { f := loadFixture(t, "format3_time_and_key_portable") - dkr := readRelease(t, 1000) + obj := readRelease(t, 1000) for _, tc := range []struct { name string now time.Time @@ -49,7 +50,7 @@ func TestReleaseInHand(t *testing.T) { {"a clock years behind", testkit.Genesis(), true}, } { o := f.openOptions(t) - o.Source, o.Release, o.Now = nil, provider.Encoded(dkr), testkit.Fixed(tc.now) + o.Source, o.Release, o.Now = nil, provider.Encoded(obj), testkit.Fixed(tc.now) out, err := capsule.Open(context.Background(), nil, bytes.NewReader(f.dkc), o) if err != nil { t.Fatalf("%s: %v", tc.name, err) @@ -58,8 +59,8 @@ func TestReleaseInHand(t *testing.T) { t.Fatalf("%s: ClockBehind %v", tc.name, out.ClockBehind) } saved, err := provider.EncodeRelease(out.Release) - if err != nil || !bytes.Equal(saved, dkr) { - t.Fatalf("%s: the release saved again is %x, %v", tc.name, saved, err) + if err != nil || !bytes.Equal(saved, obj) { + t.Fatalf("%s: the release encodes to %x, %v", tc.name, saved, err) } } } diff --git a/capsule/open.go b/capsule/open.go index 52acb68..a737e8e 100644 --- a/capsule/open.go +++ b/capsule/open.go @@ -38,7 +38,7 @@ type OpenOptions struct { // kept, not its code. Source provider.ReleaseSource // Release is a release the caller has in hand, the alternative to - // Source (spec v0.15, §63 step 9.c): a .dkr file or drand's JSON read + // Source (spec v0.15, §63 step 9.c): a release object or drand's JSON read // with provider.Encoded, or a local archive with provider.Archive. It // makes no network request, so Open asks it for the release without // comparing Now with the round time, and reports in @@ -87,8 +87,7 @@ type Opened struct { Inspection *Inspection // Release is the release that opened the capsule, verified at step 10, // with the chain hash of the pinned profile: provider.EncodeRelease - // gives the .dkr that a reader saves next to the capsule (spec v0.15, - // §62.1). + // gives its release object (spec v0.15, §47.1). Release provider.Release // ClockBehind reports that the release was in the caller's hand and that // Now was before the round time of the DateKey: the release proves the diff --git a/cmd/datekeys/main.go b/cmd/datekeys/main.go index 8bca962..18dc86b 100644 --- a/cmd/datekeys/main.go +++ b/cmd/datekeys/main.go @@ -7,8 +7,7 @@ // datekeys author keygen -out autor.key -pass-file clave.txt // datekeys encrypt -at 2030-01-01T00:00:00Z -in carta.txt -sign autor.key -sign-pass-file clave.txt -out carta.dkc // datekeys decrypt -in regalo.dkc -out regalo [-dkk key.dkk] [-identity key.txt] [-words-file palabras.txt] -// datekeys decrypt -in regalo.dkc -out regalo -release regalo.dkr -// datekeys release -in regalo.dkc -out regalo.dkr +// datekeys decrypt -in regalo.dkc -out regalo -release ronda.cbor // datekeys datekey resolve -at 2030-01-01T00:00:00Z // datekeys profile hash // datekeys version @@ -53,8 +52,7 @@ import ( const usage = `usage: datekeys encrypt -at TIME -in FILE|FOLDER... -out FILE.dkc [-comment TEXT] [-author TEXT] [-no-mtime] [-policy time_only|time_and_key] [-recipient age1...]... [-dkk FILE.dkk] [-words TEXT|-words-file FILE] [-padding reforzado|bloque256] [-note TEXT] [-sign KEY [-sign-pass-file FILE]] [-large-area] - datekeys decrypt -in FILE.dkc -out PATH [-dkk FILE.dkk] [-identity FILE]... [-words TEXT|-words-file FILE] [-expect-author dkauthor1...] [-relay URL]... [-release FILE] [-save-release FILE.dkr] - datekeys release -in FILE.dkc -out FILE.dkr [-relay URL]... + datekeys decrypt -in FILE.dkc -out PATH [-dkk FILE.dkk] [-identity FILE]... [-words TEXT|-words-file FILE] [-expect-author dkauthor1...] [-relay URL]... [-release FILE] datekeys author keygen -out FILE (-pass-file FILE|-plain) datekeys author public -key FILE [-pass-file FILE] datekeys inspect -in FILE.dkc [-json] @@ -72,13 +70,10 @@ new folder PATH, and the content of formats 1 and 2 to the new file PATH. decrypt fetches the release of the round from drand relays, never before the round time. -release FILE gives it instead, without any network request: a -.dkr file, drand's JSON or a local release archive. It is verified like one -from a relay, and the clock does not stop it: a valid release proves that the -round was published (spec v0.15, §63 step 9.c). -save-release keeps the -release that opened the capsule in a new .dkr file, to keep next to the .dkc: -in decades the relays may no longer serve the round. release fetches, -verifies and saves that .dkr without opening the capsule; the request tells -the relay the round, as opening does. +release object, drand's JSON or a local release archive. It is verified like +one from a relay, against the pinned key, so it does not matter who served +it; and the clock does not stop it: a valid release proves that the round +was published (spec v0.15, §63 step 9.c). -words and -words-file give a key of words to a time_and_key capsule: at least 6 different words of 3 or more letters that open it with decrypt, @@ -138,8 +133,6 @@ func run(args []string, stdout, stderr io.Writer, now func() time.Time) error { return encrypt(args[1:], stderr, now) case "decrypt": return decrypt(args[1:], stdout, stderr, now) - case "release": - return saveRelease(args[1:], stderr, now) case "author": return author(args[1:], stdout, stderr, stdin) case "inspect": @@ -328,8 +321,7 @@ func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) erro words := fs.String("words", "", "the words of a key of words; they stay in the shell history") wordsFile := fs.String("words-file", "", "file with the words of a key of words") expect := fs.String("expect-author", "", "fail unless the capsule is signed with this public key, dkauthor1...") - release := fs.String("release", "", "the release in hand: a .dkr, drand's JSON or a local release archive; no network request") - keep := fs.String("save-release", "", "new .dkr file for the release that opened the capsule; never overwritten") + release := fs.String("release", "", "the release in hand: a release object, drand's JSON or a local release archive; no network request") if err := parse(fs, args); err != nil { return err } @@ -339,11 +331,6 @@ func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) erro if *release != "" && len(relays) > 0 { return errors.New("decrypt: -release and -relay are exclusive") } - if *keep != "" { - if err := checkNew(*keep); err != nil { - return err - } - } var expected []byte if *expect != "" { k, err := authorkey.ParsePublic(*expect) @@ -464,12 +451,6 @@ func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) erro fmt.Fprintf(stderr, "warning: the release proves that round %d was published at %s, and this clock says %s: it may be behind\n", opened.Release.Round, opened.Inspection.UnlockAt.Format(time.RFC3339), now().UTC().Format(time.RFC3339)) } - if *keep != "" { - if err := writeRelease(*keep, opened.Release); err != nil { - return err - } - fmt.Fprintf(stderr, " release saved to %s: keep it next to the capsule\n", *keep) - } if opened.Format == capsule.Format3 { fmt.Fprintf(stderr, " format 3, %d files\n", len(opened.Head.Files)) if len(opened.Head.Files) == 0 { @@ -492,7 +473,7 @@ func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) erro // releaseInHand reads the release that the person has in hand, from path: a // local release archive, which is read when Open asks for the round, or a -// .dkr file or drand's JSON, which Open decodes at step 10. A file larger +// release object or drand's JSON, which Open decodes at step 10. A file larger // than any of them is cut where it can no longer be valid, so that step 10 // rejects it with the code of its size. func releaseInHand(path string) (provider.Supplier, io.Closer, error) { @@ -521,76 +502,6 @@ func releaseInHand(path string) (provider.Supplier, io.Closer, error) { return provider.Encoded(append(head, rest...)), io.NopCloser(nil), nil } -// writeRelease writes the release object of r, a verified release, to the -// new file path (spec v0.15, §47.1). -func writeRelease(path string, r provider.Release) error { - b, err := provider.EncodeRelease(r) - if err != nil { - return err - } - return writeAtomic(path, func(w io.Writer) error { - _, err := w.Write(b) - return err - }) -} - -// saveRelease fetches the release of the round of a capsule from drand -// relays, verifies it and saves it as a new .dkr file, without opening the -// capsule (spec v0.15, §62.1). Steps 1 to 8 come first, and no request is -// made before the round time. -func saveRelease(args []string, stderr io.Writer, now func() time.Time) error { - fs := newFlags("release") - in := fs.String("in", "", ".dkc file") - out := fs.String("out", "", "new .dkr file; never overwritten") - timeout := fs.Duration("timeout", 30*time.Second, "release request timeout") - var relays multi - fs.Var(&relays, "relay", "drand relay base URL (repeatable); default: public relays") - if err := parse(fs, args); err != nil { - return err - } - if *in == "" || *out == "" { - return errors.New("release: -in and -out are required") - } - if err := checkNew(*out); err != nil { - return err - } - reg, err := profile.Default() - if err != nil { - return err - } - src, err := os.Open(*in) - if err != nil { - return err - } - defer src.Close() - insp, err := capsule.Inspect(src, capsule.InspectOptions{Registry: reg}) - if err != nil { - return err - } - cond := provider.Condition{Round: insp.Header.DateKey.Round} - if t := now(); t.Before(insp.UnlockAt) { - return fmt.Errorf("release: round %d is published at %s, it is %s: %w", cond.Round, - insp.UnlockAt.Format(time.RFC3339), t.UTC().Format(time.RFC3339), datekeys.ErrReleaseUnavailable) - } - ctx, cancel := context.WithTimeout(context.Background(), *timeout) - defer cancel() - // The relays verify each answer and discard the invalid ones; it is - // verified again here, as Open does at step 10. - r, err := drand.New(relays...).Fetch(ctx, insp.Profile, cond) - if err != nil { - return err - } - if err := provider.Verify(insp.Profile, cond, r); err != nil { - return err - } - r.ChainHash = insp.Profile.ChainHash[:] - if err := writeRelease(*out, r); err != nil { - return err - } - fmt.Fprintf(stderr, "Saved the release of round %d, verified locally, to %s: keep it next to %s\n", cond.Round, *out, *in) - return nil -} - // wordsText is the text of the words of -words or of -words-file, at most // 4 KiB, or "" when neither is given. func wordsText(cmd, words, file string) (string, error) { diff --git a/cmd/datekeys/release_test.go b/cmd/datekeys/release_test.go index 31d9212..c2fbf48 100644 --- a/cmd/datekeys/release_test.go +++ b/cmd/datekeys/release_test.go @@ -19,10 +19,12 @@ const releases = "../../testdata/releases" // unlock1000 is the round time of round 1000, that of time_only.dkc. var unlock1000 = time.Date(2023, 8, 23, 15, 59, 24, 0, time.UTC) +// releaseFile is the release object of round in testdata/releases. +func releaseFile(round uint64) string { return filepath.Join(releases, testkit.ReleaseFileName(round)) } + // Spec v0.15, §63 step 9.c: a release in hand opens the capsule without any // network request, even with a clock behind the round time, which decrypt -// only warns about; -save-release keeps it as a .dkr, the same bytes as the -// official one. +// only warns about. func TestDecryptWithReleaseInHand(t *testing.T) { dir := t.TempDir() want, err := os.ReadFile(filepath.Join(fixtures, "time_only.plaintext")) @@ -33,19 +35,18 @@ func TestDecryptWithReleaseInHand(t *testing.T) { if err := os.WriteFile(jsonFile, []byte(`{"round":1000,"signature":"`+hex.EncodeToString(testkit.Release(1000).Signature)+`"}`), 0o644); err != nil { t.Fatal(err) } - for i, c := range []struct { + for _, c := range []struct { name, release string now time.Time }{ - {".dkr", filepath.Join(releases, "1000.dkr"), later}, - {".dkr and a clock behind", filepath.Join(releases, "1000.dkr"), unlock1000.Add(-time.Hour)}, + {"a release object", releaseFile(1000), later}, + {"a release object and a clock behind", releaseFile(1000), unlock1000.Add(-time.Hour)}, {"drand's JSON", jsonFile, later}, {"a local archive", filepath.Join(releases, testkit.ArchiveFile), unlock1000.Add(-time.Nanosecond)}, } { t.Run(c.name, func(t *testing.T) { out := filepath.Join(dir, c.name) - saved := filepath.Join(dir, "saved"+string(rune('a'+i))+".dkr") - _, stderr, err := cli(t, c.now, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", out, "-release", c.release, "-save-release", saved) + _, stderr, err := cli(t, c.now, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", out, "-release", c.release) if err != nil { t.Fatal(err) } @@ -55,10 +56,6 @@ func TestDecryptWithReleaseInHand(t *testing.T) { if behind := strings.Contains(stderr, "may be behind"); behind != c.now.Before(unlock1000) { t.Fatalf("warning of a clock behind: %v, in %q", behind, stderr) } - official, _ := os.ReadFile(filepath.Join(releases, "1000.dkr")) - if got, _ := os.ReadFile(saved); !bytes.Equal(got, official) { - t.Fatalf("saved .dkr %x, want %x", got, official) - } }) } } @@ -74,8 +71,8 @@ func TestDecryptWithBadReleaseInHand(t *testing.T) { } return p } - r1001, _ := os.ReadFile(filepath.Join(releases, "1001.dkr")) - r1000, _ := os.ReadFile(filepath.Join(releases, "1000.dkr")) + r1001, _ := os.ReadFile(releaseFile(1001)) + r1000, _ := os.ReadFile(releaseFile(1000)) v2 := bytes.Clone(r1000) v2[bytes.Index(v2, []byte{0x01, 0x01})+1] = 2 for _, c := range []struct { @@ -84,10 +81,10 @@ func TestDecryptWithBadReleaseInHand(t *testing.T) { capsule string want *datekeys.Error }{ - {"another round", write("1001.dkr", r1001), "time_only", datekeys.ErrRoundMismatch}, - {"version 2", write("v2.dkr", v2), "time_only", datekeys.ErrUnsupportedVersion}, - {"a byte after it", write("long.dkr", append(bytes.Clone(r1000), 0)), "time_only", datekeys.ErrNonCanonicalCBOR}, - {"a large file", write("large.dkr", append(bytes.Clone(r1000), make([]byte, 20000)...)), "time_only", datekeys.ErrNonCanonicalCBOR}, + {"another round", write("1001.cbor", r1001), "time_only", datekeys.ErrRoundMismatch}, + {"version 2", write("v2.cbor", v2), "time_only", datekeys.ErrUnsupportedVersion}, + {"a byte after it", write("long.cbor", append(bytes.Clone(r1000), 0)), "time_only", datekeys.ErrNonCanonicalCBOR}, + {"a large file", write("large.cbor", append(bytes.Clone(r1000), make([]byte, 20000)...)), "time_only", datekeys.ErrNonCanonicalCBOR}, {"bad JSON", write("bad.json", []byte(`{"round":1000}`)), "time_only", datekeys.ErrReleaseInvalid}, {"a round the archive lacks", filepath.Join(releases, testkit.ArchiveFile), "format2_time_and_key_sixteen", datekeys.ErrReleaseUnavailable}, } { @@ -109,7 +106,7 @@ func TestDecryptWithBadReleaseInHand(t *testing.T) { } }) } - if _, _, err := cli(t, later, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", filepath.Join(dir, "x"), "-release", "a.dkr", "-relay", "http://127.0.0.1:1"); err == nil { + if _, _, err := cli(t, later, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", filepath.Join(dir, "x"), "-release", "a.cbor", "-relay", "http://127.0.0.1:1"); err == nil { t.Fatal("-release and -relay together") } } @@ -121,25 +118,3 @@ func TestDecryptFromRelayBeforeTheRoundTime(t *testing.T) { t.Fatalf("got %v", err) } } - -// datekeys release fetches, verifies and saves the .dkr of a capsule -// without opening it, and never before the round time. -func TestReleaseCommand(t *testing.T) { - dir := t.TempDir() - url := relay(t) - out := filepath.Join(dir, "carta.dkr") - if _, _, err := cli(t, later, "release", "-in", filepath.Join(fixtures, "format3_time_and_key_portable.dkc"), "-out", out, "-relay", url); err != nil { - t.Fatal(err) - } - official, _ := os.ReadFile(filepath.Join(releases, "1000.dkr")) - if got, _ := os.ReadFile(out); !bytes.Equal(got, official) { - t.Fatalf("saved .dkr %x, want %x", got, official) - } - if _, _, err := cli(t, later, "release", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", out, "-relay", url); err == nil { - t.Fatal("an existing .dkr was overwritten") - } - _, _, err := cli(t, unlock1000.Add(-time.Second), "release", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", filepath.Join(dir, "early.dkr"), "-relay", "http://127.0.0.1:1") - if !errors.Is(err, datekeys.ErrReleaseUnavailable) { - t.Fatalf("before the round time: %v", err) - } -} diff --git a/docs/traceability.md b/docs/traceability.md index a0f0c0d..ab2ba5c 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -33,7 +33,7 @@ number their sections the same, but for §7.10, new in v0.14, and §47.1 and | 17 | Past-round attack; at step 10 the release round is compared before the signature, `ERR_ROUND_MISMATCH` for a release supplied directly, a network source discarding one of another round at step 9 (`ERR_RELEASE_UNAVAILABLE`) | `provider.Verify` (round equality first), `capsule.Encrypt` (round time ≥ requested), `agewrap.CheckTimeStanzas`, `provider/drand.Client` | `provider.TestVerifyRejects` (*another round and a short signature*); `capsule.TestReleaseFromANetworkSource`; mutations *DateKey A + release of round B*, *tlock stanza round differs from DateKey.round* | | 18 | `dk1_` representation; the canonical JSON has no escapes, the `profile_id` alphabet needs none | `DateKey.CanonicalJSON`, `DateKey.Compact` | `datekey.TestGoldenDK1Vectors`, `TestNormativeRoundVector` | | 19 | `dk1_` canonicality; steps 1 to 3 `ERR_DATEKEY_INVALID`, step 6 `ERR_DATEKEY_NON_CANONICAL`; step 1 accepts either Base64 alphabet, padding and non-zero trailing bits but no other character (CR and LF included); step 2 one RFC 8259 JSON object, no byte order mark; JSON numbers by their exact decimal value; round in 1..2^53−1 without the profile | `datekey.Parse` (`decodeBase64`, which rejects CR and LF before the Go decoders, `parseJSON`, which rejects invalid UTF-8 before `encoding/json` can replace it, `jsonUint`) | `datekey.TestGoldenDK1Vectors`, `TestReadingRules`, `TestNumberSpellings`, `FuzzParse`; mutation *non-canonical dk1_ JSON*; `testdata/vectors/dk1.json` (*byte order mark*, *line feed inside the Base64*, *carriage return and line feed after the Base64*, *version 1.0000000000000001: its exact value, not a double*, *invalid UTF-8 in a member a repeated name overwrites*) | -| 20 | File extensions and magic | magic checks in `capsule.ParsePrelude`, `accesskey.Decode`; v0.15: `.dkr`, a release object identified by its type tag (`provider.DecodeRelease`, `provider.IsArchive` to tell an archive apart in `cmd/datekeys`) | mutation *a .dkk offered as a .dkc*; `accesskey.TestDecodeRejects` *a .dkc*; `provider.TestReleaseVectors` | +| 20 | File extensions and magic | magic checks in `capsule.ParsePrelude`, `accesskey.Decode`; v0.15: no extension for the release object, identified by its type tag (`provider.DecodeRelease`, `provider.IsArchive` to tell an archive apart in `cmd/datekeys`) | mutation *a .dkk offered as a .dkc*; `accesskey.TestDecodeRejects` *a .dkc*; `provider.TestReleaseVectors` | | 21 | `capsule_id`: exactly 16 bytes from a CSPRNG (MUST) | `capsule.Encrypt` (16 bytes from `crypto/rand`), `capsule.DecodeHeader` | `capsule.TestPortableKeysAreNeverReused` | | 22 | `.dkc` framing; `VERSION` is the capsule format, 1, 2 or 3, which fixes the schema version of CONTROL_CBOR, the stanzas of INNER_ACCESS_AGE and the padding of the payload; `PUBLIC_HEADER_LEN` in 1..1 MiB, `SEALED_CONTROL_LEN` in 1..64 MiB; PAYLOAD_AGE to EOF, at least an age header | `capsule.Format` (`Format1`, `Format2`, `Format3`), `capsule.Prelude` (`Format`), `capsule.ParsePrelude` | `capsule.TestFormatDispatch`, `TestFormatRelabel`, `TestFrameLengthLowerBounds`, `FuzzParsePrelude`; mutations *version changed* (`VERSION` 4, in the three formats), *flags != 0*, *reserved != 0*, *magic*, length limits, and the relabelings of the lists of formats 2 and 3 of §64 | | 23 | PRELUDE; order of the checks of steps 1 and 2, the version being the format, 1, 2 or 3; section bytes present at steps 3 and 5 | `Prelude.Bytes`, `capsule.ParsePrelude`, `capsule.Inspect` | `capsule.TestConformanceFixtures`, `TestFrameLengthLowerBounds`, `TestPrecedenceAcrossSteps`, `TestFormatDispatch`; `testdata/vectors/inspect_differential.json` | @@ -80,10 +80,10 @@ number their sections the same, but for §7.10, new in v0.14, and §47.1 and | 45 | Release API: its answer is the release object of §47.1 (v0.15), and its user a network source; the HTTP form is informative | the object: `provider.EncodeRelease`, `provider.DecodeRelease`; the API itself is out of scope (server, plan §2) | `provider.TestReleaseVectors` | | 46 | Release Queue | out of scope (server) | — | | 47 | Release Cache | every release is verified again: `capsule.Open` step 10 and `agewrap.TimeIdentity`; `release_material` is the release object (v0.15): `provider.EncodeRelease` | mutations *release of another round* | -| 47.1 | Release object (v0.15): deterministic CBOR without a frame, `{0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}`; size from 1 to 1024 bytes, then type and version, then schema (`ERR_NON_CANONICAL_CBOR`, a version other than 1 `ERR_UNSUPPORTED_VERSION`); the chain hash, the round and the signature at step 10; drand's JSON accepted as the input of the caller, `ERR_RELEASE_INVALID` when unreadable; a writer saves the object | `provider/release.go`: `EncodeRelease`, `DecodeRelease` (`releaseWire` with `codec.CheckSchema` and `codec.Unmarshal`), `ParseRelease`, `NewReleaseObject`, `MaxReleaseObjectSize`, `MaxReleaseJSONSize`; `cmd/datekeys` `releaseInHand`, `writeRelease` | `provider.TestReleaseVectors`, `TestEncodeRelease`, `FuzzDecodeRelease`; `testdata/vectors/release.json`, `testdata/releases/.dkr`; `internal/testkit.TestVectorFilesAreCurrent` | +| 47.1 | Release object (v0.15): deterministic CBOR without a frame, `{0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}`; size from 1 to 1024 bytes, then type and version, then schema (`ERR_NON_CANONICAL_CBOR`, a version other than 1 `ERR_UNSUPPORTED_VERSION`); the chain hash, the round and the signature at step 10; drand's JSON accepted as the input of the caller, `ERR_RELEASE_INVALID` when unreadable; a Release Cache and the Release API keep and serve the object | `provider/release.go`: `EncodeRelease`, `DecodeRelease` (`releaseWire` with `codec.CheckSchema` and `codec.Unmarshal`), `ParseRelease`, `NewReleaseObject`, `MaxReleaseObjectSize`, `MaxReleaseJSONSize`; `cmd/datekeys` `releaseInHand` | `provider.TestReleaseVectors`, `TestEncodeRelease`, `FuzzDecodeRelease`; `testdata/vectors/release.json`, `testdata/releases/.cbor`; `internal/testkit.TestVectorFilesAreCurrent` | | 48 | Multi-relay; a network source verifies every response with the rules of §63 step 10 and discards the invalid ones: none valid is `ERR_RELEASE_UNAVAILABLE` at step 9, and so is any other failure of a source, with no other code | `provider/drand.Client` (race, first *verified* release wins; the failure of each relay kept as text only, a context that ended detectable with `errors.Is`), the `provider.ReleaseSource` contract, `capsule.Open` (step 9 keeps only the text of a source error with another code or none) | `drand.TestRaceWaitsForAValidSignature`, `TestRejectMalformedRelayResponses`, `TestFetchErrorHasOneCode`, `TestUnavailabilityAndCancellation`; `capsule.TestReleaseFromANetworkSource`, `TestReleaseSourceErrorsAtStep9` | | 49 | Direct recovery from the provider | `provider/drand`; v0.15: a release in hand, `provider.Supplier` (`provider.Encoded`, `provider.Archive`) in `capsule.OpenOptions.Release`; `datekeys decrypt -release` | `drand.TestLiveRelays`, `capsule.TestLiveLifecycle` (`-tags integration`); `capsule.TestReleaseInHand`, `TestReleaseInHandErrors`; `cmd/datekeys.TestDecryptWithReleaseInHand` | -| 50 | Historical release dependency; v0.15: the `.dkr` next to the `.dkc` as the main path, the `.dkr` next to a `.dkk` with a locator, the release archive as an informative format | documented in `README.md`; `provider.Archive`, `provider.EncodeArchiveHeader`; `datekeys decrypt -save-release`, `datekeys release` | `provider.TestArchive`; `cmd/datekeys.TestReleaseCommand`, `TestDecryptWithReleaseInHand` (*a local archive*); the `archive` block of `testdata/vectors/release.json`, `testdata/releases/archive_1000_1004.bin` | +| 50 | Historical release dependency; v0.15: long-term recovery on archives of all rounds and on cache services that serve them, with no hosting promise, the release archive as an informative format | documented in `README.md`; `provider.Archive`, `provider.EncodeArchiveHeader`; `datekeys decrypt -release` with a local archive | `provider.TestArchive`; `cmd/datekeys.TestDecryptWithReleaseInHand` (*a local archive*); the `archive` block of `testdata/vectors/release.json`, `testdata/releases/archive_1000_1004.bin` | | 51 | Quicknet release verification; order and codes of §63 step 10: the round (`ERR_ROUND_MISMATCH`), then the signature, the canonical encoding of a point of G1 other than the identity (§12.2) that verifies as the BLS signature of the round (`ERR_RELEASE_INVALID`); those codes for a release supplied directly, a network source discarding an invalid one at step 9 (`ERR_RELEASE_UNAVAILABLE`) | `provider.Verify`; v0.15: first the chain hash a release object names, `ERR_PROFILE_MISMATCH` (`provider.Verify`) | `provider.TestVerifyPublishedReleases`, `TestVerifyRejects` (x + p, the point at infinity alone, with a payload or with the sort flag, no compression flag, the negated signature), `TestVerifyUsesThePinnedKeyOnly`; `capsule.TestReleaseFromANetworkSource`; mutations *DateKey A + release of round B*, *release of another round*, *release signature …*; the `objects` of `testdata/vectors/release.json`; mutations *release object of another chain* | | 52 | DNS / MITM | `provider/drand` (no redirects, bounded responses, BLS) | `drand.TestRedirectsAreNotFollowed`, `TestRejectMalformedRelayResponses`, `TestRandomnessMustMatchWhenPresent` | | 53 | Harvest now, decrypt later | `cmd/datekeys` warning beyond one year; v0.15: the release among what a long horizon needs (text) | `cmd/datekeys.TestLongHorizonWarning` | @@ -99,7 +99,7 @@ number their sections the same, but for §7.10, new in v0.14, and §47.1 and | 60 | Conceptual Go interfaces | `provider.ReleaseSource`, `provider.Verify`, `datekey.Resolve`, `datekey.RoundTime` | — | | 61 | `time_only` encryption flow, format 3: the files measured, hashed, sealed and read again | `capsule.EncryptFiles` | `capsule.TestEncryptFilesRoundTrip`, `TestEncryptFilesLengths`, `TestEncryptFilesChangedFile`, `TestSealedControlLength`; the live test (`-tags integration`) | | 62 | `time_and_key` encryption flow, format 3: 16 recipients, and the SEALED_CONTROL_LEN of an INNER_ACCESS_AGE of 16 stanzas | `capsule.EncryptFiles` | `capsule.TestEncryptFilesTimeAndKey`, `TestEncryptRoundTripBothPolicies`, `TestPortableKeysAreNeverReused`, `TestSealedControlLength` | -| 62.1 | Writer rules: format 3 only, formats 1 and 2 being written only by a generator of test vectors; an instant after the clock of the writer; from 1 to 16 credentials, none twice, canonical and not of low order; dummies and a random order; `capsule_id`, `I_PAYLOAD`, `I_ACCESS`, `credential_id`, dummies and order from a CSPRNG, `I_PAYLOAD` and dummies never reused or derived; L known before sealing, at most L_MAX, code 1 or 2; SEALED_CONTROL_LEN exact, measured by a provisional seal and checked; limits; on error, the output is discarded; in format 3, the area of 32768 bytes, signed or not, and 65536 only when the creator widens it once the signatures are made, without signing again for it, a capsule refused when they do not fit, `SECURITY_CBOR` always and empty without a signature or a seal, another area or `SECURITY_CBOR` only from a generator of test vectors (rule 13), a head with a fresh salt, a file or a comment, the order of R8, the layout and the SHA-256 of what is written, at most 16 MiB, paths and texts refused with the rule and the character, the mtime taken at load and omitted out of range, the three CBOR objects decoded with the rules of the reader (MUST), and files that must not change between the two readings; with a signature or a seal, each verified with the rules of the reader before writing, never one that gives F1, F2, F5, S1, S2 or S3 (rule 19), `AUTHOR_MESSAGE` given as text and its code shown before each signature, `SIGNERS` closed before the first (rule 20), a CAdES-T for each signer of `alg` 2 (rule 21), the seal of `seal_type` 2 over `SEAL_SUBJECT` after the signature (rule 22), and no secret on disk while waiting for them (rule 25); the public note only when asked for, with the rules of the declared author and a warning (rule 23); the rules of §38.1 for a key of words, and for a `.dkk` with a locator the rest stored before the `.dkk` is written (rule 24). SHOULD: code 2 by default, self-checks, wiping | `capsule.EncryptFiles` (`newHead`, `readSource`, `selfCheckHead`) and `capsule.Encrypt`, through their sealer (`EncryptOptions.Padding`, `accessRecipients`, `fillSlots`, `copyExactly`, `selfCheckHeader`, `selfCheckControl`, `selfCheckInner`, `selfCheckPayload`); `agewrap.CheckX25519Recipient`; `EncryptOptions.TestVectors` for `Encrypt` of format 2, and `internal/testkit.Build`, generators of test vectors (§70); in format 3, `capsule.AreaLen`, `LargeAreaLen` and `EncryptOptions.LargeArea`, the area decided by `EncryptFiles`, in the `prepare` that it gives `sealer.write`, once `sealer.security` has signed and sealed and checked both with `EvaluateSecurityIn` (rules 13, 19, 21 and 22), `EncryptOptions.AuthorKey`, `CMSSigner` and `Sealer`, a typed nil in one of them an error (`newSealer`, `isNil`), nothing written to `dst` before they return and the control and `I_PAYLOAD` kept in memory (rule 25); `EncryptOptions.TestAreaLen`, with `TestVectors`, the area of 512 bytes of the fixtures of v0.10 (rule 13); `EncryptOptions.PublicNote` and `extension.CheckWrite` (rule 23, §72); `EncryptOptions.Words` and `wordkey.Check`, and `locator.NewEnvelope`, `Locator.Marshal` and `Info.Extension` (rule 24); `cmd/datekeys`: `announced` (rule 20) and the warning of `-note` (rule 23); v0.15, rules 26 to 28 (SHOULD of the SDK): `datekeys release` and `decrypt -save-release` save the `.dkr`; the warning and the annex next to the `.dkc` are not implemented by the CLI | `capsule.TestEncryptFilesRejects`, `TestEncryptFilesChangedFile`, `TestEncryptIsForTestVectors`, `TestEncryptFilesHeadCritical`, `TestEncryptRejectsInvalidOptions`, `TestCredentialBounds`, `TestEncryptSourceLength`, `TestEncryptSelfCheck`, `TestSealedControlLength`, `TestPayloadIdentityReuse`, `TestStanzaOrderIsUniform`, `TestDummyRecipients`, `TestPortableKeysAreNeverReused`; `cmd/datekeys.TestEncryptRefusesPaths`; rules 13 and 19 to 25: `capsule.TestEncryptFilesSigned`, `TestEncryptFilesSignatureChecked` (nothing written), `TestEncryptFilesCMSAndSeal` (a signature without a required signer, or without seals, is not written), `TestAreaChosenAfterSigning` (the area widened once signed, signing once; without a signature, 32 KiB with `LargeArea`), `TestWriterOptionsChecked`, `TestPublicNoteRules`, `TestRegisteredExtensionsWhereRegistered`, `TestEncryptFilesWords`; `wordkey.TestCheck`; `locator.TestUsableAddresses`, `TestInfo`; `cmd/datekeys.TestAuthorSignRoundTrip` (the key and the code before the signature), `TestPublicNoteCLI`, `TestKeyOfWords`; rule 25 has no test of its own; `cmd/datekeys.TestReleaseCommand` | +| 62.1 | Writer rules: format 3 only, formats 1 and 2 being written only by a generator of test vectors; an instant after the clock of the writer; from 1 to 16 credentials, none twice, canonical and not of low order; dummies and a random order; `capsule_id`, `I_PAYLOAD`, `I_ACCESS`, `credential_id`, dummies and order from a CSPRNG, `I_PAYLOAD` and dummies never reused or derived; L known before sealing, at most L_MAX, code 1 or 2; SEALED_CONTROL_LEN exact, measured by a provisional seal and checked; limits; on error, the output is discarded; in format 3, the area of 32768 bytes, signed or not, and 65536 only when the creator widens it once the signatures are made, without signing again for it, a capsule refused when they do not fit, `SECURITY_CBOR` always and empty without a signature or a seal, another area or `SECURITY_CBOR` only from a generator of test vectors (rule 13), a head with a fresh salt, a file or a comment, the order of R8, the layout and the SHA-256 of what is written, at most 16 MiB, paths and texts refused with the rule and the character, the mtime taken at load and omitted out of range, the three CBOR objects decoded with the rules of the reader (MUST), and files that must not change between the two readings; with a signature or a seal, each verified with the rules of the reader before writing, never one that gives F1, F2, F5, S1, S2 or S3 (rule 19), `AUTHOR_MESSAGE` given as text and its code shown before each signature, `SIGNERS` closed before the first (rule 20), a CAdES-T for each signer of `alg` 2 (rule 21), the seal of `seal_type` 2 over `SEAL_SUBJECT` after the signature (rule 22), and no secret on disk while waiting for them (rule 25); the public note only when asked for, with the rules of the declared author and a warning (rule 23); the rules of §38.1 for a key of words, and for a `.dkk` with a locator the rest stored before the `.dkk` is written (rule 24). SHOULD: code 2 by default, self-checks, wiping | `capsule.EncryptFiles` (`newHead`, `readSource`, `selfCheckHead`) and `capsule.Encrypt`, through their sealer (`EncryptOptions.Padding`, `accessRecipients`, `fillSlots`, `copyExactly`, `selfCheckHeader`, `selfCheckControl`, `selfCheckInner`, `selfCheckPayload`); `agewrap.CheckX25519Recipient`; `EncryptOptions.TestVectors` for `Encrypt` of format 2, and `internal/testkit.Build`, generators of test vectors (§70); in format 3, `capsule.AreaLen`, `LargeAreaLen` and `EncryptOptions.LargeArea`, the area decided by `EncryptFiles`, in the `prepare` that it gives `sealer.write`, once `sealer.security` has signed and sealed and checked both with `EvaluateSecurityIn` (rules 13, 19, 21 and 22), `EncryptOptions.AuthorKey`, `CMSSigner` and `Sealer`, a typed nil in one of them an error (`newSealer`, `isNil`), nothing written to `dst` before they return and the control and `I_PAYLOAD` kept in memory (rule 25); `EncryptOptions.TestAreaLen`, with `TestVectors`, the area of 512 bytes of the fixtures of v0.10 (rule 13); `EncryptOptions.PublicNote` and `extension.CheckWrite` (rule 23, §72); `EncryptOptions.Words` and `wordkey.Check`, and `locator.NewEnvelope`, `Locator.Marshal` and `Info.Extension` (rule 24); `cmd/datekeys`: `announced` (rule 20) and the warning of `-note` (rule 23); v0.15, rules 26 and 27 (SHOULD of the SDK): the warning and the annex next to the `.dkc` are not implemented by the CLI | `capsule.TestEncryptFilesRejects`, `TestEncryptFilesChangedFile`, `TestEncryptIsForTestVectors`, `TestEncryptFilesHeadCritical`, `TestEncryptRejectsInvalidOptions`, `TestCredentialBounds`, `TestEncryptSourceLength`, `TestEncryptSelfCheck`, `TestSealedControlLength`, `TestPayloadIdentityReuse`, `TestStanzaOrderIsUniform`, `TestDummyRecipients`, `TestPortableKeysAreNeverReused`; `cmd/datekeys.TestEncryptRefusesPaths`; rules 13 and 19 to 25: `capsule.TestEncryptFilesSigned`, `TestEncryptFilesSignatureChecked` (nothing written), `TestEncryptFilesCMSAndSeal` (a signature without a required signer, or without seals, is not written), `TestAreaChosenAfterSigning` (the area widened once signed, signing once; without a signature, 32 KiB with `LargeArea`), `TestWriterOptionsChecked`, `TestPublicNoteRules`, `TestRegisteredExtensionsWhereRegistered`, `TestEncryptFilesWords`; `wordkey.TestCheck`; `locator.TestUsableAddresses`, `TestInfo`; `cmd/datekeys.TestAuthorSignRoundTrip` (the key and the code before the signature), `TestPublicNoteCLI`, `TestKeyOfWords`; rule 25 has no test of its own | | 63 | Decryption flow; step 2 accepts the formats 1, 2 and 3, and the steps after it apply the rules of the format: in formats 2 and 3, 16 stanzas at step 12, a control of schema version 2 at step 14, L, the code and P at step 16, a plaintext of P bytes with a zero padding at step 17 (`ERR_INTEGRITY` whenever it is found), the first L bytes at step 18; in format 3, step 17 in its substeps, a failure of age or a plaintext whose length is not P prevailing and a code other than `ERR_INTEGRITY` reported only after reading to EOF, and a caller without a `Sink` stopped right after step 2; steps 4 and 14 validate critical extensions (unknown, then invalid data); step 5 reads SEALED_CONTROL, a MUST (`ERR_INTEGRITY`), and SHOULD inspect its age header; step 8 argument rules; step 9 order: the `.dkk` as an object (decoded there when still encoded), its `capsule_id` and `capsule_digest`, credentials (nil identities are none) before the clock, round time, request, and nothing of the credentials under `time_only`; a network source verifies each response with the rules of step 10 and discards the invalid ones (none valid: `ERR_RELEASE_UNAVAILABLE`, step 9), and any failure of a source is `ERR_RELEASE_UNAVAILABLE` alone, whatever code its error carries; step 10: round, then signature, a canonical point other than the identity (§12.2), the codes of a release supplied directly; step 11: the tlock stanza body `U \|\| V \|\| W` of \|U\| + 32 bytes (128 in Quicknet), U canonical and not the identity, the IBE check r·G == U, every failure `ERR_INTEGRITY`, H2, H3 and H4 those of drand/kyber `encrypt/ibe`, H2 over the element of GT serialized in the order of kilic/bls12-381 (c1 before c0 at every level of the tower), with the frozen vector H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`; the codes of the identities at steps 11, 13 (malformed X25519 stanza `ERR_INTEGRITY`, an identity that unwraps two stanzas `ERR_POLICY_STRUCTURE_MISMATCH` whatever the order, none `ERR_ACCESS_INVALID`) and 17; step 15 `ERR_HEADER_BINDING` | `capsule.Inspect` (steps 1–8), `capsule.Open` (steps 9–18; `openBody`, `drain`, `ErrSinkRequired`; `OpenOptions.AccessKeyFile`, `checkAccessKey`, `checkCapsuleDigest`), the `provider.ReleaseSource` contract, `provider/drand.Client` and `capsule.sourceFailure` (step 9), `tlock.TimeUnlock` with the kyber-bls12381 pairing (step 11), MUST rules inside `agewrap` identities (`AccessIdentity` tries every identity on every stanza; `TimeIdentity` checks the length of the tlock stanza body and U before `tlock.TimeUnlock`); no error copies the text of an error of age, tlock, kyber or drand (`agewrap`, `capsule.classify`), since kyber's IBE error carries the candidate plaintext and r; `cmd/datekeys` hands the `.dkk` over encoded; `datekeys inspect -json` rendered by `internal/inspectview`; v0.15: step 9.c only for `OpenOptions.Source`, a release in hand (`OpenOptions.Release`) not compared with the clock and `Opened.ClockBehind`; step 10 starts with `provider.ParseRelease`, then `provider.Verify` with the chain hash | `capsule.TestConformanceFixtures` (stage by stage), `TestOpen3`, `TestOpen3Substeps`, `TestFormatDispatch`, `TestFormatRelabel`, `TestPaddingChecksAtStep17`, `TestTlockFailureDiagnosticsCarryNoSecrets`, `TestPlaintextWriterFailureKeepsItsText`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestPrecedenceAcrossSteps`, `TestControlCriticalBeforeHeaderBinding`, `TestReleaseFromANetworkSource`, `TestReleaseSourceErrorsAtStep9`, `agewrap.TestAccessIdentityStrictness`, `TestMalformedX25519Stanzas`, `TestTlockH2Vector` (`testdata/vectors/tlock_ibe.json`, generated by `internal/testkit.IBEVectors`, and step 11 recomputed with H2 and H4 against the file key tlock unwraps), `cmd/datekeys.TestDecryptAccessKeyOrder`, `TestMutationCorpus`, `TestInspectDifferentialCorpus` (`testdata/vectors/inspect_differential.json`: 5110 deterministic mutations of 14 fixtures, two of them of format 3, the 1825 of the format 1 ones first with the verdict of steps 1–8, generated by `internal/testkit.InspectDifferential`); `cmd/datekeys.TestInspectJSONGoldens` (`testdata/fixtures/*.inspect.json`); `capsule.TestReleaseInHand`, `TestReleaseInHandErrors`; mutations *round not reached yet* (opens) and *round not reached yet, from a network source* | | 64 | Mandatory mutation tests: the first two lists in the three formats, the list of format 2 in format 2, and that of format 3, four of whose cases open with their verdicts, F2 for a signature of `alg` 1 that does not verify among them; the lists of v0.11 and v0.12: the signature of `alg` 1 and of `alg` 2, the area widened after signing, the seal, `alg` and `seal_type` 4294967295, the same P with a signature and without, the public note, the key of words and `datekeys.capsule` | `internal/testkit.Mutations` (the corpus: `specMutations` for each format, `furtherMutations`, `format2Mutations`, `format3Mutations` with `LoadedFixture.WithBody`, among them those of the list of v0.11 from `format3_signed`, `format3_unsigned` and `format3_note`, `signed1`), `internal/testkit.MutationCorpus` (its export); the cases of formats 2 and 3 derived without randomness, by sealing the fixtures again with their known file keys and nonces (`internal/testkit/reseal.go`, `mutations3.go`), exported as edits of their fixture (`internal/testkit.Splice`); the cases of `alg` 2, `seal_type` 2 and `datekeys.capsule`, vectors of `security_cms.json` and `locator.json`, frozen once written (`internal/testkit/genfixtures`, `frozenVectors`) | `capsule.TestMutationCorpus`: the 178 listed mutations, 33 in each format, the 23 of the list of format 2, the 48 of that of format 3 and 8 of that of v0.11 (`internal/testkit.SpecMutationsPerFormat`, `Format2SpecMutations`, `Format3SpecMutations`, `V011SpecMutations`), plus 44 more, built afresh; `capsule.TestExportedMutationCorpus`: `testdata/vectors/mutations.json`, the same 222 cases as frozen data (capsule, `.dkk`, identities, recorded release and its source, clock, registry, known extensions), replayed with the recorded error and step, or the recorded verdicts; `capsule.TestPointMutationsChangeOnlyTheEncoding`: the ten point mutations keep a valid header MAC, and a decoder that reduces coordinates modulo p opens the c0 + p and x + p cases; `internal/testkit.TestResealReproducesFixtures`, `TestFixedX25519Stanza`; the cases of the lists of v0.11 and v0.12 outside the corpus: `ed25519strict.TestVectors` (`ed25519_strict.json`), `capsule.TestCMSVectors` (`security_cms.json`), `locator.TestLocatorVectors` (`locator.json`), `wordkey.TestKeyVector`, `TestNormalize` and `TestCheck`, `testdata/vectors/note.json`, and the same P of the fixtures `format3_unsigned` and `format3_signed` (`capsule.TestConformanceFixtures`); those of `alg` 2, `seal_type` 2 and `datekeys.capsule` of the lists of v0.11 and v0.12, in `security_cms.json` and `locator.json` | | 65 | Quicknet vectors | `internal/testkit.RoundVectors` | `datekey.TestGoldenRoundVectors` | diff --git a/internal/testkit/genfixtures/main.go b/internal/testkit/genfixtures/main.go index 6faec38..b77be06 100644 --- a/internal/testkit/genfixtures/main.go +++ b/internal/testkit/genfixtures/main.go @@ -164,8 +164,9 @@ func vectors(dir string) error { } // releaseVectors writes the release objects (spec v0.15, §47.1): -// vectors/release.json, and in releases/ the .dkr of each published round of -// the tests and a local archive, the informative format of §50. +// vectors/release.json, and in releases/ the release object of each +// published round of the tests and a local archive, the informative format +// of §50. func releaseVectors(dir string) error { rv, err := testkit.ReleaseVectors() if err != nil { diff --git a/internal/testkit/releasevectors.go b/internal/testkit/releasevectors.go index 565f4d6..a667c6d 100644 --- a/internal/testkit/releasevectors.go +++ b/internal/testkit/releasevectors.go @@ -15,8 +15,8 @@ import ( "g.activething.com/go/DateKeys/provider" ) -// ReleaseVectorFile is testdata/vectors/release.json: release objects, the -// content of a .dkr file (spec v0.15, §47.1), and drand's JSON, each checked +// ReleaseVectorFile is testdata/vectors/release.json: release objects (spec +// v0.15, §47.1), and drand's JSON, each checked // as step 10 of spec §63 checks a release that the caller supplies, against // the pinned Quicknet profile and the round of a DateKey; and the lookups of // a local release archive, an informative format (spec v0.15, §50). @@ -67,8 +67,10 @@ const ( archiveCount = 5 ) -// ReleaseFileName is the name of the .dkr of round in testdata/releases. -func ReleaseFileName(round uint64) string { return strconv.FormatUint(round, 10) + ".dkr" } +// ReleaseFileName is the name of the release object of round in +// testdata/releases: the round and .cbor, the generic extension of CBOR (RFC +// 8949), since the protocol gives the object no file extension of its own. +func ReleaseFileName(round uint64) string { return strconv.FormatUint(round, 10) + ".cbor" } // releaseObject returns the release object of r with the Quicknet chain // hash, or with chain when it is not nil. @@ -84,8 +86,8 @@ func releaseObject(r provider.Release, chain []byte) []byte { return b } -// ReleaseFiles computes the files of testdata/releases: the .dkr of each -// published round of the tests, and the local archive ArchiveFile. +// ReleaseFiles computes the files of testdata/releases: the release object +// of each published round of the tests, and the local archive ArchiveFile. func ReleaseFiles() (map[string][]byte, error) { files := map[string][]byte{} for _, round := range Rounds { @@ -124,7 +126,7 @@ func ReleaseVectors() (ReleaseVectorFile, error) { p := profile.Quicknet() f := ReleaseVectorFile{ Spec: SpecVersion, - Description: "The release object, the content of a .dkr file (spec v0.15, §47.1), and drand's JSON as the input of the caller, " + + Description: "The release object (spec v0.15, §47.1), and drand's JSON as the input of the caller, " + "each checked against the pinned Quicknet profile and the round of a DateKey as step 10 of spec §63 checks a release that the caller supplies; " + "and the lookups of a local release archive (spec v0.15, §50). See testdata/README.md.", Profile: p.ID, @@ -288,7 +290,7 @@ func ReleaseVectors() (ReleaseVectorFile, error) { errs = append(errs, fmt.Errorf("archive round %d: %s, want %s (%v)", round, l.Result, want, err)) } if l.Result == ok && !bytes.Equal(b, files[ReleaseFileName(round)]) { - errs = append(errs, fmt.Errorf("archive round %d: another object than its .dkr", round)) + errs = append(errs, fmt.Errorf("archive round %d: another object than its file in testdata/releases", round)) } f.Archive.Lookups = append(f.Archive.Lookups, l) } diff --git a/internal/testkit/testkit_test.go b/internal/testkit/testkit_test.go index c7bf41c..2ef7894 100644 --- a/internal/testkit/testkit_test.go +++ b/internal/testkit/testkit_test.go @@ -293,6 +293,16 @@ func TestVectorFilesAreCurrent(t *testing.T) { t.Errorf("testdata/releases/%s is stale: run go run ./internal/testkit/genfixtures -out testdata", name) } } + // The generator never deletes: a file it no longer writes is stale. + entries, err := os.ReadDir("../../testdata/releases") + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + if _, ok := files[e.Name()]; !ok { + t.Errorf("testdata/releases/%s is not generated: remove it", e.Name()) + } + } for _, v := range []struct { file string want any diff --git a/provider/archive.go b/provider/archive.go index 1686502..8df91a0 100644 --- a/provider/archive.go +++ b/provider/archive.go @@ -109,10 +109,10 @@ func IsArchive(b []byte) bool { // Quicknet. A round written as zeros is missing. // // A local archive is a release in hand: it implements Supplier, and its -// entry is decoded and verified at step 10 of spec §63 like a .dkr. A round -// it lacks, a header it cannot read, an archive of another chain or of -// another length are failures to supply a release, ErrReleaseUnavailable at -// step 9: the format is informative and has no codes of its own. +// entry is decoded and verified at step 10 of spec §63 like any release +// object. A round it lacks, a header it cannot read, an archive of another +// chain or of another length are failures to supply a release, +// ErrReleaseUnavailable at step 9: the format is informative and has no codes of its own. type Archive struct { r io.ReaderAt size int64 diff --git a/provider/release.go b/provider/release.go index 3a6655f..aa673e3 100644 --- a/provider/release.go +++ b/provider/release.go @@ -11,8 +11,9 @@ import ( "g.activething.com/go/DateKeys/profile" ) -// Schema constants of the release object, the content of a .dkr file (spec -// v0.15, §47.1). +// Schema constants of the release object (spec v0.15, §47.1): the answer of +// the Release API, an entry of a Release Cache, and a release the caller +// gives from a file or from an archive. const ( ReleaseTypeTag = "datekeys-release" ReleaseSchemaVersion = 1 @@ -98,8 +99,8 @@ func (w *releaseWire) decode(d *codec.Decoder) error { return d.EndMap() } -// EncodeRelease returns the release object of r, the content of a .dkr file -// (spec v0.15, §47.1): its chain hash, its round and its signature. It does +// EncodeRelease returns the release object of r (spec v0.15, §47.1): its +// chain hash, its round and its signature. It does // not verify the release: Verify does, against the pinned profile. func EncodeRelease(r Release) ([]byte, error) { switch { @@ -174,8 +175,8 @@ func parseDrandJSON(b []byte) (Release, error) { } // Supplier hands over a release that the caller has in hand (spec v0.15, -// §49, §63 step 9.c): a release object read from a .dkr file, drand's JSON -// that the person saved, or an entry of a local archive. It makes no network +// §49, §63 step 9.c): a release object read from a file, drand's JSON that +// the person saved, or an entry of a local archive. It makes no network // request, so capsule.Open asks it for the release without comparing its // clock with the round time: a valid signature proves that the round was // published. @@ -188,8 +189,8 @@ type Supplier interface { Supply(p *profile.Profile, c Condition) ([]byte, error) } -// Encoded is a release in hand, already read: the bytes of a .dkr file or of -// drand's JSON. It supplies itself whatever the condition; step 10 compares +// Encoded is a release in hand, already read: the bytes of a release object +// or of drand's JSON. It supplies itself whatever the condition; step 10 compares // its round with the DateKey. type Encoded []byte @@ -197,8 +198,9 @@ type Encoded []byte func (e Encoded) Supply(*profile.Profile, Condition) ([]byte, error) { return e, nil } // NewReleaseObject returns the release object of a release of the profile p, -// with the chain hash of p: what a reader saves as a .dkr after verifying the -// release (spec v0.15, §62.1). +// with the chain hash of p: what a Release Cache or an archive stores, or +// the Release API serves, after verifying the release (spec v0.15, §45, +// §47, §47.1). func NewReleaseObject(p *profile.Profile, r Release) ([]byte, error) { r.ChainHash = p.ChainHash[:] return EncodeRelease(r) diff --git a/provider/release_test.go b/provider/release_test.go index 8daf6e6..694904d 100644 --- a/provider/release_test.go +++ b/provider/release_test.go @@ -58,15 +58,15 @@ func TestReleaseVectors(t *testing.T) { } } -// The local archive of testdata supplies the .dkr of each round it holds, -// and nothing for a round it lacks or outside it, for another chain, or when -// its length is not the one its header announces. +// The local archive of testdata supplies the release object of each round it +// holds, and nothing for a round it lacks or outside it, for another chain, +// or when its length is not the one its header announces. func TestArchive(t *testing.T) { b, err := os.ReadFile("../testdata/releases/" + testkit.ArchiveFile) if err != nil { t.Fatal(err) } - if !provider.IsArchive(b) || provider.IsArchive(mustRead(t, "../testdata/releases/1000.dkr")) { + if !provider.IsArchive(b) || provider.IsArchive(mustRead(t, "../testdata/releases/1000.cbor")) { t.Fatal("IsArchive") } p := profile.Quicknet() @@ -85,15 +85,15 @@ func TestArchive(t *testing.T) { other := p.Clone() other.ChainHash[0] ^= 1 short := provider.NewArchive(bytes.NewReader(b[:len(b)-1]), int64(len(b)-1)) - notArchive := mustRead(t, "../testdata/releases/1000.dkr") + notArchive := mustRead(t, "../testdata/releases/1000.cbor") for name, c := range map[string]struct { a *provider.Archive p *profile.Profile }{ - "another chain": {a, other}, - "a byte missing": {short, p}, - "a .dkr": {provider.NewArchive(bytes.NewReader(notArchive), int64(len(notArchive))), p}, - "no byte": {provider.NewArchive(bytes.NewReader(nil), 0), p}, + "another chain": {a, other}, + "a byte missing": {short, p}, + "a release object": {provider.NewArchive(bytes.NewReader(notArchive), int64(len(notArchive))), p}, + "no byte": {provider.NewArchive(bytes.NewReader(nil), 0), p}, } { if _, err := c.a.Supply(c.p, provider.Condition{Round: 1000}); !onlyUnavailable(err) { t.Fatalf("%s: %v", name, err) @@ -153,7 +153,7 @@ func TestEncodeRelease(t *testing.T) { // FuzzDecodeRelease: whatever the input, DecodeRelease returns exactly one // normative code, and what it accepts encodes again to the same bytes. func FuzzDecodeRelease(f *testing.F) { - f.Add(mustReadF(f, "../testdata/releases/1000.dkr")) + f.Add(mustReadF(f, "../testdata/releases/1000.cbor")) f.Add([]byte(`{"round":1000,"signature":"00"}`)) f.Fuzz(func(t *testing.T, b []byte) { r, err := provider.ParseRelease(b) diff --git a/scripts/recovery/main.go b/scripts/recovery/main.go index d676462..052461f 100644 --- a/scripts/recovery/main.go +++ b/scripts/recovery/main.go @@ -8,7 +8,12 @@ // tlock. The tlock layer and the age file that it protects, whose file key no // age tool accepts, are written out here step by step. // -// go run ./scripts/recovery -dkc FILE.dkc -release FILE.dkr [-dkk FILE.dkk] -out PATH [-body FILE] +// go run ./scripts/recovery -dkc FILE.dkc -release FILE [-dkk FILE.dkk] -out PATH [-body FILE] +// +// FILE is the release object of the round of the capsule (spec §47.1), from +// any source: an archive, a cache service or any copy. Its signature is +// verified against the Quicknet public key, so its source need not be +// trusted. // // Formats 1 and 2 write the content to the file -out; format 3 writes each // file of its head under the directory -out. -body writes the L bytes the @@ -255,7 +260,7 @@ func (m cborMap) checkType(tag string, version uint64) error { } // --------------------------------------------------------------------------- -// Step 2. The release (.dkr): a CBOR map +// Step 2. The release object: a CBOR map // // 0 → "datekeys-release", 1 → 1, 2 → chain_hash (32 bytes), // 3 → round, 4 → signature (48 bytes, a compressed point of G1) @@ -982,7 +987,7 @@ type result struct { body *body // format 3 only } -func recoverCapsule(dkc, dkr, dkk []byte, log io.Writer) (*result, error) { +func recoverCapsule(dkc, relObj, dkk []byte, log io.Writer) (*result, error) { c, err := parseCapsule(dkc) if err != nil { return nil, err @@ -990,7 +995,7 @@ func recoverCapsule(dkc, dkr, dkk []byte, log io.Writer) (*result, error) { fmt.Fprintf(log, "capsule: format %d, round %d (%s), policy %d\n", c.format, c.round, roundTime(c.round).Format(time.RFC3339), c.policy) - rel, err := readRelease(dkr) + rel, err := readRelease(relObj) if err != nil { return nil, err } @@ -1052,21 +1057,21 @@ func run(args []string, log io.Writer) error { fs := flag.NewFlagSet("recovery", flag.ContinueOnError) fs.SetOutput(log) dkcPath := fs.String("dkc", "", "the capsule (.dkc)") - dkrPath := fs.String("release", "", "the release of its round (.dkr)") + relPath := fs.String("release", "", "the release object of its round") dkkPath := fs.String("dkk", "", "the access key (.dkk), for time_and_key") out := fs.String("out", "", "output: a file in formats 1 and 2, a directory in format 3") bodyPath := fs.String("body", "", "optional: write the L bytes (content, or BODY in format 3)") if err := fs.Parse(args); err != nil { return err } - if *dkcPath == "" || *dkrPath == "" || *out == "" { - return errors.New("usage: recovery -dkc FILE.dkc -release FILE.dkr [-dkk FILE.dkk] -out PATH [-body FILE]") + if *dkcPath == "" || *relPath == "" || *out == "" { + return errors.New("usage: recovery -dkc FILE.dkc -release FILE [-dkk FILE.dkk] -out PATH [-body FILE]") } dkc, err := os.ReadFile(*dkcPath) if err != nil { return err } - dkr, err := os.ReadFile(*dkrPath) + relObj, err := os.ReadFile(*relPath) if err != nil { return err } @@ -1076,7 +1081,7 @@ func run(args []string, log io.Writer) error { return err } } - res, err := recoverCapsule(dkc, dkr, dkk, log) + res, err := recoverCapsule(dkc, relObj, dkk, log) if err != nil { return err } diff --git a/scripts/recovery/main_test.go b/scripts/recovery/main_test.go index 41579ac..a13ebf7 100644 --- a/scripts/recovery/main_test.go +++ b/scripts/recovery/main_test.go @@ -97,12 +97,12 @@ func encodeRelease(chainHash []byte, round uint64, sig []byte) []byte { return b } -// committedRelease reads testdata/releases/.dkr and checks that it is +// committedRelease reads testdata/releases/.cbor and checks that it is // the release object built here from the fixture record. func committedRelease(t *testing.T, round uint64, sigHex string) []byte { t.Helper() want := encodeRelease(unhex(t, quicknetChainHash), round, unhex(t, sigHex)) - path := filepath.Join(releasesDir, strconv.FormatUint(round, 10)+".dkr") + path := filepath.Join(releasesDir, strconv.FormatUint(round, 10)+".cbor") got, err := os.ReadFile(path) if err != nil { t.Fatalf("the committed release is missing: %v", err) @@ -143,15 +143,15 @@ func TestRecoverFixtures(t *testing.T) { "time_and_key_portable", } { t.Run(name, func(t *testing.T) { - rec, dkc, dkr, dkk := loadFixture(t, name) + rec, dkc, relObj, dkk := loadFixture(t, name) if name == "format2_time_and_key_sixteen" { - _, err := recoverCapsule(dkc, dkr, nil, io.Discard) + _, err := recoverCapsule(dkc, relObj, nil, io.Discard) if err == nil || !strings.Contains(err.Error(), "needs its .dkk") { t.Fatalf("got %v, want the .dkk to be required", err) } return } - res, err := recoverCapsule(dkc, dkr, dkk, io.Discard) + res, err := recoverCapsule(dkc, relObj, dkk, io.Discard) if err != nil { t.Fatal(err) } @@ -197,7 +197,7 @@ func TestRun(t *testing.T) { out, bodyFile := filepath.Join(dir, "out"), filepath.Join(dir, "body") args := []string{ "-dkc", filepath.Join(fixturesDir, "format3_time_and_key_portable.dkc"), - "-release", filepath.Join(releasesDir, "1000.dkr"), + "-release", filepath.Join(releasesDir, "1000.cbor"), "-dkk", filepath.Join(fixturesDir, "format3_time_and_key_portable.dkk"), "-out", out, "-body", bodyFile, } @@ -230,7 +230,7 @@ func TestBadReleases(t *testing.T) { for _, tc := range []struct { name, want string - dkr []byte + relObj []byte }{ {"wrong chain_hash", "not Quicknet's", encodeRelease(wrongChain, 1000, sig)}, {"release of another round", "needs round 1000", encodeRelease(chain, other.Release.Round, otherSig)}, @@ -238,7 +238,7 @@ func TestBadReleases(t *testing.T) { {"wrong type tag", "type tag", bytes.Replace(encodeRelease(chain, 1000, sig), []byte("release"), []byte("relaxed"), 1)}, } { t.Run(tc.name, func(t *testing.T) { - _, err := recoverCapsule(dkc, tc.dkr, nil, io.Discard) + _, err := recoverCapsule(dkc, tc.relObj, nil, io.Discard) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("got %v, want an error with %q", err, tc.want) } @@ -373,7 +373,7 @@ func TestTampered(t *testing.T) { if err != nil { t.Fatal(err) } - dkr := encodeRelease(unhex(t, quicknetChainHash), rec.Release.Round, unhex(t, rec.Release.Signature)) + relObj := encodeRelease(unhex(t, quicknetChainHash), rec.Release.Round, unhex(t, rec.Release.Signature)) sealedEnd := 16 + 0x79 + 0x1ca // PUBLIC_HEADER_LEN and SEALED_CONTROL_LEN of its prelude stanza := bytes.Index(dkc, []byte("-> tlock")) body := stanza + bytes.IndexByte(dkc[stanza:], '\n') + 1 @@ -388,7 +388,7 @@ func TestTampered(t *testing.T) { t.Run(tc.name, func(t *testing.T) { bad := bytes.Clone(dkc) bad[tc.at] ^= 0x01 - if _, err := recoverCapsule(bad, dkr, nil, io.Discard); err == nil { + if _, err := recoverCapsule(bad, relObj, nil, io.Discard); err == nil { t.Fatal("a tampered capsule opened") } }) diff --git a/scripts/recovery_check.sh b/scripts/recovery_check.sh index bc20446..a27f1b1 100644 --- a/scripts/recovery_check.sh +++ b/scripts/recovery_check.sh @@ -18,7 +18,7 @@ recover_one() { local name=$1 round=$2 shift 2 echo "== $name (round $round)" - go run ./scripts/recovery -dkc "$fx/$name.dkc" -release "$rel/$round.dkr" "$@" \ + go run ./scripts/recovery -dkc "$fx/$name.dkc" -release "$rel/$round.cbor" "$@" \ -out "$tmp/$name" -body "$tmp/$name.body" cmp "$tmp/$name.body" "$fx/$name.plaintext" echo "files written:" diff --git a/spec/DateKeys_Protocol_Specification_v0.15.md b/spec/DateKeys_Protocol_Specification_v0.15.md index 592c8df..013e998 100644 --- a/spec/DateKeys_Protocol_Specification_v0.15.md +++ b/spec/DateKeys_Protocol_Specification_v0.15.md @@ -36,8 +36,8 @@ Define: - reglas del escritor; - Release API; - Release Cache; -- el objeto release (`.dkr`), con el que se guarda el release de una ronda (§47.1); -- recuperación directa contra el proveedor, y a largo plazo con el release guardado (§50); +- el objeto release, el release de una ronda como dato (§47.1); +- recuperación directa contra el proveedor, y a largo plazo con archivos de releases y servicios de caché (§50); - extensiones genéricas; - consideraciones de privacidad; - un anexo informativo para abrir una cápsula sin software de DateKeys (§79). @@ -93,7 +93,7 @@ DateKeys persigue: Implementaciones independientes deben producir y consumir objetos compatibles. 6. **Recuperación independiente** - Siempre que el proveedor conserve o pueda servir el release necesario, o alguien lo haya guardado, como el `.dkr` de la cápsula (§47.1, §50), el ciphertext siga disponible y las credenciales correspondientes existan, un objeto maduro debería poder abrirse sin pasar por la API DateKeys, e incluso sin software de DateKeys (§79). + Siempre que el proveedor conserve o pueda servir el release necesario, o lo conserve un archivo de releases o un servicio de caché (§50), el ciphertext siga disponible y las credenciales correspondientes existan, un objeto maduro debería poder abrirse sin pasar por la API DateKeys, e incluso sin software de DateKeys (§79). 7. **Extensibilidad** Nuevos proveedores y extensiones no deben redefinir objetos antiguos. @@ -254,9 +254,9 @@ DateKeys Access Key (.dkk) ↓ capacidad adicional de acceso -Release (.dkr) +Objeto release ↓ -firma pública de una ronda, guardada como dato (§47.1) +firma pública de una ronda, como dato (§47.1) ``` --- @@ -640,12 +640,9 @@ Esta validación no consulta el perfil: la cota de §15 se comprueba en el paso ```text .dkc → DateKeyCap .dkk → DateKeys Access Key -.dkr → release de una ronda (§47.1) ``` -La extensión no sustituye los magic bytes. El `.dkr` no tiene magic bytes: lo identifica su type tag, la clave 0 de su objeto (§47.1). - -El nombre de un fichero no pertenece al protocolo (§6). El SDK oficial SHOULD dar al `.dkr` el nombre de su cápsula, como `carta.dkc` y `carta.dkr`. +La extensión no sustituye los magic bytes. --- @@ -2168,7 +2165,7 @@ Un escritor: ## 45. Release API -La Release API entrega el release de una condición, sin `capsule_id`. Su respuesta MUST ser el objeto release de §47.1, el mismo que guarda un fichero `.dkr`. Quien la usa es una fuente de red (§49): verifica cada respuesta con las reglas del paso 10 de §63 y descarta la que no las cumple, como con un relay de drand. +La Release API entrega el release de una condición, sin `capsule_id`. Su respuesta MUST ser el objeto release de §47.1, el mismo que guarda una Release Cache (§47). Quien la usa es una fuente de red (§49): verifica cada respuesta con las reglas del paso 10 de §63 y descarta la que no las cumple, como con un relay de drand. Informativo: la forma HTTP de la petición no es normativa. La recomendada se indexa por condición y responde con el objeto, con el tipo `application/cbor`: @@ -2222,7 +2219,7 @@ El SDK MUST verificarlo de nuevo. ## 47.1 Objeto release -El objeto release es el release de una ronda como dato que se guarda: en un fichero `.dkr` junto a la cápsula (§20, §50), en una Release Cache (§47) o como respuesta de la Release API (§45). Es **Deterministic CBOR** con el perfil de §58, sin trama: +El objeto release es el release de una ronda como dato: la respuesta de la Release API (§45), una entrada de una Release Cache (§47) y un release que el llamador da desde un fichero o desde un archivo de releases (§49, §50). El protocolo no le da extensión de fichero propia (§20): lo identifica su type tag, la clave 0 de su objeto. Es **Deterministic CBOR** con el perfil de §58, sin trama: ```text 0 → "datekeys-release" @@ -2234,7 +2231,7 @@ El objeto release es el release de una ronda como dato que se guarda: en un fich Las cinco claves son obligatorias. La cadena se identifica por su `chain_hash`, el identificador de drand: el mismo del stanza tlock (§63, paso 8) y de cualquier archivo de releases ajeno a DateKeys. El objeto no lleva el `profile_hash`, que nadie fuera de DateKeys conoce, ni el `profile_id` (§24). -El objeto de la ronda 1000 de Quicknet mide 111 bytes (`testdata/releases/1000.dkr`): +El objeto de la ronda 1000 de Quicknet mide 111 bytes (`testdata/releases/1000.cbor`): ```text a5 @@ -2259,9 +2256,9 @@ Un lector MAY decodificar el objeto al recibirlo, antes del paso 1, pero MUST in - no nombra su cadena, así que el paso 10 no compara ningún `chain_hash`; - una entrada de más de 8192 bytes, un JSON mal formado, sin `round` o sin `signature`, con otro tipo en ellos, una firma que no es hexadecimal o un `randomness` que no corresponde → `ERR_RELEASE_INVALID`, en el paso 10, antes que la ronda. -Un escritor que guarda un release MUST guardarlo como objeto release, nunca como ese JSON. +Una Release Cache (§47) y la Release API (§45) MUST guardar y servir el release como objeto release, nunca como ese JSON. -Vectores: `testdata/vectors/release.json`, con objetos válidos e inválidos, el JSON de drand y su resultado en el paso 10, y `testdata/releases/.dkr`, el objeto de cada ronda publicada de los fixtures. +Vectores: `testdata/vectors/release.json`, con objetos válidos e inválidos, el JSON de drand y su resultado en el paso 10, y `testdata/releases/.cbor`, el objeto de cada ronda publicada de los fixtures. --- @@ -2286,7 +2283,7 @@ Para Quicknet: + Provider Profile pinneado + -release obtenido de un relay drand, de un .dkr o de un archivo +release obtenido de un relay drand, de un servicio de caché o de un archivo + .dkk si la política la exige ``` @@ -2295,8 +2292,8 @@ debe ser suficiente para ejecutar el flujo de apertura. Un release llega de una de estas dos clases de fuente, y el paso 9 de §63 las trata de forma distinta: -- **fuente de red:** un relay de drand, la Release API, una caché remota o un archivo de releases remoto (§50). Cada petición es observable y revela la ronda a quien la sirve. La fuente verifica cada respuesta y descarta la que no cumple el paso 10, y no se le pide nada antes de `round_time` (paso 9.c); -- **release en la mano:** un release que el llamador suministra sin red: un fichero `.dkr` (§47.1), el JSON de drand que la persona guardó o la entrada de un archivo de releases local (§50). No se compara con el reloj (paso 9.c), y un release que no cumple el paso 10 da los códigos de ese paso. +- **fuente de red:** un relay de drand, la Release API, un servicio de caché o un archivo de releases remoto (§50). Cada petición es observable y revela la ronda a quien la sirve. La fuente verifica cada respuesta y descarta la que no cumple el paso 10, y no se le pide nada antes de `round_time` (paso 9.c); +- **release en la mano:** un release que el llamador suministra sin red: un objeto release (§47.1) o el JSON de drand en un fichero, o la entrada de un archivo de releases local (§50). No se compara con el reloj (paso 9.c), y un release que no cumple el paso 10 da los códigos de ese paso. La API DateKeys es una capa de conveniencia, disponibilidad y caché, no una autoridad criptográfica obligatoria. @@ -2308,27 +2305,29 @@ La recuperación años después depende de que el release histórico necesario s Para Quicknet, esto puede provenir de: -- el `.dkr` de la cápsula, guardado junto al `.dkc` (§47.1, §62.1): es la vía principal, porque no depende de ningún tercero y no revela a nadie la fecha de la cápsula; - un relay drand que conserve/entregue rondas históricas; -- un archivo de releases, local o remoto (abajo); o +- un archivo de releases con todas las rondas de la cadena, local o remoto (abajo); +- un servicio de caché, de DateKeys o de otros, que guarde de forma continua los releases de todas las rondas y los sirva; o - una Release Cache válida conservada por otra fuente. El protocolo NO debe asumir silenciosamente que cualquier proveedor conservará histórico indefinidamente. Una aplicación que prometa horizontes largos SHOULD documentar esta dependencia. La misma vale para el `.dkc`, que el protocolo no guarda (§6), y para el resto de un sobre guardado fuera (§44.1): una cápsula a décadas necesita que alguien conserve las tres cosas. -El release es la única de ellas que no existe al sellar: se publica en `round_time`. Por eso el SDK oficial lo guarda como `.dkr` en cuanto puede (§62.1, regla 28). En `time_only`, guardarlo junto al `.dkc` no cambia la confidencialidad: tras la fecha, el release es público. En `time_and_key`, el `.dkr` SHOULD ir también junto a una `.dkk` con localizador (§44.1): el mismo release abre el localizador de su sobre, y quien solo tiene la llave lo necesita para encontrar la cápsula. +El release es la única de ellas que no existe al sellar: se publica en `round_time`, así que quien crea la cápsula no puede guardarlo con ella. Y llegada la fecha, la cápsula ya se puede abrir: un release guardado entonces junto a ella solo sirve para volver a abrirla. El caso que importa es otro: quien abre la cápsula décadas después de su fecha, cuando drand quizá ya no exista y nadie guardó nada para ella. Por eso la recuperación a largo plazo descansa en archivos de releases y en servicios de caché que guardan, de forma continua, los releases de todas las rondas de la cadena, y no en el release de una cápsula concreta: guardar solo las rondas de cápsulas conocidas revelaría sus fechas y no se recomienda. + +Quien lee pide la ronda de su DateKey y verifica la firma con la clave pública del perfil pinneado, en el paso 10 de §63. No necesita confiar en quien la sirve: para una ronda solo hay una firma válida (§47.1). Un servicio de caché que se consulta por red es una fuente de red y conoce la ronda que se le pide (§49); un archivo leído en local, no. Un servicio de caché de DateKeys responde con el objeto release, como la Release API (§45); uno ajeno puede servir el JSON de drand, que un lector también acepta (§47.1). Este documento no promete que exista un archivo publicado ni un servicio de caché, ni dice dónde se alojan. -**Archivo de releases (informativo).** Un archivo guarda las firmas de un tramo de rondas de una cadena, para quien no guardó su `.dkr`. Su formato no es normativo ni tiene códigos de error propios: una cabecera en Deterministic CBOR con el perfil de §58, +**Archivo de releases (informativo).** Un archivo guarda las firmas de un tramo de rondas consecutivas de una cadena. Su formato no es normativo ni tiene códigos de error propios: una cabecera en Deterministic CBOR con el perfil de §58, ```text {0: "datekeys-release-archive", 1: 1, 2: chain_hash (32 bytes), 3: primera ronda, 4: número de rondas} ``` -seguida de las firmas, una tras otra, cada una de n bytes, la longitud de una firma de la cadena: 48 en Quicknet. La de la ronda r empieza en |cabecera| + (r − primera ronda)·n, y el fichero mide exactamente |cabecera| + número·n. Una ronda que falta se escribe con n ceros. La entrada de una ronda es el objeto release con el `chain_hash` de la cabecera, la ronda y esa firma, y se verifica en el paso 10 como un `.dkr`. Un archivo de otra cadena, de otra longitud o que no tiene la ronda no entrega ningún release: `ERR_RELEASE_UNAVAILABLE` en el paso 9. +seguida de las firmas, una tras otra, cada una de n bytes, la longitud de una firma de la cadena: 48 en Quicknet. La de la ronda r empieza en |cabecera| + (r − primera ronda)·n, y el fichero mide exactamente |cabecera| + número·n. Una ronda que falta se escribe con n ceros. La entrada de una ronda es el objeto release con el `chain_hash` de la cabecera, la ronda y esa firma, y se verifica en el paso 10 como cualquier otro objeto release. Un archivo de otra cadena, de otra longitud o que no tiene la ronda no entrega ningún release: `ERR_RELEASE_UNAVAILABLE` en el paso 9. -Leído en local, el archivo es un release en la mano (§49). Leído en remoto, por ejemplo con un rango de HTTP, es una fuente de red: revela la ronda a quien lo sirve. Quicknet publica una ronda cada 3 segundos, 10 512 000 al año: unos 505 MB de firmas al año, que no se comprimen, y unos 42 MB al mes. Un paquete por mes, publicado con su SHA-256, es un tamaño razonable. Este documento no promete que exista un archivo publicado ni dice dónde se aloja; un archivo que solo guardase las rondas de cápsulas conocidas revelaría sus fechas y no se recomienda. Vector: `testdata/releases/archive_1000_1004.bin`, de las rondas 1000 a 1004, con la 1002 y la 1003 a cero. +Leído en local, el archivo es un release en la mano (§49). Leído en remoto, por ejemplo con un rango de HTTP, es una fuente de red: revela la ronda a quien lo sirve. Quicknet publica una ronda cada 3 segundos, 10 512 000 al año: unos 505 MB de firmas al año, que no se comprimen, y unos 42 MB al mes. Un paquete por mes, publicado con su SHA-256, es un tamaño razonable. Vector: `testdata/releases/archive_1000_1004.bin`, de las rondas 1000 a 1004, con la 1002 y la 1003 a cero. --- @@ -2376,7 +2375,7 @@ Debe explicar: - Quicknet V1 no es post-cuántico, ni lo son las firmas y los sellos que lleve la cápsula (§7.7); - el ciphertext puede permanecer disponible durante años; - la seguridad futura depende del provider y de la criptografía subyacente; -- abrir la cápsula dentro de años exigirá el `.dkc`, el release de su ronda, que solo existe después de la fecha, y en `time_and_key` la `.dkk` (§50, §62.1). +- abrir la cápsula dentro de años exigirá el `.dkc`, el release de su ronda, que solo existe después de la fecha y habrá que obtener de drand, de un archivo de releases o de un servicio de caché, y en `time_and_key` la `.dkk` (§50, §62.1). El umbral temporal de la advertencia es política de producto, no parte de la semántica criptográfica del protocolo. @@ -2919,9 +2918,8 @@ Con firma o sello, además, MUST: Para la recuperación a largo plazo (v0.15), el SDK oficial SHOULD: -26. **Aviso.** Al sellar, decir que dentro de años harán falta el `.dkc`, el release de su ronda, que solo existe después de la fecha, y en `time_and_key` la `.dkk` (§50, §53). MAY programar un aviso local para `round_time`. +26. **Aviso.** Al sellar, decir que dentro de años harán falta el `.dkc`, en `time_and_key` la `.dkk`, y el release de su ronda, que solo existe después de la fecha: abrirla años después de esa fecha dependerá de que drand siga sirviendo la ronda o de que la conserve un archivo de releases o un servicio de caché (§50, §53). MAY programar un aviso local para `round_time`. 27. **Anexo.** Guardar junto al `.dkc` el texto del anexo de recuperación (§79), igual para toda cápsula y sin ningún dato de ella: no revela nada que no revele ya el magic `DKC1`. -28. **Release.** Después de `round_time`, la primera vez que tenga red, obtener el release, verificarlo con el paso 10 de §63 y guardarlo como `.dkr` junto al `.dkc` (§47.1), aunque no abra la cápsula, y en `time_and_key` también junto a una `.dkk` con localizador (§44.1, §50). Pedirlo revela la ronda al relay, como al abrir la cápsula: un cliente sin estado, como una página web (§7.10), solo lo pide cuando la persona lo pide. Un lector SHOULD ofrecer guardar el `.dkr` tras verificar un release y aceptar un `.dkr` como entrada. Nota informativa: longitudes en Quicknet. Con `age` estándar, un stanza X25519 mide 98 bytes, y el stanza tlock, 249 + d, con d el número de dígitos decimales de la ronda. Con c(n) = max(1, ⌈n / 65536⌉) y k stanzas X25519 (16 en los formatos 2 y 3): @@ -3195,7 +3193,7 @@ Precedencia: el código que se informa es el del primer paso que falla y, dentro La `.dkk` es una entrada distinta del `.dkc`. Una implementación MAY decodificarla al recibirla, antes del paso 1, pero MUST informar de cualquier error suyo —de trama, de schema o de sus campos— solo en el paso 9.a, en el orden de ese paso, y nunca si `access_policy` es `time_only`. -Un release en la mano también es una entrada distinta. Una implementación MAY decodificarlo al recibirlo, pero MUST informar de sus errores solo en el paso 10, después de los pasos 1 a 9: una cápsula inválida o sin credenciales da su propio código aunque el `.dkr` también sea inválido. +Un release en la mano también es una entrada distinta. Una implementación MAY decodificarlo al recibirlo, pero MUST informar de sus errores solo en el paso 10, después de los pasos 1 a 9: una cápsula inválida o sin credenciales da su propio código aunque el release en la mano también sea inválido. `PAYLOAD_AGE` comienza exactamente en: @@ -3623,7 +3621,7 @@ Ejemplos, reproducibles con los vectores oficiales o con los tests de la impleme | Objeto release de otra cadena y de otra ronda, en la mano | `ERR_PROFILE_MISMATCH`, paso 10 | | Objeto release de versión 2 y de 1025 bytes | `ERR_NON_CANONICAL_CBOR`, paso 10 | | Objeto release de versión 2 con una clave desconocida y otra cadena | `ERR_UNSUPPORTED_VERSION`, paso 10 | -| Cápsula `time_and_key` sin credenciales y un `.dkr` vacío | `ERR_ACCESS_REQUIRED`, paso 9 | +| Cápsula `time_and_key` sin credenciales y un objeto release vacío en la mano | `ERR_ACCESS_REQUIRED`, paso 9 | | Firma del release negada y U del stanza tlock con c0 + p | `ERR_RELEASE_INVALID`, paso 10 | | Dos identities: una desenvuelve un stanza de `INNER_ACCESS_AGE` y la otra dos | `ERR_POLICY_STRUCTURE_MISMATCH`, paso 13 | | `CONTROL_CBOR` con una extensión crítica desconocida y el `header_binding` de otra cabecera | `ERR_EXTENSION_CRITICAL_UNKNOWN`, paso 14 | @@ -3674,7 +3672,7 @@ Las versiones que definan la firma de autor y el sello de tiempo no cambiarán l Un lector de la v0.10 abre las cápsulas de un escritor de la v0.11: acepta el área de 32 KiB y la de 64 KiB (§29.2), da F1 a `alg` 1 y 2 y S1 a `seal_type` 2 (§29.7), e ignora la nota pública y la extensión `datekeys.capsule` (§54). La llave de palabras es un recipient X25519 más, así que la identity que se deriva abre la cápsula en cualquier lector (§38.1). Un lector de la v0.11 abre las cápsulas de la v0.10, con su área de 512 bytes. -La v0.15 no cambia ningún formato de `.dkc` ni de `.dkk`: añade el objeto release (§47.1), que sirve a toda cápsula de los tres formatos, también a las escritas antes. Cambia un veredicto: un release válido en la mano abre la cápsula aunque el reloj del lector sea anterior a `round_time`, donde la v0.14 daba `ERR_RELEASE_UNAVAILABLE` en el paso 9 (§63, paso 9.c). Con una fuente de red nada cambia. Un lector de la v0.14 no lee un `.dkr`, pero sí el JSON de drand de la misma ronda; un objeto release cuyo `chain_hash` no es el del perfil pinneado da `ERR_PROFILE_MISMATCH` en el paso 10. +La v0.15 no cambia ningún formato de `.dkc` ni de `.dkk`: añade el objeto release (§47.1), que sirve a toda cápsula de los tres formatos, también a las escritas antes. Cambia un veredicto: un release válido en la mano abre la cápsula aunque el reloj del lector sea anterior a `round_time`, donde la v0.14 daba `ERR_RELEASE_UNAVAILABLE` en el paso 9 (§63, paso 9.c). Con una fuente de red nada cambia. Un lector de la v0.14 no lee un objeto release, pero sí el JSON de drand de la misma ronda; un objeto release cuyo `chain_hash` no es el del perfil pinneado da `ERR_PROFILE_MISMATCH` en el paso 10. La v0.14 no cambia ningún formato ni ningún veredicto de una cápsula de Quicknet. Un Provider Profile de un scheme de drand distinto de `bls-unchained-g1-rfc9380` deja de pasar §12.1, con `ERR_UNKNOWN_PROFILE`: V1 no pinnea ninguno. @@ -3853,17 +3851,17 @@ GT en H2 release sources = una fuente de red verifica cada respuesta y no se le pide nada - antes de round_time; un release en la mano (.dkr, JSON de drand, - archivo local) no se compara con el reloj; sin release, por + antes de round_time; un release en la mano (objeto release, JSON de + drand, archivo local) no se compara con el reloj; sin release, por cualquier causa, ERR_RELEASE_UNAVAILABLE en el paso 9 y ningún otro código; los códigos del paso 10, para un release en la mano release object -= .dkr: Deterministic CBOR sin trama, de 1 a 1024 bytes, += Deterministic CBOR sin trama, de 1 a 1024 bytes, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, - 4: signature}; la cadena por su chain_hash; el JSON de drand, - solo como entrada; en el paso 10, capas, chain_hash, ronda y - firma + 4: signature}, sin extensión de fichero; la cadena por su + chain_hash; el JSON de drand, solo como entrada; en el paso 10, + capas, chain_hash, ronda y firma extension placement = un encoder no escribe una extensión fuera de los objetos y @@ -3934,13 +3932,15 @@ public note opaco, solo o dentro de otro fichero recovery -= puede obtener release directamente del provider; el .dkr junto al - .dkc es la vía principal; el archivo de releases es informativo; - anexo informativo para abrir sin software de DateKeys (§79) += puede obtener release directamente del provider; a largo plazo, + archivos de releases de todas las rondas y servicios de caché que + los sirven, sin promesa de alojamiento; el formato del archivo es + informativo; anexo informativo para abrir sin software de + DateKeys (§79) historical release availability -= dependencia explícita del horizonte de recuperación; el SDK guarda - el .dkr tras la fecha (§62.1) += dependencia explícita del horizonte de recuperación; el SDK avisa + al sellar y guarda el anexo junto al .dkc (§62.1) ``` --- @@ -3983,8 +3983,7 @@ El máximo de 16 credenciales (§39), L_MAX (§29.1) y los límites del formato Quedan fuera de la v0.15, como trabajo futuro que esta versión no especifica: -- la extensión `datekeys.release` de la `.dkk`, para guardar el release dentro de una llave con localizador (§44.1, §50), si hace falta; -- un archivo de releases publicado por el proyecto y su alojamiento (§50); +- un archivo de releases o un servicio de caché del proyecto, y su alojamiento (§50); - el sello del servicio de DateKeys (`seal_type` 1) y OpenTimestamps dentro de la cápsula (`seal_type` 3); - listas generales de firmas y de sellos, y roles y umbrales de firmantes; - borradores cifrados en disco, para que un organismo firme días después, y anexos cifrados para la misma ronda; @@ -4432,36 +4431,37 @@ La v0.14 escribe lo que la revisión de completitud del 6 de octubre de 2026 enc ### Cambios normativos de la v0.15 -La v0.15 cierra los dos puntos de trabajo futuro de §74 sobre la recuperación a largo plazo: el formato de un objeto de release y de su fuente de archivo, y el uso del reloj local en el paso 9.c de §63. Sigue el diseño del 6 de octubre de 2026 y las ocho decisiones de su autor. No cambia ningún formato de `.dkc` ni de `.dkk`; añade el objeto release, y cambia un veredicto: un release en la mano abre la cápsula aunque el reloj vaya atrasado (§70). +La v0.15 cierra los dos puntos de trabajo futuro de §74 sobre la recuperación a largo plazo: el formato de un objeto de release y de su fuente de archivo, y el uso del reloj local en el paso 9.c de §63. Sigue el diseño del 6 de octubre de 2026, las ocho decisiones de su autor y su corrección del 7 de octubre, que quitó el fichero `.dkr` (cambio 4). No cambia ningún formato de `.dkc` ni de `.dkk`; añade el objeto release, y cambia un veredicto: un release en la mano abre la cápsula aunque el reloj vaya atrasado (§70). -1. **El objeto release** (§8, §20, §45, §47, §47.1, §57, §69.1, `datekeys.cddl`). - - Cambio: el release de una ronda tiene un formato, el objeto release: Deterministic CBOR sin trama, de 1 a 1024 bytes, `{0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}`, que se guarda en un fichero `.dkr`. Es el `release_material` de §47 y la respuesta de la Release API. Un lector SHOULD aceptar además el JSON de drand como entrada, pero un escritor no lo guarda. - - Motivo: §47 dejaba `release_material` sin definir, y §74 dejaba su formato como trabajo futuro. Una persona no tenía cómo guardar el release que abre su cápsula, y sin él la cápsula depende de que un relay conserve la ronda (§50). El `chain_hash` identifica la cadena como la identifica drand; CBOR sin trama, como el Provider Profile, usa el codec que ya tiene cada implementación. - - Caso: `testdata/releases/1000.dkr`, de 111 bytes, abre `time_only.dkc` con `datekeys decrypt -release`, sin red; el JSON de drand de la ronda 1000 da el mismo release sin nombrar la cadena. - - Pruebas previstas: `release.json` (§64), nuevo, y los cuatro `.dkr` de `testdata/releases`. +1. **El objeto release** (§8, §45, §47, §47.1, §57, §69.1, `datekeys.cddl`). + - Cambio: el release de una ronda tiene un formato, el objeto release: Deterministic CBOR sin trama, de 1 a 1024 bytes, `{0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}`. Es la respuesta de la Release API, el `release_material` de una Release Cache (§47) y la forma de un release que el llamador da desde un fichero o desde un archivo de releases; no tiene extensión de fichero propia. Un lector SHOULD aceptar además el JSON de drand como entrada, pero una Release Cache y la Release API no lo guardan ni lo sirven así. + - Motivo: §47 dejaba `release_material` sin definir, y §74 dejaba su formato como trabajo futuro. Una caché, un archivo o un servicio no tenían un formato común para guardar y servir el release de una ronda, y sin ellos una cápsula depende de que un relay conserve la ronda (§50). El `chain_hash` identifica la cadena como la identifica drand; CBOR sin trama, como el Provider Profile, usa el codec que ya tiene cada implementación. + - Caso: `testdata/releases/1000.cbor`, de 111 bytes, abre `time_only.dkc` con `datekeys decrypt -release`, sin red; el JSON de drand de la ronda 1000 da el mismo release sin nombrar la cadena. + - Pruebas previstas: `release.json` (§64), nuevo, y los cuatro objetos de `testdata/releases`. 2. **La cadena del release en el paso 10** (§51, §63 paso 10, §69.1). - Cambio: el paso 10 empieza por las capas del objeto release y compara su `chain_hash` con el del perfil pinneado, con `ERR_PROFILE_MISMATCH`, antes que la ronda y la firma. No hay ningún código nuevo. - - Motivo: un `.dkr` dice de qué cadena es, y un objeto que se contradice no se acepta, como el chain hash del stanza tlock en el paso 8. + - Motivo: un objeto release dice de qué cadena es, y un objeto que se contradice no se acepta, como el chain hash del stanza tlock en el paso 8. - Caso: el objeto de la ronda 1000 con un bit de su `chain_hash` cambiado y la firma publicada de Quicknet: sin la comparación abriría `time_only.dkc`, y con ella da `ERR_PROFILE_MISMATCH` en el paso 10. - Pruebas previstas: `mutations.json`, «release object of another chain» y «release object of another chain and another round, with a clock behind»; `release.json`. 3. **El paso 9.c y el release en la mano** (§17, §49, §51, §63 paso 9, §69.1, §70, §73). - - Cambio: 9.c solo se aplica antes de una petición de red. Un release en la mano —un `.dkr`, el JSON de drand que se guardó o la entrada de un archivo local— no se compara con el reloj, y el lector MAY avisar de que su reloj va atrasado. Con una fuente de red, la persona MAY pedir la petición antes de `round_time`. + - Cambio: 9.c solo se aplica antes de una petición de red. Un release en la mano —un objeto release o el JSON de drand en un fichero, o la entrada de un archivo local— no se compara con el reloj, y el lector MAY avisar de que su reloj va atrasado. Con una fuente de red, la persona MAY pedir la petición antes de `round_time`. - Motivo: las razones de 9.c son de red: no hacer peticiones inútiles ni observables y fallar pronto. Para un release en la mano no vale ninguna: su firma prueba que la ronda se publicó, y una firma válida de una ronda futura solo es posible si drand está comprometido (§7.6), cuando el reloj ya no protege la confidencialidad. El reloj vetaba un release válido por un dato local que nadie puede verificar, lo contrario de §3: una pila del CMOS gastada o una máquina virtual dejaban la cápsula cerrada con el release en la mano. - Caso: «round not reached yet» de `mutations.json`: `time_only.dkc`, el release publicado de la ronda 1000 en la mano y el reloj 1 ns antes de su `round_time`. La v0.14 daba `ERR_RELEASE_UNAVAILABLE` en el paso 9; ahora se abre. Con una fuente de red sigue dando `ERR_RELEASE_UNAVAILABLE` en el paso 9, sin petición. - Pruebas previstas: `mutations.json`, con el campo nuevo `source` en cada caso, «round not reached yet», que pasa a abrir, y «round not reached yet, from a network source» y «release of another round, from a network source», nuevos. 4. **La recuperación a largo plazo** (§1, §4, §50, §53, §62.1, §79). - - Cambio: §50 nombra el `.dkr` junto al `.dkc` como vía principal, describe el archivo de releases como formato informativo y une el `.dkr` con la `.dkk` con localizador (§44.1); §53 avisa de que hará falta el release; §62.1 añade las reglas 26 a 28, SHOULD del SDK oficial: avisar al sellar, guardar el anexo junto al `.dkc` y guardar el `.dkr` tras la fecha; §79, nuevo, es un anexo informativo para abrir una cápsula sin software de DateKeys. + - Cambio: §50 basa la recuperación a largo plazo en archivos de releases con todas las rondas de la cadena y en servicios de caché, de DateKeys o de otros, que los guardan de forma continua y los sirven; quien lee pide su ronda y verifica la firma con la clave pinneada, sin confiar en quien la sirve. §50 no promete ningún alojamiento y describe el formato del archivo como informativo; §53 avisa de que hará falta el release; §62.1 añade las reglas 26 y 27, SHOULD del SDK oficial: avisar al sellar y guardar el anexo junto al `.dkc`; §79, nuevo, es un anexo informativo para abrir una cápsula sin software de DateKeys, con el release tomado de un archivo, de un servicio de caché o de cualquier copia. - Motivo: una cápsula a veinte años depende de que alguien conserve el release de su ronda, y quizá de un software que ya no exista (revisión de completitud, punto 6; revisión de la v0.13, punto 2.6). `tle` no acepta un release dado y `age` no acepta una file key: sin el anexo, abrir una cápsula exigía leer drand, kyber, tlock y age. - Caso: `scripts/recovery_check.sh` abre `format3_single` (`time_only`) y `format3_time_and_key_portable` (`time_and_key`) solo con lo que dice el anexo, una librería BLS12-381 y `age`, sin código de DateKeys, tlock ni drand, y obtiene sus ficheros. - Pruebas previstas: `scripts/recovery` y su test, que prohíbe importar esos módulos, sobre ocho fixtures de los tres formatos; `scripts/check.sh` ejecuta la comprobación. + - Descartado: el borrador del 6 de octubre guardaba el release en un fichero `.dkr` junto a la cápsula, y junto a una `.dkk` con localizador, con una extensión de fichero propia, la regla 28 de §62.1 (el SDK lo obtenía y lo guardaba tras la fecha) y las opciones `decrypt -save-release` y `datekeys release` de la CLI. El autor lo quitó el 7 de octubre: al crear la cápsula el release no existe, y llegada la fecha la cápsula ya se puede abrir, así que un release guardado junto a ella solo sirve para volver a abrirla y no cubre el caso real, el de quien la abre décadas después, cuando drand ya no existe y nadie guardó nada. El objeto release y el release en la mano se quedan. 5. **La Release API** (§45). - Cambio: §45 fija que la respuesta es el objeto release y que quien la usa es una fuente de red; la forma HTTP de la petición pasa a ser informativa. - Motivo: ningún servicio la ofrece, y la forma de la URL no decide nada de seguridad: el release se verifica siempre (§51). La revisión sugería pasarla a un anexo; el autor prefirió mantenerla con el objeto. - Caso: las implementaciones de referencia piden el release a los relays de drand (`provider/drand`), nunca a la Release API, así que su forma HTTP no tenía ningún caso que la fijara. - Pruebas previstas: ninguna; es texto. 6. **Trabajo futuro** (§74). - - Cambio: salen los dos puntos de arriba; entran la extensión `datekeys.release` de la `.dkk` y un archivo de releases publicado. - - Motivo: el autor eligió guardar por ahora solo el `.dkr`, y dejar el alojamiento del archivo para cuando haya un sitio propio. + - Cambio: salen los dos puntos de arriba; entra un archivo de releases o un servicio de caché del proyecto, con su alojamiento. + - Motivo: el autor deja el alojamiento del archivo y del servicio para cuando haya un sitio propio. La extensión `datekeys.release` de la `.dkk`, que el borrador del 6 de octubre dejaba como trabajo futuro para guardar el release dentro de una llave con localizador, sale con el `.dkr`: tampoco existe el release al escribir la llave, y añadirlo después de la fecha no ayuda a quien abre décadas más tarde. - Pruebas previstas: ninguna. --- @@ -4587,7 +4587,7 @@ La regla 27 de §62.1 recomienda al SDK oficial guardar este anexo junto al `.dk Hace falta: - el `.dkc`; -- el release de su ronda: el `.dkr` guardado junto a la cápsula (§47.1), o la firma de la ronda obtenida de un relay o de un archivo de releases (§50); +- el release de su ronda, de cualquier fuente: un relay de drand, un archivo de releases, un servicio de caché (§50) o cualquier copia. No hace falta confiar en quien lo da: se verifica con la clave pública de 79.1 (79.3); - en `time_and_key`, una credencial: la `.dkk`, la identity `age` de un recipient o las palabras de una llave de palabras (§38.1); - una librería de BLS12-381 con pairing y con el hash a G1 de RFC 9380, SHA-256, HMAC-SHA256, HKDF-SHA256 (RFC 5869), ChaCha20-Poly1305 (RFC 8439), un decodificador de CBOR y la herramienta `age` (§77) o una librería compatible. @@ -4639,7 +4639,7 @@ Le siguen `PUBLIC_HEADER`, de `PUBLIC_HEADER_LEN` bytes; `SEALED_CONTROL`, de `S ### 79.3 El release -Un `.dkr` es un mapa CBOR (§47.1): la clave 0 es `"datekeys-release"`; la 2, el `chain_hash`, que ha de ser el de 79.1; la 3, la ronda, que ha de ser la de la DateKey; y la 4, la firma, de 48 bytes. Un relay de drand la entrega como JSON, `{"round": …, "signature": "…"}`, con la firma en hexadecimal. Hoy se pide así, aunque las direcciones pueden cambiar: +Un objeto release es un mapa CBOR (§47.1): la clave 0 es `"datekeys-release"`; la 2, el `chain_hash`, que ha de ser el de 79.1; la 3, la ronda, que ha de ser la de la DateKey; y la 4, la firma, de 48 bytes. Así lo sirven la Release API y un servicio de caché. En un archivo de releases (§50), la firma de la ronda r son los 48 bytes que empiezan en |cabecera| + (r − primera ronda)·48, y 48 ceros si el archivo no la tiene. Un relay de drand la entrega como JSON, `{"round": …, "signature": "…"}`, con la firma en hexadecimal. Hoy se pide así, aunque las direcciones pueden cambiar: ```text GET https://api.drand.sh/v2/chains//rounds/ diff --git a/spec/README.md b/spec/README.md index a1b1884..6e94daf 100644 --- a/spec/README.md +++ b/spec/README.md @@ -67,12 +67,13 @@ the tlock IBE. Its §76 records each change with its case. - `DateKeys_Protocol_Specification_v0.15.md`: the draft v0.15, work in progress and not approved; the branch `v0.15` implements it. It covers the - long-term recovery of capsules: the release object, the content of a `.dkr` - file that keeps the release of a round next to its capsule (§47.1), with - its chain hash checked at step 10; step 9.c only before a network request, - so that a release in hand is not compared with the clock; the release - archive as an informative format (§50); what the writer and the SDK keep - (§62.1); and an informative annex (§79) to open a capsule without DateKeys + long-term recovery of capsules: the release object, the release of a + round as data, the answer of the Release API and an entry of a cache + (§47.1), with its chain hash checked at step 10; step 9.c only before a + network request, so that a release in hand is not compared with the clock; + long-term recovery resting on archives and cache services that keep the + releases of all rounds, with the release archive as an informative format + (§50); what the SDK warns about and keeps (§62.1); and an informative annex (§79) to open a capsule without DateKeys software. It changes no format of `.dkc` or `.dkk`, and one verdict: a valid release in hand opens a capsule with a clock behind its round time. Its §76 records each change with its case. diff --git a/spec/datekeys.cddl b/spec/datekeys.cddl index c134f2e..a9d5381 100644 --- a/spec/datekeys.cddl +++ b/spec/datekeys.cddl @@ -2,7 +2,7 @@ ; ; Normative companion of spec/DateKeys_Protocol_Specification_v0.15.md, the ; draft v0.15, not approved yet; spec-v0.14 tags the schemas of v0.14. The -; draft adds the release object of a .dkr file (spec section 47.1); the other +; draft adds the release object (spec section 47.1); the other ; schemas have not changed since v0.12. They include the three control ; versions: 1, of capsule format 1 (v0.8.2), 2, of format 2 (v0.9), and 3, of ; format 3, and the security and head objects of format 3. @@ -87,9 +87,9 @@ provider-profile = { 10 => bstr .size 32, ; genesis_seed } -; Spec section 47.1 (v0.15). The release of a round, the content of a .dkr -; file, the release_material of a Release Cache (section 47) and the answer -; of the Release API (section 45). It has no frame: an input of 0 bytes or of +; Spec section 47.1 (v0.15). The release of a round: the answer of the +; Release API (section 45), the release_material of a Release Cache (section +; 47) and a release the caller gives from a file or from an archive. It has no frame: an input of 0 bytes or of ; more than 1024 is ERR_NON_CANONICAL_CBOR before it is decoded, its layer 1 ; (spec section 69.1); a valid one is at most 165 bytes. A violation of this ; rule is ERR_NON_CANONICAL_CBOR, but a version other than 1, diff --git a/testdata/README.md b/testdata/README.md index 579909e..a7dd795 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -52,8 +52,8 @@ Conventions for every file: | `vectors/dk1.json` | canonical `dk1_` strings, and rejected encodings with their code | §18, §19, §66 | | `vectors/cbor.json` | the CBOR profile, and one block of vectors per schema, CONTROL_CBOR in the three formats | §58, CDDL | | `vectors/tlock_ibe.json` | H2 of the tlock IBE: the serialization of an element of GT | §63 step 11 | -| `vectors/release.json` | the release object, the content of a `.dkr` file, and drand's JSON, each with the result of step 10; the lookups of a local release archive | §47.1, §50, §63 step 10 (v0.15) | -| `releases/.dkr` | the release object of each published round of the tests: 1000, 1001, 1004 and 2000 | §47.1 (v0.15) | +| `vectors/release.json` | the release object and drand's JSON, each with the result of step 10; the lookups of a local release archive | §47.1, §50, §63 step 10 (v0.15) | +| `releases/.cbor` | the release object of each published round of the tests: 1000, 1001, 1004 and 2000 | §47.1 (v0.15) | | `releases/archive_1000_1004.bin` | a local release archive, the informative format of §50, of rounds 1000 to 1004, two of them missing | §50 (v0.15) | | `vectors/tlock_steps.json` | steps 10 and 11 for Quicknet value by value: the message of a round, its hash to G1, and the decryption of a tlock stanza with H2, H4, H3 and the file key | §63 steps 10 and 11 (v0.14) | | `vectors/padding.json` | the padding of formats 2 and 3: P for each content length L, and the length of PAYLOAD_AGE | §29.1 | @@ -321,8 +321,9 @@ writes them, give `0118eea9d5971745f71e3c94926f1717` and another FK_TIME. ## `vectors/release.json` and `releases/` -The release object of spec v0.15, §47.1: the release of a round as a file, -`.dkr`, that a person keeps next to the capsule. It is deterministic CBOR with +The release object of spec v0.15, §47.1: the release of a round as data, +the answer of the Release API, an entry of a Release Cache and a release the +caller gives from a file or from an archive. It is deterministic CBOR with the profile of §58, a map of five keys, all required: ```text @@ -333,10 +334,11 @@ the profile of §58, a map of five keys, all required: 4 → signature (1 to 96 bytes; 48 in Quicknet) ``` -The object of a round above 255 measures 111 bytes. `releases/.dkr` is -the object of each published round the fixtures use, 1000, 1001, 1004 and +The object of a round above 255 measures 111 bytes. `releases/.cbor` +is the object of each published round the fixtures use, 1000, 1001, 1004 and 2000, with the Quicknet chain hash: the release that opens each fixture, as a -file. +file. The protocol gives the object no file extension; `.cbor` is the generic +one of CBOR (RFC 8949). `release.json` has three lists: @@ -365,7 +367,7 @@ file. each round, one after another; a round the archive lacks is 48 zero bytes. This one holds rounds 1000 to 1004, and 1002 and 1003 are zeros. Each lookup gives a `round` and its `result`: `ok` with the `encoding` of the release - object the archive supplies, the `.dkr` of that round, or + object the archive supplies, `releases/.cbor` for that round, or `ERR_RELEASE_UNAVAILABLE` for a round the archive lacks or does not cover. The texts are those of the reference, for an implementation that wants to @@ -849,7 +851,8 @@ reading flow (`capsule.Open`, §63) must fail. cases only, is the chain the release object names when it is not the Quicknet chain. - `source` (v0.15): what kind of source answers, spec v0.15 §63 step 9: - - `supplied`: the release is in the caller's hand, as a `.dkr` would be. + - `supplied`: the release is in the caller's hand, as a release object + read from a file would be. The reader is given the release object of `release`, with the Quicknet chain hash unless `chain_hash` says another, and decodes and verifies it at step 10: one that breaks a rule of step 10 gets the code of step 10. diff --git a/testdata/releases/1000.dkr b/testdata/releases/1000.cbor similarity index 100% rename from testdata/releases/1000.dkr rename to testdata/releases/1000.cbor diff --git a/testdata/releases/1001.dkr b/testdata/releases/1001.cbor similarity index 100% rename from testdata/releases/1001.dkr rename to testdata/releases/1001.cbor diff --git a/testdata/releases/1004.dkr b/testdata/releases/1004.cbor similarity index 100% rename from testdata/releases/1004.dkr rename to testdata/releases/1004.cbor diff --git a/testdata/releases/2000.dkr b/testdata/releases/2000.cbor similarity index 100% rename from testdata/releases/2000.dkr rename to testdata/releases/2000.cbor diff --git a/testdata/vectors/release.json b/testdata/vectors/release.json index 51766bf..1d93c71 100644 --- a/testdata/vectors/release.json +++ b/testdata/vectors/release.json @@ -1,6 +1,6 @@ { "spec": "0.14", - "description": "The release object, the content of a .dkr file (spec v0.15, §47.1), and drand's JSON as the input of the caller, each checked against the pinned Quicknet profile and the round of a DateKey as step 10 of spec §63 checks a release that the caller supplies; and the lookups of a local release archive (spec v0.15, §50). See testdata/README.md.", + "description": "The release object (spec v0.15, §47.1), and drand's JSON as the input of the caller, each checked against the pinned Quicknet profile and the round of a DateKey as step 10 of spec §63 checks a release that the caller supplies; and the lookups of a local release archive (spec v0.15, §50). See testdata/README.md.", "profile": "datekeys:quicknet:v1", "objects": [ {