You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/capsule/mutation_test.go

118 lines
3.8 KiB

package capsule_test
import (
"bytes"
"context"
"testing"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/testkit"
)
const mutationsFile = "../testdata/vectors/mutations.json"
// build returns a capsule made by testkit.Build and the options to open it.
func build(t *testing.T, b testkit.Build) ([]byte, capsule.OpenOptions) {
t.Helper()
if b.Plaintext == nil {
b.Plaintext = []byte("malicious creator")
}
out, err := b.Make()
if err != nil {
t.Fatal(err)
}
return out.DKC, capsule.OpenOptions{Registry: testkit.Registry(), Source: testkit.NewSource(testkit.Release(1000)), Now: testkit.Fixed(testkit.Genesis().AddDate(1, 0, 0))}
}
// Spec §64: every mutation of the corpus (testkit.Mutations), built afresh,
Format 3, step 6d: the mutations of format 3 The mutation corpus of spec 64 gains format 3: 209 cases, 169 of the spec. - The 33 mutations of the first two lists on format3_single and format3_time_and_key_portable, named "format 3: ...", with a sibling written by EncryptFiles and built capsules that hold a BODY. - The list of format 3, 47 cases: one for each value of a line with several (AREA_LEN 0, 511, 513 and 66048; SECURITY_LEN 0 and 513; HEAD_LEN 0 and 2^24 + 1), and the three that open without a code, with the verdicts X, F1 and S1. VERSION 4 is "format 3: version changed", as in format 2. Each seals BODY again with FK_PAYLOAD and the nonce of its fixture, and the control with the new L when L changes; the two that need a head followed by another STREAM chunk derive from format3_tree, whose comment takes the bytes the path loses so that only the head and its chunk change. - Further cases: format 3 relabeled 1, and time_and_key relabeled 2 with the identity and with the .dkk. - A mutation may expect the capsule to open with its verdicts; the exported case records them, with the result ok at step 0. - Splice gives an edit for each run of changed bytes, runs closer than 16 bytes merged, and one more for what one side has beyond the other: a head sealed again changes its bytes and the tag of its chunk, 64 KiB apart. Earlier cases are written with more edits and give the same capsules. The corpus is 706 KB, 476 KB of them the capsule of 65536 implicit folders, whose head is 235 KB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// fails with its exact normative error at its exact step, or opens with its
// verdicts, and a failure before the release request causes no request. The
// thirty-three mutations of the first two lists of spec §64 are there once
// for each format, and those of the lists of formats 2 and 3 once.
func TestMutationCorpus(t *testing.T) {
e, err := testkit.NewMutationEnv(fixtureDir)
if err != nil {
t.Fatal(err)
}
n := 0
for _, m := range testkit.Mutations() {
if m.Spec {
n++
}
t.Run(m.Name, func(t *testing.T) {
in, err := m.Make(e)
if err != nil {
t.Fatal(err)
}
if err := m.Check(in); err != nil {
t.Fatal(err)
}
})
}
if want := 3*testkit.SpecMutationsPerFormat + testkit.Format2SpecMutations + testkit.Format3SpecMutations + testkit.V011SpecMutations; n != want {
Implement capsule format 2 of spec v0.9 The reference moves to the DateKeys Protocol Specification v0.9, approved by its author on 29 September 2026. Encrypt writes capsule format 2 only; Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the verdict v0.8.2 gave it. Format 2 (spec §22, §29.1, §31, §39): - VERSION in the PRELUDE is the capsule format, capsule.Format; any other value is ERR_UNSUPPORTED_VERSION at step 2. - CONTROL_CBOR has the schema version of its format. Version 2 adds key 6, payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without extensions, whatever L. - The payload is the content padded with zeros to P = rule(L). Step 17 checks the length and the zeros, and Open writes only the first L bytes. - INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials, and a dummy in each slot left, in a uniformly random order. Writer rules (spec §62.1): EncryptOptions.Length is required and the source must deliver exactly that many bytes; recipients that are not canonical or of low order are rejected (agewrap.CheckX25519Recipient); self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE. The CLI measures its input, takes -padding and reports the format. Test data: seven format 2 fixtures, padding vectors checked against math/big, format 2 CBOR vectors, and the mutation corpus in both formats with the 22 cases of the third list of spec §64, built without randomness by sealing the fixtures again with their known keys and nonces. The format 1 fixtures are kept byte for byte and never regenerated; the differential corpus keeps its 1825 cases and adds a block per format 2 fixture. The spec copy loses its "to be implemented" markers, and the READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
t.Fatalf("spec §64 gives %d mutations, the corpus has %d", want, n)
}
}
// The exported corpus, testdata/vectors/mutations.json, holds every mutation
// of the corpus in order, and replaying each case from the file alone gives
// exactly the recorded error and step. The capsules that are not built with
// randomness are the ones the corpus derives from the fixtures.
func TestExportedMutationCorpus(t *testing.T) {
var f testkit.MutationFile
if err := testkit.ReadJSON(mutationsFile, &f); err != nil {
t.Fatal(err)
}
e, err := testkit.NewMutationEnv(fixtureDir)
if err != nil {
t.Fatal(err)
}
muts := testkit.Mutations()
if len(f.Cases) != len(muts) {
t.Fatalf("%d exported cases, the corpus has %d mutations", len(f.Cases), len(muts))
}
for i, c := range f.Cases {
m := muts[i]
t.Run(c.Name, func(t *testing.T) {
Format 3, step 6d: the mutations of format 3 The mutation corpus of spec 64 gains format 3: 209 cases, 169 of the spec. - The 33 mutations of the first two lists on format3_single and format3_time_and_key_portable, named "format 3: ...", with a sibling written by EncryptFiles and built capsules that hold a BODY. - The list of format 3, 47 cases: one for each value of a line with several (AREA_LEN 0, 511, 513 and 66048; SECURITY_LEN 0 and 513; HEAD_LEN 0 and 2^24 + 1), and the three that open without a code, with the verdicts X, F1 and S1. VERSION 4 is "format 3: version changed", as in format 2. Each seals BODY again with FK_PAYLOAD and the nonce of its fixture, and the control with the new L when L changes; the two that need a head followed by another STREAM chunk derive from format3_tree, whose comment takes the bytes the path loses so that only the head and its chunk change. - Further cases: format 3 relabeled 1, and time_and_key relabeled 2 with the identity and with the .dkk. - A mutation may expect the capsule to open with its verdicts; the exported case records them, with the result ok at step 0. - Splice gives an edit for each run of changed bytes, runs closer than 16 bytes merged, and one more for what one side has beyond the other: a head sealed again changes its bytes and the tag of its chunk, 64 KiB apart. Earlier cases are written with more edits and give the same capsules. The corpus is 706 KB, 476 KB of them the capsule of 65536 implicit folders, whose head is 235 KB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
if c.Name != m.Name || c.Spec != m.Spec || c.Error != m.Code() || c.Step != m.Step || c.Network != m.Network || c.Frozen != m.Random ||
(c.Verdicts != nil) != (m.Verdicts != nil) {
t.Fatalf("exported case %+v does not match mutation %q", c, m.Name)
}
if err := c.Check(fixtureDir); err != nil {
t.Fatal(err)
}
if m.Random {
if c.DKC.Base != "" {
t.Fatal("a frozen capsule must not depend on a fixture")
}
return
}
want, err := m.Make(e)
if err != nil {
t.Fatal(err)
}
got, err := c.Input(fixtureDir)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got.DKC, want.DKC) || !bytes.Equal(got.DKK, want.DKK) {
t.Fatal("the exported capsule or .dkk differs from the one the mutation derives")
}
})
}
}
// Known critical extensions are accepted when the application declares them.
func TestKnownCriticalExtensions(t *testing.T) {
crit := []extension.Extension{{ID: "org.example.must-understand", Version: 1}}
for _, b := range []testkit.Build{{HeaderCritical: crit}, {ControlCritical: crit}} {
dkc, o := build(t, b)
o.Extensions = extension.Set{"org.example.must-understand": {1}}
var out bytes.Buffer
if _, err := capsule.Open(context.Background(), &out, bytes.NewReader(dkc), o); err != nil || out.String() != "malicious creator" {
t.Fatalf("known critical extension rejected: %v", err)
}
}
}

Powered by TurnKey Linux.