package testkit import ( "encoding/json" "os" "path/filepath" "regexp" ) // FixtureStanza is the visible part of an age stanza in a fixture. type FixtureStanza struct { Type string `json:"type"` Args []string `json:"args"` } // FixtureRelease is the release a fixture opens with. type FixtureRelease struct { Round uint64 `json:"round"` Signature string `json:"signature"` } // FixtureStage is the expected result of one step of spec §63. type FixtureStage struct { Step int `json:"step"` Name string `json:"name"` OK bool `json:"ok"` Error string `json:"error,omitempty"` } // DKCFixture holds the expected values of an official .dkc fixture (spec §67). type DKCFixture struct { Description string `json:"description"` Spec string `json:"spec"` // Format is the capsule format, the VERSION of the PRELUDE (spec §22). Format int `json:"format"` File string `json:"file"` SHA256 string `json:"sha256"` Release FixtureRelease `json:"release"` Prelude string `json:"prelude"` PublicHeader string `json:"public_header"` DateKey string `json:"datekey"` CapsuleID string `json:"capsule_id"` AccessPolicy string `json:"access_policy"` Structure string `json:"structure"` UnlockAt string `json:"unlock_at"` HeaderBinding string `json:"header_binding"` OuterStanzas []FixtureStanza `json:"outer_stanzas"` PayloadStanzas []FixtureStanza `json:"payload_stanzas"` InnerStanzas []FixtureStanza `json:"inner_stanzas,omitempty"` // AccessKeyStanza and IdentityStanzas are, in a format 2 time_and_key // fixture, the index in InnerStanzas of the stanza each credential // opens: the .dkk, and each identity of Identities in order. The stanzas // no credential opens are dummies. Official vectors are the only place // where this is recorded (spec §39, §67). AccessKeyStanza *int `json:"access_key_stanza,omitempty"` IdentityStanzas []int `json:"identity_stanzas,omitempty"` AccessKeyFile string `json:"access_key_file,omitempty"` Identities []string `json:"identities,omitempty"` ControlCBOR string `json:"control_cbor"` PayloadIdentity string `json:"payload_identity"` // PayloadLength is L, the length of the content: the plaintext the // reader delivers in formats 1 and 2, and BODY in format 3, whose files // Files describes. In formats 2 and 3 the plaintext of PAYLOAD_AGE is // PaddedLength bytes, P = rule(L), the rule being Padding (spec §29.1, // §29.2). PlaintextFile holds those L bytes. PayloadLength uint64 `json:"payload_length"` Padding int `json:"padding,omitempty"` PaddedLength uint64 `json:"padded_length,omitempty"` PlaintextFile string `json:"plaintext_file"` PlaintextSHA256 string `json:"plaintext_sha256"` HeaderExtensions []FixtureExt `json:"header_extensions,omitempty"` ControlExt []FixtureExt `json:"control_extensions,omitempty"` // The fields of format 3 (spec §29.2 to §29.7): the size of the security // area, SECURITY_CBOR and HEAD_CBOR, the salt, the comment and the // declared author, the extensions of the head, the offset of CONTENT in // BODY, 12 + AREA_LEN + HEAD_LEN, each file, and the verdicts. AreaLen uint32 `json:"area_len,omitempty"` Security string `json:"security_cbor,omitempty"` Head string `json:"head_cbor,omitempty"` Salt string `json:"salt,omitempty"` Comment string `json:"comment,omitempty"` Author string `json:"declared_author,omitempty"` HeadExtensions []FixtureExt `json:"head_extensions,omitempty"` ContentOffset uint64 `json:"content_offset,omitempty"` Files []FixtureFile `json:"files,omitempty"` Verdicts *FixtureVerdicts `json:"verdicts,omitempty"` // Signature is, in a fixture signed with alg 1, what a second // implementation needs to check the signature and to make it again // (spec v0.11, §29.8, §29.9). Signature *FixtureSignature `json:"signature,omitempty"` Stages []FixtureStage `json:"stages"` } // FixtureFile is a file of a format 3 fixture: its entry of the head. Its // bytes are those of BODY from ContentOffset + Start to ContentOffset + End. type FixtureFile struct { Path string `json:"path"` Size uint64 `json:"size"` Start uint64 `json:"start"` End uint64 `json:"end"` SHA256 string `json:"sha256"` MTime *uint64 `json:"mtime,omitempty"` } // FixtureVerdicts are the verdicts of the security area of a format 3 // fixture, and the lines that show them (spec §29.7). type FixtureVerdicts struct { Signature string `json:"signature"` Seal string `json:"seal"` Lines []string `json:"lines"` // AuthorKey is the dkauthor1… key of a valid signature (F3, F4). AuthorKey string `json:"author_key,omitempty"` } // FixtureSignature describes the signature of alg 1 of a format 3 fixture. // SecretSeed is the 32-byte seed of a test key made for it: Ed25519 is // deterministic, so signing AuthorMessage with it gives SignatureValue // again. ControlCommit, HeadDigest and SignersDigest are the commitments of // spec §29.8, in hexadecimal; AuthorMessage is the ASCII text that is // signed, and AuthorCode its code. SecurityKey2 is the exact content of key // 2 of SECURITY_CBOR, in hexadecimal. type FixtureSignature struct { Alg int `json:"alg"` SecretSeed string `json:"secret_seed"` AuthorKey string `json:"author_key"` ControlCommit string `json:"control_commit"` HeadDigest string `json:"head_digest"` SignersDigest string `json:"signers_digest"` AuthorMessage string `json:"author_message"` AuthorCode string `json:"author_code"` SignatureValue string `json:"signature"` SecurityKey2 string `json:"security_key_2"` } // FixtureExt is an extension in a fixture. type FixtureExt struct { Critical bool `json:"critical"` ID string `json:"id"` Version uint64 `json:"version"` Data string `json:"data,omitempty"` // hex of the exact data bytes; absent without data } // DKKFixture holds the expected values of an official .dkk fixture (spec §68). type DKKFixture struct { Description string `json:"description"` Spec string `json:"spec"` File string `json:"file"` SHA256 string `json:"sha256"` CredentialID string `json:"credential_id"` CapsuleID string `json:"capsule_id"` AccessType string `json:"access_type"` Material string `json:"access_material"` CapsuleDigest string `json:"capsule_digest,omitempty"` Extensions []FixtureExt `json:"extensions,omitempty"` Capsule string `json:"capsule"` ExpectedResult string `json:"expected_result"` Stages []FixtureStage `json:"stages,omitempty"` } // ReadJSON decodes a JSON file. func ReadJSON(path string, v any) error { b, err := os.ReadFile(path) if err != nil { return err } return json.Unmarshal(b, v) } // WriteJSON writes v as indented JSON with a trailing newline. func WriteJSON(path string, v any) error { b, err := json.MarshalIndent(v, "", " ") if err != nil { return err } return writeFile(path, append(b, '\n')) } // editPattern is an Edit as json.MarshalIndent spreads it over five lines. var editPattern = regexp.MustCompile(`\[\n\s*(\d+),\n\s*(\d+),\n\s*("[0-9a-f]*")\n\s*\]`) // WriteJSONEdits is WriteJSON with every Edit, [at, delete, "hex"], on one // line. func WriteJSONEdits(path string, v any) error { b, err := json.MarshalIndent(v, "", " ") if err != nil { return err } return writeFile(path, append(editPattern.ReplaceAll(b, []byte("[$1, $2, $3]")), '\n')) } func writeFile(path string, b []byte) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } return os.WriteFile(path, b, 0o644) }