From 6fa2b54bd3bd7ba356c77bd265e6203711406223 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 30 Sep 2026 21:00:44 +0200 Subject: [PATCH] Format 3, step 8: fuzzing of format 3 and the SHA-256 of spec v0.10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Three fuzz targets, in scripts/fuzz.sh too: FuzzDecodeHead (a head that is accepted re-encodes to its input, and a rejection carries one normative code), FuzzEvaluateSecurity (verdicts of this version, X for both or for neither) and FuzzCheckPath (the rules of one entry and the decoder of the head agree on every path). About a million runs each, clean; scripts/check.sh 60s is clean. - Spec v0.10, section 67: the fixtures of format 3 exist, so "Serán ... (por implementar)" reads "Son ...", as for those of format 2. No rule changes. - spec/README.md: v0.10 approved by its author on 30 September 2026 and implemented on this branch, with the SHA-256 of its text; the tag spec-v0.10 waits for the author. Co-Authored-By: Claude Opus 5.5 --- capsule/fuzz_test.go | 61 +++++++++++++++++++ internal/pathrule/fuzz_test.go | 39 ++++++++++++ scripts/fuzz.sh | 3 + spec/DateKeys_Protocol_Specification_v0.10.md | 2 +- spec/README.md | 11 ++-- 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 internal/pathrule/fuzz_test.go diff --git a/capsule/fuzz_test.go b/capsule/fuzz_test.go index 7785de9..9e716e5 100644 --- a/capsule/fuzz_test.go +++ b/capsule/fuzz_test.go @@ -118,6 +118,67 @@ func FuzzDecodeControl(f *testing.F) { }) } +// FuzzDecodeHead: a head that DecodeHead accepts re-encodes to its input, +// and each rejection carries exactly one normative code (spec §29.4, §69.1). +func FuzzDecodeHead(f *testing.F) { + for _, name := range fixtureNames { + if fx := loadFixture(f, name); fx.Head != "" { + b, _ := hexDecode(fx.Head) + f.Add(b) + } + } + var heads testkit.HeadSchemaFile + if err := testkit.ReadJSON("../testdata/vectors/head_schema.json", &heads); err != nil { + f.Fatal(err) + } + for _, v := range heads.Heads { + b, _ := hexDecode(v.Hex) + f.Add(b) + } + f.Fuzz(func(t *testing.T, b []byte) { + h, err := capsule.DecodeHead(b, nil) + if err != nil { + if n := codes(err); n != 1 { + t.Fatalf("%d normative codes: %v", n, err) + } + return + } + if re, err := capsule.EncodeHead(h); err != nil || !bytes.Equal(re, b) { + t.Fatalf("accepted a head that does not re-encode to its input: %v", err) + } + }) +} + +// FuzzEvaluateSecurity: security never fails, and gives verdicts of this +// version, X for both the signature and the seal or for neither (spec §29.3, +// §29.7). +func FuzzEvaluateSecurity(f *testing.F) { + for _, name := range fixtureNames { + if fx := loadFixture(f, name); fx.Security != "" { + b, _ := hexDecode(fx.Security) + f.Add(b) + } + } + var security testkit.SecurityVectorFile + if err := testkit.ReadJSON("../testdata/vectors/security.json", &security); err != nil { + f.Fatal(err) + } + for _, v := range security.Vectors { + b, _ := hexDecode(v.Hex) + f.Add(b) + } + f.Fuzz(func(t *testing.T, b []byte) { + v := capsule.EvaluateSecurity(b) + switch { + case v.Signature != capsule.VerdictUnreadable && v.Signature != capsule.VerdictNoSignature && v.Signature != capsule.VerdictSignatureUnchecked, + v.Seal != capsule.VerdictUnreadable && v.Seal != capsule.VerdictNoSeal && v.Seal != capsule.VerdictSealUnsupported && v.Seal != capsule.VerdictSealUnreadable, + (v.Signature == capsule.VerdictUnreadable) != (v.Seal == capsule.VerdictUnreadable), + len(v.Lines()) == 0: + t.Fatalf("verdicts %+v", v) + } + }) +} + // FuzzInspect feeds whole capsules to the pre-unlock validation, and to Open // with a source that never has the release: a capsule that Inspect rejects // must not cause a request, and nothing may pass the release step. The diff --git a/internal/pathrule/fuzz_test.go b/internal/pathrule/fuzz_test.go new file mode 100644 index 0000000..9bf7bfc --- /dev/null +++ b/internal/pathrule/fuzz_test.go @@ -0,0 +1,39 @@ +package pathrule_test + +import ( + "errors" + "testing" + "unicode/utf8" + + "g.activething.com/go/DateKeys/internal/pathrule" + "g.activething.com/go/DateKeys/internal/testkit" +) + +// FuzzCheckPath: the rules of one entry and the decoder of the head agree on +// every path: a head that holds it alone decodes exactly when the path is +// valid UTF-8 of 1 to 1024 bytes (R1, layer 3) that CheckPath accepts, since +// one path collides with none and makes at most 31 folders (spec §29.5). +func FuzzCheckPath(f *testing.F) { + var paths testkit.PathVectorFile + if err := testkit.ReadJSON("../../testdata/vectors/paths.json", &paths); err != nil { + f.Fatal(err) + } + for _, v := range paths.Paths { + f.Add(v.Path) + } + f.Fuzz(func(t *testing.T, p string) { + err := pathrule.CheckPath(p) + var e *pathrule.Error + if err != nil && !errors.As(err, &e) { + t.Fatalf("an error that is not a pathrule.Error: %v", err) + } + valid := err == nil && utf8.ValidString(p) && len(p) >= 1 && len(p) <= pathrule.MaxPathLen + head, merr := testkit.TreeHead([]string{p}) + if merr != nil { + return + } + if got, _ := testkit.HeadResult(head); (got == testkit.ResultOK) != valid { + t.Fatalf("path %+q: CheckPath %v, the head decodes to %s", p, err, got) + } + }) +} diff --git a/scripts/fuzz.sh b/scripts/fuzz.sh index 7d34e75..dbcf2b0 100644 --- a/scripts/fuzz.sh +++ b/scripts/fuzz.sh @@ -25,6 +25,9 @@ targets=( "./capsule FuzzParsePrelude" "./capsule FuzzDecodeHeader" "./capsule FuzzDecodeControl" + "./capsule FuzzDecodeHead" + "./capsule FuzzEvaluateSecurity" + "./internal/pathrule FuzzCheckPath" "./capsule FuzzInspect" "./capsule FuzzEncodeImpliesDecode" ) diff --git a/spec/DateKeys_Protocol_Specification_v0.10.md b/spec/DateKeys_Protocol_Specification_v0.10.md index 79731ae..957181e 100644 --- a/spec/DateKeys_Protocol_Specification_v0.10.md +++ b/spec/DateKeys_Protocol_Specification_v0.10.md @@ -2883,7 +2883,7 @@ Los de formato 3 cubren, como mínimo: - una firma de `alg` 1 con una clave de 32 bytes y una firma de 64, aleatorias, con el veredicto F1; - esa firma y un sello de `seal_type` 1 con un token aleatorio, con F1 y S1. -Serán `format3_single.dkc`, `format3_tree.dkc`, `format3_comment_only.dkc`, `format3_bloque256.dkc`, `format3_time_and_key_portable.dkc` con su `.dkk`, `format3_area_1024.dkc`, `format3_security_v2.dkc`, `format3_signature_unsupported.dkc` y `format3_seal_unsupported.dkc` (por implementar). Los vectores de rutas y del head van en `testdata/vectors/paths.json`, `head_schema.json` y `path_fold.json`, y los de `security`, en `security.json`. +Son `format3_single.dkc`, `format3_tree.dkc`, `format3_comment_only.dkc`, `format3_bloque256.dkc`, `format3_time_and_key_portable.dkc` con su `.dkk`, `format3_area_1024.dkc`, `format3_security_v2.dkc`, `format3_signature_unsupported.dkc` y `format3_seal_unsupported.dkc`. Los vectores de rutas y del head van en `testdata/vectors/paths.json`, `head_schema.json` y `path_fold.json`, y los de `security`, en `security.json`. Los vectores de rutas y del head cubren, como mínimo: U+00A0 y U+3000 al principio y al final de un segmento, con el veredicto que dan las tablas, que para U+3000 es el rechazo por R6c; best-fit; alias 8.3, «~1» incluido; Cn; `["b/..", "a"]`; U+206A a U+206F, las etiquetas y otros ignorables fuera de la lista blanca; «.» seguido de ZWJ y un segmento hecho solo de ZWJ; 127 veces «ΐ»; U+F03A; «.datekeys-x» en el primer nivel y en otro; el orden de U+FF5E y U+1F600; «ab» con y sin ZWNJ; «¿», «§» y «♥», que se aceptan; VS16 tras un emoji que lo admite, que se acepta, y tras «a», que no; ZWJ al principio, al final y dos seguidos; y la bandera arcoíris, que se acepta, y la de Escocia, que no. Los de `head_schema.json` cubren además un comentario con etiquetas y con selectores de variante sueltos. Los de `security.json` cubren: el mapa exterior con la clave 2 que no es una cadena de bytes, con una clave 4 o con un byte de más dentro de `SECURITY_LEN` (X); `alg` 0 o una clave vacía dentro de la clave 2 (F1, con el sello intacto); y un `seal` que incumple su schema con un `seal_type` desconocido (S2). diff --git a/spec/README.md b/spec/README.md index f182745..79c70e7 100644 --- a/spec/README.md +++ b/spec/README.md @@ -14,16 +14,19 @@ reproducible cases in the specification's §76. - `DateKeys_Protocol_Specification_v0.9.md`: frozen copy of the normative draft v0.9 (29 September 2026), approved by its author on that date and - tagged `spec-v0.9`; this module implements it. SHA-256: + tagged `spec-v0.9`. SHA-256: `36189e1e62f0f835b7665219aa63df7200cd89ac4e4e924f2705f970fa7c40a9`. It adds capsule format 2, which hides until the unlock date the exact length of the content (the payload is padded) and the number of credentials (always 16 X25519 stanzas), and it makes the writer rules normative. A v0.9 reader still opens format 1, the format of v0.8.2. Its §76 records each change with its reproducible case. -- `DateKeys_Protocol_Specification_v0.10.md`: working draft v0.10 (30 - September 2026), not yet approved by its author and not implemented. It - adds capsule format 3, which stores several files with their paths, sizes, +- `DateKeys_Protocol_Specification_v0.10.md`: normative draft v0.10 (30 + September 2026), approved by its author on that date; this module + implements it on the branch `v0.10`, and it is tagged `spec-v0.10` once its + author authorizes the tag. SHA-256: + `7f26419a444aa3e89a3aa8afbbba9d952af69e048aee1e93cd70732c2d1d99d1`. + It adds capsule format 3, which stores several files with their paths, sizes, hashes and dates, encrypted, and reserves the `security` area for an author signature and a timestamp seal that later versions will define without changing the format. Its §76 records each change with its reproducible case.