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/internal/testkit/fixture.go

231 lines
9.4 KiB

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 {
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
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"`
Format 3, step 6b: the nine fixtures of format 3 The fixtures of spec 67 for format 3, each with its record, its BODY, its inspect output and, for time_and_key, its .dkk: - format3_single, format3_tree (five files in three folders, one over two STREAM chunks, one without mtime, a comment and a declared author), format3_comment_only (no files; a TAB in the comment), format3_bloque256 and format3_time_and_key_portable, written with EncryptFiles; - format3_area_1024, format3_security_v2 (verdict X), format3_signature_unsupported (an author-signature of alg 1 with a random key of 32 bytes and a random signature of 64: F1) and format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1), which only a generator of test vectors writes (62.1 rule 13), built with testkit.Build. The record of a format 3 fixture adds the area, SECURITY_CBOR, HEAD_CBOR, the salt, the comment, the declared author, the head extensions, the offset of CONTENT in BODY, each file with its layout, SHA-256 and mtime, and the verdicts with their lines; its plaintext file is BODY. The generator writes, then recovers every value by opening layer by layer for the three formats alike, and refreshes the records of format 3 through a Sink. Tests: the conformance test checks BODY, the head, security and every file, and opens through a MemorySink; the .dkk tests take the .dkk of formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures into folders; the control fuzz target decodes with the three schema versions. The differential corpus gains two bases, format3_single and format3_time_and_key_portable, one per policy: 5110 cases, the earlier ones unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// 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"`
// Seal is, in a fixture with a valid time seal, what a second
// implementation needs to check it (spec v0.11, §29.11).
Seal *FixtureSeal `json:"seal,omitempty"`
Stages []FixtureStage `json:"stages"`
Format 3, step 6b: the nine fixtures of format 3 The fixtures of spec 67 for format 3, each with its record, its BODY, its inspect output and, for time_and_key, its .dkk: - format3_single, format3_tree (five files in three folders, one over two STREAM chunks, one without mtime, a comment and a declared author), format3_comment_only (no files; a TAB in the comment), format3_bloque256 and format3_time_and_key_portable, written with EncryptFiles; - format3_area_1024, format3_security_v2 (verdict X), format3_signature_unsupported (an author-signature of alg 1 with a random key of 32 bytes and a random signature of 64: F1) and format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1), which only a generator of test vectors writes (62.1 rule 13), built with testkit.Build. The record of a format 3 fixture adds the area, SECURITY_CBOR, HEAD_CBOR, the salt, the comment, the declared author, the head extensions, the offset of CONTENT in BODY, each file with its layout, SHA-256 and mtime, and the verdicts with their lines; its plaintext file is BODY. The generator writes, then recovers every value by opening layer by layer for the three formats alike, and refreshes the records of format 3 through a Sink. Tests: the conformance test checks BODY, the head, security and every file, and opens through a MemorySink; the .dkk tests take the .dkk of formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures into folders; the control fuzz target decodes with the three schema versions. The differential corpus gains two bases, format3_single and format3_time_and_key_portable, one per policy: 5110 cases, the earlier ones unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
}
// 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 and AuthorKey are those of an alg 1 signature.
SecretSeed string `json:"secret_seed,omitempty"`
AuthorKey string `json:"author_key,omitempty"`
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 is the 64 bytes of alg 1, or the DER of the CMS
// signature of alg 2.
SignatureValue string `json:"signature"`
SecurityKey2 string `json:"security_key_2"`
// Signers is, with alg 2, SIGNERS in hexadecimal, Certificates the DER of
// the certificates inside the signature, and Results what each required
// signer gave, in the order of SIGNERS; Foreign are the others.
Signers string `json:"signers,omitempty"`
Certificates []string `json:"certificates,omitempty"`
Results []FixtureSignerResult `json:"signer_results,omitempty"`
Foreign []FixtureSignerResult `json:"foreign_signers,omitempty"`
}
// FixtureSignerResult is a signer of an alg 2 signature as a reader shows it
// (spec §29.7, §29.10).
type FixtureSignerResult struct {
Holder string `json:"holder"`
Issuer string `json:"issuer"`
Result string `json:"result"`
SealTime string `json:"seal_time,omitempty"`
Before bool `json:"before_round_time"`
}
// FixtureSeal describes the seal of seal_type 2 of a format 3 fixture:
// SEAL_SUBJECT, the token in hexadecimal, the holder of the certificate of
// the authority as §29.7 shows it, and t.
type FixtureSeal struct {
SealType int `json:"seal_type"`
SealSubject string `json:"seal_subject"`
Token string `json:"token"`
Holder string `json:"holder"`
Time string `json:"time"`
}
// 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)
}

Powered by TurnKey Linux.