Format 3, step 6a: specification 0.10 and ERR_HEAD_INVALID

- SpecVersion is 0.10: the version command, the catalogue test and the
  spec field of every test data file name spec v0.10. The regenerated
  test data change in that field only.
- All lists ERR_HEAD_INVALID, last, as section 69 of the spec does.
- The tests of the path rules and of format 3 held literal invisible
  and combining characters (ZWJ, VS16, U+202E, soft hyphen, the Kelvin
  sign and others), which an editor could normalize or hide; they are
  Go escapes now, with the same values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.10
dev 1 week ago
parent a0de80fd85
commit 3c3933627d

@ -135,7 +135,7 @@ func TestSecurityVerdicts(t *testing.T) {
func sampleHead() *capsule.Head {
h := &capsule.Head{
Comment: "Para ti ❤️",
Comment: "Para ti ❤\ufe0f",
Author: "Ana López",
Files: []capsule.File{
{Path: "fotos/playa.jpg", Size: 10, Start: 0, End: 10, MTime: 1759190400, HasMTime: true},
@ -240,19 +240,19 @@ func TestDecodeHeadLayers(t *testing.T) {
"an unknown key 8": {head(t, map[uint64]any{8: uint64(1)}), datekeys.ErrNonCanonicalCBOR},
"a byte more": {append(head(t, nil), 0), datekeys.ErrNonCanonicalCBOR},
// Layer 4, in key order.
"a comment with U+202E": {head(t, map[uint64]any{3: "a‮b"}), datekeys.ErrHeadInvalid},
"a comment with U+202E": {head(t, map[uint64]any{3: "a\u202eb"}), datekeys.ErrHeadInvalid},
"a comment with the tag U+E0041": {head(t, map[uint64]any{3: "a\U000E0041"}), datekeys.ErrHeadInvalid},
"an author with LF": {head(t, map[uint64]any{4: "a\nb"}), datekeys.ErrHeadInvalid},
"R3: ..": {head(t, map[uint64]any{5: []any{file("..", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"R2: /a": {head(t, map[uint64]any{5: []any{file("/a", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"R4b: a and VS16": {head(t, map[uint64]any{5: []any{file("a️", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"R4b: a and VS16": {head(t, map[uint64]any{5: []any{file("a\ufe0f", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"R6: CON.txt": {head(t, map[uint64]any{5: []any{file("CON.txt", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"R10: .datekeys-x": {head(t, map[uint64]any{5: []any{file(".datekeys-x", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"layout: a first start that is not 0": {head(t, map[uint64]any{5: []any{file("a", 1, 1, 2)}}), datekeys.ErrHeadInvalid},
"layout: end minus start is not size": {head(t, map[uint64]any{5: []any{file("a", 2, 0, 1)}}), datekeys.ErrHeadInvalid},
"layout: a gap": {head(t, map[uint64]any{5: []any{file("a", 1, 0, 1), file("b", 1, 2, 3)}}), datekeys.ErrHeadInvalid},
"R7: A.txt and a.txt": {head(t, map[uint64]any{5: []any{file("A.txt", 0, 0, 0), file("a.txt", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"a comment and a path that break": {head(t, map[uint64]any{3: "a‮", 5: []any{file("..", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"a comment and a path that break": {head(t, map[uint64]any{3: "a\u202e", 5: []any{file("..", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"a path that breaks, then an unknown critical extension": {head(t, map[uint64]any{5: []any{file("..", 0, 0, 0)}, 6: []any{map[uint64]any{0: "x.example", 1: uint64(1)}}}), datekeys.ErrHeadInvalid},
"an unknown critical extension": {head(t, map[uint64]any{6: []any{map[uint64]any{0: "x.example", 1: uint64(1)}}}), datekeys.ErrExtensionCriticalUnknown},
} {
@ -269,10 +269,10 @@ func TestDecodeHeadLayers(t *testing.T) {
}
}
// codes counts the normative errors err wraps, ERR_HEAD_INVALID included.
// codes counts the normative errors err wraps.
func codes(err error) int {
n := 0
for _, e := range append(datekeys.All(), datekeys.ErrHeadInvalid) {
for _, e := range datekeys.All() {
if errors.Is(err, e) {
n++
}

@ -110,7 +110,7 @@ func TestOpen3(t *testing.T) {
c := capsule3{
paths: []string{"fotos/playa.jpg", "nota.txt", "vacío.txt"},
contents: [][]byte{photo, []byte("Hola.\n"), {}},
comment: "Para ti ❤️", author: "Ana López",
comment: "Para ti ❤\ufe0f", author: "Ana López",
}
dkc, body := c.build(t)

@ -73,8 +73,7 @@ var (
ErrExtensionDataInvalid = &Error{"ERR_EXTENSION_DATA_INVALID"}
// ErrHeadInvalid: the head of a format 3 capsule is well encoded but one
// of its fields breaks its rules: a path, the comment, the declared author
// or the layout of the files (spec §29.4 to §29.6). New in v0.10; All
// lists it once SpecVersion is 0.10.
// or the layout of the files (spec §29.4 to §29.6). New in v0.10.
ErrHeadInvalid = &Error{"ERR_HEAD_INVALID"}
)
@ -85,7 +84,7 @@ func All() []*Error {
ErrUnknownProfile, ErrProfileMismatch, ErrDateKeyInvalid, ErrDateKeyNonCanonical,
ErrRoundMismatch, ErrReleaseUnavailable, ErrReleaseInvalid, ErrAccessRequired,
ErrAccessInvalid, ErrPolicyStructureMismatch, ErrHeaderBinding, ErrIntegrity,
ErrExtensionCriticalUnknown, ErrExtensionDataInvalid,
ErrExtensionCriticalUnknown, ErrExtensionDataInvalid, ErrHeadInvalid,
}
}

@ -46,15 +46,15 @@ func TestProperties(t *testing.T) {
func TestNFD(t *testing.T) {
for _, c := range []struct{ in, want string }{
{"é", "é"},
{"Å", "Å"}, // ANGSTROM SIGN, a singleton decomposition
{"K", "K"}, // KELVIN SIGN
{"é", "e\u0301"},
{"\u212b", "A\u030a"}, // ANGSTROM SIGN, a singleton decomposition
{"\u212a", "K"}, // KELVIN SIGN
{"한", "한"}, // Hangul, with a final jamo
{"하", "하"}, // Hangul, without
{"ạ́", "ạ́"}, // canonical order
{"ΐ", "ΐ"},
{"ǖ", "ǖ"}, // recursive decomposition
{"á‍̣", "á‍̣"}, // ZWJ is a starter
{"a\u0301\u0323", "a\u0323\u0301"}, // canonical order
{"ΐ", "ι\u0308\u0301"},
{"ǖ", "u\u0308\u0304"}, // recursive decomposition
{"a\u0301\u200d\u0323", "a\u0301\u200d\u0323"}, // ZWJ is a starter
} {
if got := NFD(c.in); got != c.want {
t.Errorf("NFD(%+q) = %+q, want %+q", c.in, got, c.want)
@ -66,11 +66,11 @@ func TestKey(t *testing.T) {
for _, same := range [][2]string{
{"A.txt", "a.txt"},
{"Straße", "STRASSE"},
{"K", "k"},
{"\u212a", "k"},
{"ı", "i"},
{"é", "é"},
{"ab", "a‌b"},
{"á‍̣", "ạ́"},
{"é", "e\u0301"},
{"ab", "a\u200cb"},
{"a\u0301\u200d\u0323", "a\u0323\u0301"},
{".DateKeys-x", ".datekeys-x"},
} {
if Key(same[0]) != Key(same[1]) {
@ -96,9 +96,9 @@ func rule(err error) string {
func TestCheckPath(t *testing.T) {
const (
zwj = "‍"
zwnj = "‌"
vs16 = "️"
zwj = "\u200d"
zwnj = "\u200c"
vs16 = "\ufe0f"
)
scotland := "\U0001F3F4\U000E0067\U000E0062\U000E0073\U000E0063\U000E0074\U000E007F"
for _, c := range []struct {
@ -111,7 +111,7 @@ func TestCheckPath(t *testing.T) {
{"Para ti ♥.jpg", ""}, // bestfit874 maps ♥ to 0x03
{"§ 3 contrato.pdf", ""}, // bestfit874 maps § to 0x15
{"Madrid → Lisboa", ""}, // bestfit1253 maps → to '>'
{"❤" + vs16 + ".txt", ""}, // ❤️
{"❤" + vs16 + ".txt", ""}, // ❤\ufe0f
{"\U0001F3F3" + vs16 + zwj + "\U0001F308", ""}, // rainbow flag
{"\U0001F468" + zwj + "\U0001F469" + zwj + "\U0001F467", ""},
{"ab" + zwnj + "c", ""},
@ -121,7 +121,7 @@ func TestCheckPath(t *testing.T) {
{"report~2023.txt", ""},
{strings.Repeat("a", 255), ""},
{strings.Repeat("ΐ", 85), ""}, // 170 bytes, 255 UTF-16 units after NFD
{" a ", ""}, // no table maps U+00A0 to U+0020... checked below
{"\u00a0a\u00a0", ""}, // no table maps U+00A0 to U+0020... checked below
// R2.
{"/a", "R2"},
@ -139,13 +139,13 @@ func TestCheckPath(t *testing.T) {
{"a\\b", "R4"},
{"a?b", "R4"},
{"a\tb", "R4"},
{"a
b", "R4"},
{"a­b", "R4"},
{"ab", "R4"},
{"a‮b", "R4"},
{"informeanexo", "R4"},
{"a͸b", "R4"},
{"a￾b", "R4"},
{"a\u2028b", "R4"},
{"a\u00adb", "R4"},
{"a\u206ab", "R4"},
{"a\u202eb", "R4"},
{"informe\uf03aanexo", "R4"},
{"a\u0378b", "R4"},
{"a\ufffeb", "R4"},
{scotland, "R4"},
{"\U0001F600\U000E0100", "R4"}, // VS17
// R4b.
@ -174,7 +174,7 @@ func TestCheckPath(t *testing.T) {
{"CON.txt", "R6c"}, // full-width CON
{"a∖b", "R6c"}, // SET MINUS
{"a∶b", "R6c"}, // RATIO
{" a", "R6c"}, // bestfit1252 maps U+3000 to U+0020, which breaks R5
{"\u3000a", "R6c"}, // bestfit1252 maps U+3000 to U+0020, which breaks R5
{"a/b", "R6c"}, // full-width solidus
{"..", "R6c"}, // full-width dots project to ".."
// R10.
@ -189,7 +189,7 @@ func TestCheckPath(t *testing.T) {
}
continue
}
if c.path == " a " {
if c.path == "\u00a0a\u00a0" {
// Whatever the tables give, it must be R6c or nothing (R5 is
// about U+0020 only).
if got != "" && got != "R6c" {
@ -214,8 +214,8 @@ func TestCheckTree(t *testing.T) {
{[]string{"a/b", "a/b/c"}, "R7"},
{[]string{"Fotos/b", "fotos/a"}, "R7"},
{[]string{"STRASSE", "Straße"}, "R7"},
{[]string{"ab", "a‌b"}, "R7"},
{[]string{"k", "K"}, "R7"},
{[]string{"ab", "a\u200cb"}, "R7"},
{[]string{"k", "\u212a"}, "R7"},
} {
if got := rule(CheckTree(c.paths)); got != c.rule {
t.Errorf("CheckTree(%+q): %s, want %s", c.paths, got, c.rule)
@ -240,22 +240,22 @@ func TestTexts(t *testing.T) {
rule string
}{
{"Hola\tmundo\nsegunda línea", true, ""},
{"❤️ para ti", true, ""},
{"❤\ufe0f para ti", true, ""},
{"Hola\r\n", true, "text"},
{"a‮b", true, "text"},
{"a\u202eb", true, "text"},
{"Hola\U000E0049\U000E0047", true, "text"}, // tag characters
{"\U0001F600\U000E0100", true, "text"}, // VS17
{"a️", true, "text"}, // VS16 after a letter
{"a‍\nb", true, "text"}, // ZWJ at the end of a line
{"a￾", true, "text"},
{"a﷐", true, "text"},
{"a­b", true, "text"},
{"a\ufe0f", true, "text"}, // VS16 after a letter
{"a\u200d\nb", true, "text"}, // ZWJ at the end of a line
{"a\ufffe", true, "text"},
{"a\ufdd0", true, "text"},
{"a\u00adb", true, "text"},
{"Ana López", false, ""},
{"Ana\tLópez", false, "text"},
{"Ana\nLópez", false, "text"},
{" Ana", false, "text"},
{"Ana ", false, "text"},
{"a‎b", false, "text"},
{"a\u200eb", false, "text"},
} {
check := CheckAuthor
if c.comment {

@ -1,6 +1,6 @@
{
"description": "time_only capsule with an empty payload",
"spec": "0.9",
"spec": "0.10",
"format": 1,
"file": "empty_payload.dkc",
"sha256": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4",

@ -1,6 +1,6 @@
{
"description": "format 2 time_only capsule with an empty content: L = 0, P = 256",
"spec": "0.9",
"spec": "0.10",
"format": 2,
"file": "format2_empty_payload.dkc",
"sha256": "7aea2b5aa48b1a46053716f733d50fab9cd0b80b1be67631bcc06c5bb765dc21",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of format2_time_and_key_portable.dkc",
"spec": "0.9",
"spec": "0.10",
"file": "format2_time_and_key_portable.dkk",
"sha256": "095b7bc516a22bf0c2366f0af3cd48bfe857a2354d6e2a9b285278b95e450fe0",
"credential_id": "e3c7be83cbf1fbd6b115c96411b3bd01",

@ -1,6 +1,6 @@
{
"description": "format 2 time_and_key capsule with one credential, a portable .dkk, and 15 dummies",
"spec": "0.9",
"spec": "0.10",
"format": 2,
"file": "format2_time_and_key_portable.dkc",
"sha256": "600892659fe4890223e895876275f656995d170fda42b07fb2bec0ca51ce4b43",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of format2_time_and_key_recipients.dkc",
"spec": "0.9",
"spec": "0.10",
"file": "format2_time_and_key_recipients.dkk",
"sha256": "2ad99b1556086ec311d7f0b3bd3aaba05e75f45c4fa22490b0d5e8bb0b1a222e",
"credential_id": "93cedf68421710e83908ec683b104436",

@ -1,6 +1,6 @@
{
"description": "format 2 time_and_key capsule for three known X25519 recipients and a portable .dkk, and 12 dummies",
"spec": "0.9",
"spec": "0.10",
"format": 2,
"file": "format2_time_and_key_recipients.dkc",
"sha256": "1a44fd8708c92e2e0a10cfcb1d864a71331ea9af25d97e1a42e969dc898959e3",

@ -1,6 +1,6 @@
{
"description": "format 2 time_and_key capsule for sixteen known X25519 recipients, without dummies",
"spec": "0.9",
"spec": "0.10",
"format": 2,
"file": "format2_time_and_key_sixteen.dkc",
"sha256": "7aaac5c18f216bf53df326ecc817179640a53408cf25dfd50488910a762dc381",

@ -1,6 +1,6 @@
{
"description": "format 2 time_only capsule, padding code 2 (reforzado): L = 78000, P = 79872, two STREAM chunks",
"spec": "0.9",
"spec": "0.10",
"format": 2,
"file": "format2_time_only.dkc",
"sha256": "f5a40ac6b8a08a0c12db6114c2bca23522d6a77b512b509a217fb15f367813c4",

@ -1,6 +1,6 @@
{
"description": "format 2 time_only capsule with the content of format2_time_only and padding code 1 (bloque256): L = 78000, P = 78080",
"spec": "0.9",
"spec": "0.10",
"format": 2,
"file": "format2_time_only_bloque256.dkc",
"sha256": "aae769c30d04920801d8b293d30864fbe223c9c9353ec2b4907a1ee1996e39f9",

@ -1,6 +1,6 @@
{
"description": "format 2 time_only capsule with a noncritical PUBLIC_HEADER extension and a noncritical CONTROL_CBOR extension",
"spec": "0.9",
"spec": "0.10",
"format": 2,
"file": "format2_time_only_extensions.dkc",
"sha256": "fb406100d5703a2e888983b3175ed34a09a34469cc722256e5cf535dd728fbe9",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of time_and_key_portable.dkc",
"spec": "0.9",
"spec": "0.10",
"file": "time_and_key_portable.dkk",
"sha256": "e528fa2c832c91119f0684bb9d6fb3c4c2d0d55183482890e7c4fe92f668426a",
"credential_id": "3955e944a3c60cfa1fd6485e9693c77d",

@ -1,6 +1,6 @@
{
"description": "time_and_key capsule whose only recipient is a portable .dkk",
"spec": "0.9",
"spec": "0.10",
"format": 1,
"file": "time_and_key_portable.dkc",
"sha256": "2e97878078bae6358037a9c264f379a3cbe839f767d69836b0343f35657b2972",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of time_and_key_portable.dkc with a noncritical extension: the credential of time_and_key_portable.dkk re-issued with org.example.delivery",
"spec": "0.9",
"spec": "0.10",
"file": "time_and_key_portable_extension.dkk",
"sha256": "0bf463a7c65627b7dda2234d728df89ec5b835816a2a37b91497d8fecc5ea548",
"credential_id": "3955e944a3c60cfa1fd6485e9693c77d",

@ -1,6 +1,6 @@
{
"description": "portable X25519 .dkk of time_and_key_recipients.dkc",
"spec": "0.9",
"spec": "0.10",
"file": "time_and_key_recipients.dkk",
"sha256": "19f6c47150c3194712d454f43c7392b7344e6b4e7b074d83e9ca5f563a8e072f",
"credential_id": "b89292aedf6d05d584cec9a871ce8735",

@ -1,6 +1,6 @@
{
"description": "time_and_key capsule for two known X25519 recipients and a portable .dkk",
"spec": "0.9",
"spec": "0.10",
"format": 1,
"file": "time_and_key_recipients.dkc",
"sha256": "69ac110380f5d768b5b6afaa157a50ed17d8ceccfbd4604ffa5b6da38539b635",

@ -1,6 +1,6 @@
{
"description": "time_only capsule, two STREAM chunks, no extensions",
"spec": "0.9",
"spec": "0.10",
"format": 1,
"file": "time_only.dkc",
"sha256": "99e915810d595f1092700b728f5e5081d78efe83f5343e76325b1bcc2c33ccf2",

@ -1,6 +1,6 @@
{
"description": "time_only capsule with a noncritical PUBLIC_HEADER extension and a noncritical CONTROL_CBOR extension",
"spec": "0.9",
"spec": "0.10",
"format": 1,
"file": "time_only_extensions.dkc",
"sha256": "0446c9b73e267adcb24e5cc89afba2544a386ec9a050016e06517a4a57aa2085",

@ -1,5 +1,5 @@
{
"spec": "0.9",
"spec": "0.10",
"description": "CBOR profile of spec §58 and the schemas of spec/datekeys.cddl, generated by the reference implementation. accept and reject are walked as one data item of the profile with the limits of walk; schemas are decoded with the decoder of their schema. See testdata/README.md.",
"walk": {
"max_depth": 3,

@ -1,5 +1,5 @@
{
"spec": "0.9",
"spec": "0.10",
"description": "Canonical dk1_ strings and rejected encodings (spec §18, §19, §66), generated by the reference implementation.",
"vectors": [
{

@ -1,5 +1,5 @@
{
"spec": "0.9",
"spec": "0.10",
"description": "Differential corpus of the pre-unlock checks (spec §63 steps 1 to 8): deterministic mutations of the official .dkc fixtures with the verdict of the reference implementation. See testdata/README.md.",
"format": "Each mutation is bases[base].file (in testdata/fixtures) with its edits applied. An edit is [at, delete, insert]: the delete bytes at offset at of the base are replaced by the bytes of the hex string insert. The edits of one mutation refer to offsets of the unmodified base, are sorted by offset and do not overlap. result is the verdict of steps 1 to 8 of spec §63 (capsule.Inspect, the Quicknet profile pinned, no extension known, no network, no secret): ok, or the normative error code, with step the step that failed. kind names the generator of the mutation and is informative.",
"seed": 20260925,

@ -1,5 +1,5 @@
{
"spec": "0.9",
"spec": "0.10",
"description": "Mutation corpus of spec §64 and further cases of capsule.TestMutationCorpus, generated by the reference implementation: each case is a .dkc and what the reader is given, with the normative error and the step of spec §63 at which capsule.Open fails. See testdata/README.md.",
"cases": [
{

@ -1,5 +1,5 @@
{
"spec": "0.9",
"spec": "0.10",
"description": "Padding rules of the payload of a format 2 capsule (spec §29.1): for each content length L, P with code 1 (bloque256) and code 2 (reforzado), and the length of PAYLOAD_AGE for each. e, s and last_bits are informative. Generated by the reference implementation. See testdata/README.md.",
"l_max": 8936830510563328,
"vectors": [

@ -1,5 +1,5 @@
{
"spec": "0.9",
"spec": "0.10",
"description": "Quicknet Provider Profile V1: exact Deterministic CBOR and profile_hash (spec §11, §12, §75 item 2), generated by the reference implementation.",
"profile_id": "datekeys:quicknet:v1",
"provider": "drand",

@ -1,5 +1,5 @@
{
"spec": "0.9",
"spec": "0.10",
"profile": "datekeys:quicknet:v1",
"description": "Quicknet date to round resolution (spec §15, §16, §65), generated by the reference implementation.",
"vectors": [

@ -1,5 +1,5 @@
{
"spec": "0.9",
"spec": "0.10",
"description": "H2 of the IBE-CCA of tlock (spec §63 step 11): SHA-256 of \"IBE-H2\" and the 576 bytes of an element of GT, c1 before c0 at every level of the tower and each coordinate of Fp in 48 bytes big-endian (the order of kilic/bls12-381), truncated to 16 bytes. Generated by the reference implementation with drand/kyber-bls12381, the pairing of tlock.",
"vectors": [
{

@ -6,11 +6,11 @@ import (
)
// SpecVersion is the version of the DateKeys Protocol Specification that this
// module implements: spec/DateKeys_Protocol_Specification_v0.9.md, tagged
// spec-v0.9 in its repository. It is neither the version of the module (see
// Version) nor the versions inside the objects: the capsule format, 1 or 2,
// and the schema versions (spec §22, §70).
const SpecVersion = "0.9"
// module implements: spec/DateKeys_Protocol_Specification_v0.10.md, tagged
// spec-v0.10 in its repository once it is closed. It is neither the version
// of the module (see Version) nor the versions inside the objects: the
// capsule format, 1 to 3, and the schema versions (spec §22, §70).
const SpecVersion = "0.10"
// modulePath is the path of this module: the import path of its root
// package, whatever the module is called.

Loading…
Cancel
Save

Powered by TurnKey Linux.