Format 3, step 3: the reader
Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the
PRELUDE accepts VERSION 3, and the files go to a Sink.
- Sink: Begin with the validated head, Create for each file in the
order of the head, and Commit only after every check of step 17;
after any failure that follows a successful Begin, Abort, once. A
format 3 capsule without a Sink fails right after step 2 with
ErrSinkRequired, a caller error with no code, no failed step and no
request; a capsule of format 1 or 2 without dst fails there too.
- Step 17 in its substeps: the frame and the area, security and its
verdicts, which never fail, the head, the files filling CONTENT, the
SHA-256 of each file and the padding. A failure of age or a
plaintext whose length is not P prevails; otherwise the first
substep that fails decides, and a code other than ERR_INTEGRITY is
reported only after reading PAYLOAD_AGE to its end.
- The reads of BODY grow with the bytes received, never with AREA_LEN,
HEAD_LEN or a declared size (spec 57); a test measures it.
- A failure of the Sink is the caller's own error with ERR_INTEGRITY,
as one of dst is in formats 1 and 2.
- Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.
- Test data: "version changed" sets VERSION 4, and the format 2 list
gains "format 2 time_only relabeled format 3", which fails at step
14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec.
The randomly built capsules keep their recorded bytes.
- testkit: Build writes format 3 and can edit the padded plaintext;
Head3, Body3, DiscardSink and MemorySink build and open BODY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
package capsule_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"errors"
|
|
|
|
|
"io"
|
|
|
|
|
"runtime"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
|
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
|
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
|
|
|
"g.activething.com/go/DateKeys/extension"
|
|
|
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// The tests of this file cover the reader of format 3 (spec v0.10): step 17,
|
|
|
|
|
// its substeps and their precedence, and step 18 (spec §29.2 to §29.7, §63).
|
|
|
|
|
|
|
|
|
|
// capsule3 describes a format 3 capsule of round 1000 to build, with edits
|
|
|
|
|
// at each level: the head, HEAD_CBOR, BODY, the plaintext with its padding,
|
|
|
|
|
// and PAYLOAD_AGE.
|
|
|
|
|
type capsule3 struct {
|
|
|
|
|
paths []string
|
|
|
|
|
contents [][]byte
|
|
|
|
|
comment, author string
|
|
|
|
|
security []byte // nil: the empty security that writers write
|
|
|
|
|
head func(h *capsule.Head)
|
|
|
|
|
headCBOR func(b []byte) []byte
|
|
|
|
|
body func(b []byte) []byte
|
|
|
|
|
plain func(p []byte) []byte
|
|
|
|
|
payload func(t *testing.T, p []byte) []byte
|
|
|
|
|
structure capsule.Policy
|
|
|
|
|
recipients []age.Recipient
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// build returns the .dkc and its BODY, before any edit of the plaintext.
|
|
|
|
|
func (c capsule3) build(t *testing.T) ([]byte, []byte) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
h := testkit.Head3(c.comment, c.author, c.paths, c.contents)
|
|
|
|
|
if c.head != nil {
|
|
|
|
|
c.head(h)
|
|
|
|
|
}
|
|
|
|
|
hb, err := capsule.EncodeHead(h)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if c.headCBOR != nil {
|
|
|
|
|
hb = c.headCBOR(hb)
|
|
|
|
|
}
|
|
|
|
|
sec := c.security
|
|
|
|
|
if sec == nil {
|
|
|
|
|
sec = capsule.EncodeSecurity()
|
|
|
|
|
}
|
|
|
|
|
body := testkit.Body3(capsule.AreaUnit, sec, hb, c.contents...)
|
Format 3, step 3: the reader
Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the
PRELUDE accepts VERSION 3, and the files go to a Sink.
- Sink: Begin with the validated head, Create for each file in the
order of the head, and Commit only after every check of step 17;
after any failure that follows a successful Begin, Abort, once. A
format 3 capsule without a Sink fails right after step 2 with
ErrSinkRequired, a caller error with no code, no failed step and no
request; a capsule of format 1 or 2 without dst fails there too.
- Step 17 in its substeps: the frame and the area, security and its
verdicts, which never fail, the head, the files filling CONTENT, the
SHA-256 of each file and the padding. A failure of age or a
plaintext whose length is not P prevails; otherwise the first
substep that fails decides, and a code other than ERR_INTEGRITY is
reported only after reading PAYLOAD_AGE to its end.
- The reads of BODY grow with the bytes received, never with AREA_LEN,
HEAD_LEN or a declared size (spec 57); a test measures it.
- A failure of the Sink is the caller's own error with ERR_INTEGRITY,
as one of dst is in formats 1 and 2.
- Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.
- Test data: "version changed" sets VERSION 4, and the format 2 list
gains "format 2 time_only relabeled format 3", which fails at step
14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec.
The randomly built capsules keep their recorded bytes.
- testkit: Build writes format 3 and can edit the padded plaintext;
Head3, Body3, DiscardSink and MemorySink build and open BODY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
if c.body != nil {
|
|
|
|
|
body = c.body(body)
|
|
|
|
|
}
|
|
|
|
|
b, err := testkit.Build{Format: capsule.Format3, Declared: c.structure, Structure: c.structure,
|
|
|
|
|
AccessRecipients: c.recipients, Plaintext: body, EditPlaintext: c.plain}.Make()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if c.payload == nil {
|
|
|
|
|
return b.DKC, body
|
|
|
|
|
}
|
|
|
|
|
return testkit.Join(b.Prelude[:], b.PublicHeader, b.Sealed, c.payload(t, bytes.Clone(b.Payload))), body
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type result3 struct {
|
|
|
|
|
opened *capsule.Opened
|
|
|
|
|
sink *testkit.MemorySink
|
|
|
|
|
err error
|
|
|
|
|
unread int // bytes of the .dkc that Open did not read
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// open3 opens a capsule of round 1000 into sink.
|
|
|
|
|
func open3(t *testing.T, dkc []byte, sink capsule.Sink, ids ...age.Identity) result3 {
|
|
|
|
|
t.Helper()
|
|
|
|
|
o := defaultOpen(1000)
|
|
|
|
|
o.Identities, o.Sink = ids, sink
|
|
|
|
|
r := bytes.NewReader(dkc)
|
|
|
|
|
opened, err := capsule.Open(context.Background(), nil, r, o)
|
|
|
|
|
res := result3{opened: opened, err: err, unread: r.Len()}
|
|
|
|
|
if m, ok := sink.(*testkit.MemorySink); ok {
|
|
|
|
|
res.sink = m
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// chunk returns the offset in the age file p of its STREAM chunk i: after
|
|
|
|
|
// the header come a 16-byte nonce and chunks of 64 KiB, each with a 16-byte
|
|
|
|
|
// tag (C2SP age.md).
|
|
|
|
|
func chunk(t *testing.T, p []byte, i int) int {
|
|
|
|
|
t.Helper()
|
|
|
|
|
n, err := testkit.HeaderLen(p)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return n + 16 + i*(64<<10+16)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spec §29.2 to §29.7, §63 steps 17 and 18: a format 3 capsule opens, its
|
|
|
|
|
// files reach the Sink in the order of the head, and Commit comes last.
|
|
|
|
|
func TestOpen3(t *testing.T) {
|
|
|
|
|
photo := bytes.Repeat([]byte("playa"), 30000) // three STREAM chunks
|
|
|
|
|
c := capsule3{
|
|
|
|
|
paths: []string{"fotos/playa.jpg", "nota.txt", "vacío.txt"},
|
|
|
|
|
contents: [][]byte{photo, []byte("Hola.\n"), {}},
|
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>
1 week ago
|
|
|
comment: "Para ti ❤\ufe0f", author: "Ana López",
|
Format 3, step 3: the reader
Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the
PRELUDE accepts VERSION 3, and the files go to a Sink.
- Sink: Begin with the validated head, Create for each file in the
order of the head, and Commit only after every check of step 17;
after any failure that follows a successful Begin, Abort, once. A
format 3 capsule without a Sink fails right after step 2 with
ErrSinkRequired, a caller error with no code, no failed step and no
request; a capsule of format 1 or 2 without dst fails there too.
- Step 17 in its substeps: the frame and the area, security and its
verdicts, which never fail, the head, the files filling CONTENT, the
SHA-256 of each file and the padding. A failure of age or a
plaintext whose length is not P prevails; otherwise the first
substep that fails decides, and a code other than ERR_INTEGRITY is
reported only after reading PAYLOAD_AGE to its end.
- The reads of BODY grow with the bytes received, never with AREA_LEN,
HEAD_LEN or a declared size (spec 57); a test measures it.
- A failure of the Sink is the caller's own error with ERR_INTEGRITY,
as one of dst is in formats 1 and 2.
- Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.
- Test data: "version changed" sets VERSION 4, and the format 2 list
gains "format 2 time_only relabeled format 3", which fails at step
14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec.
The randomly built capsules keep their recorded bytes.
- testkit: Build writes format 3 and can edit the padded plaintext;
Head3, Body3, DiscardSink and MemorySink build and open BODY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
}
|
|
|
|
|
dkc, body := c.build(t)
|
|
|
|
|
|
|
|
|
|
// Steps 1 to 8 need no Sink.
|
|
|
|
|
in, err := capsule.Inspect(bytes.NewReader(dkc), capsule.InspectOptions{Registry: testkit.Registry()})
|
|
|
|
|
if err != nil || in.Prelude.Format != capsule.Format3 {
|
|
|
|
|
t.Fatalf("Inspect: format %d, %v", in.Prelude.Format, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res := open3(t, dkc, &testkit.MemorySink{})
|
|
|
|
|
if res.err != nil {
|
|
|
|
|
t.Fatal(res.err)
|
|
|
|
|
}
|
|
|
|
|
o, s := res.opened, res.sink
|
|
|
|
|
if !s.Committed || s.Aborted {
|
|
|
|
|
t.Errorf("committed %v, aborted %v", s.Committed, s.Aborted)
|
|
|
|
|
}
|
|
|
|
|
for i, want := range c.contents {
|
|
|
|
|
if !bytes.Equal(s.Files[i], want) {
|
|
|
|
|
t.Errorf("file %d: %d bytes, want %d", i+1, len(s.Files[i]), len(want))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
p, _ := capsule.PaddedLength(uint64(len(body)), capsule.Reforzado)
|
|
|
|
|
switch {
|
|
|
|
|
case o.Format != capsule.Format3 || o.AreaLen != capsule.AreaUnit:
|
Format 3, step 3: the reader
Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the
PRELUDE accepts VERSION 3, and the files go to a Sink.
- Sink: Begin with the validated head, Create for each file in the
order of the head, and Commit only after every check of step 17;
after any failure that follows a successful Begin, Abort, once. A
format 3 capsule without a Sink fails right after step 2 with
ErrSinkRequired, a caller error with no code, no failed step and no
request; a capsule of format 1 or 2 without dst fails there too.
- Step 17 in its substeps: the frame and the area, security and its
verdicts, which never fail, the head, the files filling CONTENT, the
SHA-256 of each file and the padding. A failure of age or a
plaintext whose length is not P prevails; otherwise the first
substep that fails decides, and a code other than ERR_INTEGRITY is
reported only after reading PAYLOAD_AGE to its end.
- The reads of BODY grow with the bytes received, never with AREA_LEN,
HEAD_LEN or a declared size (spec 57); a test measures it.
- A failure of the Sink is the caller's own error with ERR_INTEGRITY,
as one of dst is in formats 1 and 2.
- Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.
- Test data: "version changed" sets VERSION 4, and the format 2 list
gains "format 2 time_only relabeled format 3", which fails at step
14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec.
The randomly built capsules keep their recorded bytes.
- testkit: Build writes format 3 and can edit the padded plaintext;
Head3, Body3, DiscardSink and MemorySink build and open BODY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
t.Errorf("format %d, area %d", o.Format, o.AreaLen)
|
|
|
|
|
case o.PayloadLength != uint64(len(body)) || o.PaddedLength != p || o.Padding != capsule.Reforzado:
|
|
|
|
|
t.Errorf("L = %d, P = %d, padding %s; want %d, %d", o.PayloadLength, o.PaddedLength, o.Padding, len(body), p)
|
|
|
|
|
case o.Head == nil || o.Head != s.Head || o.Head.Comment != c.comment || o.Head.Author != c.author || len(o.Head.Files) != 3:
|
|
|
|
|
t.Errorf("head %+v", o.Head)
|
|
|
|
|
case o.Verdicts != capsule.Verdicts{Signature: capsule.VerdictNoSignature, Seal: capsule.VerdictNoSeal}:
|
|
|
|
|
t.Errorf("verdicts %+v", o.Verdicts)
|
|
|
|
|
}
|
|
|
|
|
checks := o.Inspection.Checks
|
|
|
|
|
if n := len(checks); n < 2 || checks[n-2].Step != 17 || !checks[n-2].OK || checks[n-1].Step != 18 || !checks[n-1].OK {
|
|
|
|
|
t.Errorf("last checks %+v", checks[len(checks)-2:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The examples of spec §29.2: L and P of three small capsules.
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
c capsule3
|
|
|
|
|
l, p uint64
|
|
|
|
|
}{
|
|
|
|
|
{"no files and no comment", capsule3{}, 577, 768},
|
|
|
|
|
{"a comment of one byte", capsule3{comment: "a"}, 580, 768},
|
|
|
|
|
{"nota.txt of 1000 bytes, with mtime", capsule3{paths: []string{"nota.txt"}, contents: [][]byte{make([]byte, 1000)},
|
|
|
|
|
head: func(h *capsule.Head) { h.Files[0].MTime, h.Files[0].HasMTime = 1790000000, true }}, 1641, 1792},
|
|
|
|
|
} {
|
|
|
|
|
dkc, _ := tc.c.build(t)
|
|
|
|
|
res := open3(t, dkc, &testkit.MemorySink{})
|
|
|
|
|
if res.err != nil || res.opened.PayloadLength != tc.l || res.opened.PaddedLength != tc.p || !res.sink.Committed {
|
|
|
|
|
t.Errorf("%s: L = %d, P = %d, %v; want %d, %d", tc.name, res.opened.PayloadLength, res.opened.PaddedLength, res.err, tc.l, tc.p)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// time_and_key: format 3 has the 16 stanzas of format 2 (spec §29.1, §39).
|
|
|
|
|
id, _ := age.GenerateX25519Identity()
|
|
|
|
|
dkc, _ = capsule3{paths: []string{"a"}, contents: [][]byte{[]byte("x")},
|
|
|
|
|
structure: capsule.TimeAndKey, recipients: []age.Recipient{id.Recipient()}}.build(t)
|
|
|
|
|
if res := open3(t, dkc, &testkit.MemorySink{}, id); res.err != nil || string(res.sink.Files[0]) != "x" {
|
|
|
|
|
t.Errorf("time_and_key: %v", res.err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spec §63 step 17: a failure of age, or a plaintext whose length is not P,
|
|
|
|
|
// prevails; otherwise the first substep that fails decides. A code other
|
|
|
|
|
// than ERR_INTEGRITY is reported only after reading PAYLOAD_AGE to its end.
|
|
|
|
|
// Nothing reaches Commit, and a Sink that got Begin gets Abort.
|
|
|
|
|
func TestOpen3Substeps(t *testing.T) {
|
|
|
|
|
two := capsule3{paths: []string{"a.txt", "b.txt"}, contents: [][]byte{[]byte("uno"), []byte("dos")}}
|
|
|
|
|
// With a big file the head is in STREAM chunk 0, and two chunks follow.
|
|
|
|
|
big := capsule3{paths: []string{"a.bin"}, contents: [][]byte{bytes.Repeat([]byte{0x5a}, 100000)}}
|
|
|
|
|
with := func(c capsule3, edit func(c *capsule3)) capsule3 { edit(&c); return c }
|
|
|
|
|
u32 := func(at int, v uint32) func(b []byte) []byte {
|
|
|
|
|
return func(b []byte) []byte { binary.BigEndian.PutUint32(b[at:], v); return b }
|
|
|
|
|
}
|
|
|
|
|
dotdot := func(h *capsule.Head) { h.Files[0].Path = ".." }
|
|
|
|
|
lastPadding := func(p []byte) []byte { p[len(p)-1] = 1; return p }
|
|
|
|
|
short := func(p []byte) []byte { return p[:len(p)-1] }
|
|
|
|
|
long := func(p []byte) []byte { return append(p, make([]byte, 256)...) }
|
|
|
|
|
areaByte := func(b []byte) []byte { b[capsule.BodyFrameSize+capsule.AreaUnit-1] = 1; return b }
|
Format 3, step 3: the reader
Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the
PRELUDE accepts VERSION 3, and the files go to a Sink.
- Sink: Begin with the validated head, Create for each file in the
order of the head, and Commit only after every check of step 17;
after any failure that follows a successful Begin, Abort, once. A
format 3 capsule without a Sink fails right after step 2 with
ErrSinkRequired, a caller error with no code, no failed step and no
request; a capsule of format 1 or 2 without dst fails there too.
- Step 17 in its substeps: the frame and the area, security and its
verdicts, which never fail, the head, the files filling CONTENT, the
SHA-256 of each file and the padding. A failure of age or a
plaintext whose length is not P prevails; otherwise the first
substep that fails decides, and a code other than ERR_INTEGRITY is
reported only after reading PAYLOAD_AGE to its end.
- The reads of BODY grow with the bytes received, never with AREA_LEN,
HEAD_LEN or a declared size (spec 57); a test measures it.
- A failure of the Sink is the caller's own error with ERR_INTEGRITY,
as one of dst is in formats 1 and 2.
- Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.
- Test data: "version changed" sets VERSION 4, and the format 2 list
gains "format 2 time_only relabeled format 3", which fails at step
14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec.
The randomly built capsules keep their recorded bytes.
- testkit: Build writes format 3 and can edit the padded plaintext;
Head3, Body3, DiscardSink and MemorySink build and open BODY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// HEAD_CBOR starts with the map, key 0, the text header and the 13 bytes
|
|
|
|
|
// of "datekeys-head", then key 1 and the version.
|
|
|
|
|
const headTag, headVersion = 3, 17
|
|
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
c capsule3
|
|
|
|
|
want error // nil: opens
|
|
|
|
|
begun bool // the Sink got Begin, and so Abort
|
|
|
|
|
eof bool // Open read the .dkc to its end
|
|
|
|
|
}{
|
|
|
|
|
// 17.2: the frame and the area.
|
|
|
|
|
{"AREA_LEN 511", with(two, func(c *capsule3) { c.body = u32(0, 511) }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"AREA_LEN 513", with(two, func(c *capsule3) { c.body = u32(0, 513) }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"AREA_LEN 66048", with(two, func(c *capsule3) { c.body = u32(0, 66048) }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"SECURITY_LEN 0", with(two, func(c *capsule3) { c.body = u32(4, 0) }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"SECURITY_LEN 513", with(two, func(c *capsule3) { c.body = u32(4, 513) }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"HEAD_LEN 0", with(two, func(c *capsule3) { c.body = u32(8, 0) }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"HEAD_LEN 2^24 + 1", with(two, func(c *capsule3) { c.body = u32(8, 1<<24+1) }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"12 + AREA_LEN + HEAD_LEN = L + 1", with(capsule3{}, func(c *capsule3) { c.body = short }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"L < 12", with(two, func(c *capsule3) { c.body = func(b []byte) []byte { return b[:11] } }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"a byte of the area not zero", with(two, func(c *capsule3) { c.body = areaByte }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"a byte of the area not zero, and path ..", with(two, func(c *capsule3) { c.head, c.body = dotdot, areaByte }), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
|
|
|
|
|
// 17.3 and 17.6: security never fails.
|
|
|
|
|
{"security unreadable", with(two, func(c *capsule3) { c.security = []byte{0xa0} }), nil, false, true},
|
|
|
|
|
|
|
|
|
|
// 17.4: the head, whose codes wait for the end of PAYLOAD_AGE.
|
|
|
|
|
{"head of version 2", with(two, func(c *capsule3) {
|
|
|
|
|
c.headCBOR = func(b []byte) []byte { b[headVersion] = 2; return b }
|
|
|
|
|
}), datekeys.ErrUnsupportedVersion, false, true},
|
|
|
|
|
{"head of another type tag", with(two, func(c *capsule3) {
|
|
|
|
|
c.headCBOR = func(b []byte) []byte { b[headTag] = 'D'; return b }
|
|
|
|
|
}), datekeys.ErrNonCanonicalCBOR, false, true},
|
|
|
|
|
{"head with one byte more within HEAD_LEN", with(two, func(c *capsule3) {
|
|
|
|
|
c.headCBOR = func(b []byte) []byte { return append(b, 0) }
|
|
|
|
|
}), datekeys.ErrNonCanonicalCBOR, false, true},
|
|
|
|
|
{"paths b and a, in that order", with(two, func(c *capsule3) { c.paths = []string{"b.txt", "a.txt"} }), datekeys.ErrNonCanonicalCBOR, false, true},
|
|
|
|
|
{"path ..", with(two, func(c *capsule3) { c.head = dotdot }), datekeys.ErrHeadInvalid, false, true},
|
|
|
|
|
{"paths A.txt and a.txt", with(two, func(c *capsule3) { c.paths = []string{"A.txt", "a.txt"} }), datekeys.ErrHeadInvalid, false, true},
|
Format 3, step 4: the writer
EncryptFiles writes format 3 (spec 29.2 to 29.6, 61, 62, 62.1): the
files of a list of Sources, each read twice, with the comment and the
declared author.
- Before anything is written: the paths and the texts are checked with
the rules of the reader, in the words of a writer, naming the rule
and the character, and the two paths of an R7 collision (rule 15);
the comment has its CR LF and lone CR turned into LF (29.6); L is
measured with a head whose salt and SHA-256 are zero, as long as the
final one, and the first reading hashes each file, which must have
exactly its Size.
- The files go in the byte order of their paths (R8), whatever the
order of the Sources; the mtime is kept only from 1970 to 9999,
never clipped (rule 16); at least one file or a comment (rule 14).
- The head, with a fresh salt, the control and the security area are
decoded with the rules of the reader before sealing (rule 17), and
the frame is checked against L. The area is 512 bytes with the
empty security, whatever the options (rule 13).
- The second reading writes each file into PAYLOAD_AGE and fails if its
size or SHA-256 changed (rule 18).
- Encrypt and EncryptFiles share the sealing; Encrypt writes format 2
only with the new TestVectors option (rule 1), and takes no head.
The test data generators set it, and so does the CLI until step 5
moves it to EncryptFiles.
- Result.Head is the head written. DecodeHead keeps the check of the
critical extensions apart, so that the self-check decodes the head
as the one of the control does.
- The examples and the live test write with EncryptFiles.
- The reader tests had a literal U+202E, now escaped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
{"comment with U+202E", with(two, func(c *capsule3) { c.comment = "a\u202eb" }), datekeys.ErrHeadInvalid, false, true},
|
Format 3, step 3: the reader
Open reads format 3 (spec 29.2 to 29.7, 63 steps 17 and 18): the
PRELUDE accepts VERSION 3, and the files go to a Sink.
- Sink: Begin with the validated head, Create for each file in the
order of the head, and Commit only after every check of step 17;
after any failure that follows a successful Begin, Abort, once. A
format 3 capsule without a Sink fails right after step 2 with
ErrSinkRequired, a caller error with no code, no failed step and no
request; a capsule of format 1 or 2 without dst fails there too.
- Step 17 in its substeps: the frame and the area, security and its
verdicts, which never fail, the head, the files filling CONTENT, the
SHA-256 of each file and the padding. A failure of age or a
plaintext whose length is not P prevails; otherwise the first
substep that fails decides, and a code other than ERR_INTEGRITY is
reported only after reading PAYLOAD_AGE to its end.
- The reads of BODY grow with the bytes received, never with AREA_LEN,
HEAD_LEN or a declared size (spec 57); a test measures it.
- A failure of the Sink is the caller's own error with ERR_INTEGRITY,
as one of dst is in formats 1 and 2.
- Opened gains Head, Verdicts, AreaLen and UnusableHeadExtensions.
- Test data: "version changed" sets VERSION 4, and the format 2 list
gains "format 2 time_only relabeled format 3", which fails at step
14, as section 64 of spec v0.10 lists: 126 cases, 89 of the spec.
The randomly built capsules keep their recorded bytes.
- testkit: Build writes format 3 and can edit the padded plaintext;
Head3, Body3, DiscardSink and MemorySink build and open BODY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
{"start of an entry not the end of the one before", with(two, func(c *capsule3) {
|
|
|
|
|
c.head = func(h *capsule.Head) { h.Files[1].Start, h.Files[1].End = 2, 5 }
|
|
|
|
|
}), datekeys.ErrHeadInvalid, false, true},
|
|
|
|
|
{"an unknown critical extension", with(two, func(c *capsule3) {
|
|
|
|
|
c.head = func(h *capsule.Head) { h.Critical = []extension.Extension{{ID: "x.head", Version: 1}} }
|
|
|
|
|
}), datekeys.ErrExtensionCriticalUnknown, false, true},
|
|
|
|
|
|
|
|
|
|
// 17.5: the files fill CONTENT.
|
|
|
|
|
{"end of the last file not C", with(two, func(c *capsule3) {
|
|
|
|
|
c.body = func(b []byte) []byte { return append(b, 'x') }
|
|
|
|
|
}), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
|
|
|
|
|
// 17.7: the SHA-256 of each file, and 17.8: the padding, up to P.
|
|
|
|
|
{"a byte of a file changed", with(two, func(c *capsule3) {
|
|
|
|
|
c.body = func(b []byte) []byte { b[len(b)-1] ^= 1; return b }
|
|
|
|
|
}), datekeys.ErrIntegrity, true, false},
|
|
|
|
|
{"a padding byte not zero", with(two, func(c *capsule3) { c.plain = lastPadding }), datekeys.ErrIntegrity, true, false},
|
|
|
|
|
{"plaintext of P - 1 bytes", with(two, func(c *capsule3) { c.plain = short }), datekeys.ErrIntegrity, true, true},
|
|
|
|
|
{"plaintext of P + 256 bytes", with(two, func(c *capsule3) { c.plain = long }), datekeys.ErrIntegrity, true, false},
|
|
|
|
|
|
|
|
|
|
// Precedence: the head fails first, and what follows decides only when
|
|
|
|
|
// it is a failure of age or of the length.
|
|
|
|
|
{"path .. and a padding byte not zero", with(two, func(c *capsule3) { c.head, c.plain = dotdot, lastPadding }), datekeys.ErrHeadInvalid, false, true},
|
|
|
|
|
{"path .. and a plaintext of P - 1 bytes", with(two, func(c *capsule3) { c.head, c.plain = dotdot, short }), datekeys.ErrIntegrity, false, true},
|
|
|
|
|
{"path .. and a plaintext of P + 256 bytes", with(two, func(c *capsule3) { c.head, c.plain = dotdot, long }), datekeys.ErrIntegrity, false, true},
|
|
|
|
|
{"path .. and the next STREAM chunk corrupt", with(big, func(c *capsule3) {
|
|
|
|
|
c.head = dotdot
|
|
|
|
|
c.payload = func(t *testing.T, p []byte) []byte { p[chunk(t, p, 1)+100] ^= 1; return p }
|
|
|
|
|
}), datekeys.ErrIntegrity, false, false},
|
|
|
|
|
{"path .. and a cut right after its chunk", with(big, func(c *capsule3) {
|
|
|
|
|
c.head = dotdot
|
|
|
|
|
c.payload = func(t *testing.T, p []byte) []byte { return p[:chunk(t, p, 1)] }
|
|
|
|
|
}), datekeys.ErrIntegrity, false, true},
|
|
|
|
|
{"path .. and a big file", with(big, func(c *capsule3) { c.head = dotdot }), datekeys.ErrHeadInvalid, false, true},
|
|
|
|
|
} {
|
|
|
|
|
dkc, _ := tc.c.build(t)
|
|
|
|
|
res := open3(t, dkc, &testkit.MemorySink{})
|
|
|
|
|
o, s := res.opened, res.sink
|
|
|
|
|
if tc.want == nil {
|
|
|
|
|
if res.err != nil || !s.Committed || o.Verdicts.Signature != capsule.VerdictUnreadable || len(o.Verdicts.Lines()) != 1 {
|
|
|
|
|
t.Errorf("%s: %v, verdicts %+v", tc.name, res.err, o.Verdicts)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
step := failedStep(t, o.Inspection.Checks, res.err)
|
|
|
|
|
expectStep(t, tc.name, step, res.err, tc.want, 17)
|
|
|
|
|
switch {
|
|
|
|
|
case s.Committed || o.Head != nil:
|
|
|
|
|
t.Errorf("%s: committed %v, head %v", tc.name, s.Committed, o.Head)
|
|
|
|
|
case (s.Head != nil) != tc.begun || s.Aborted != tc.begun:
|
|
|
|
|
t.Errorf("%s: begun %v, aborted %v, want %v", tc.name, s.Head != nil, s.Aborted, tc.begun)
|
|
|
|
|
case tc.eof && res.unread != 0:
|
|
|
|
|
t.Errorf("%s: %d bytes of the .dkc not read", tc.name, res.unread)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// failSink fails at one point: "begin", "create", "write", "close" or
|
|
|
|
|
// "commit", at file 2 for "create", "write" and "close".
|
|
|
|
|
type failSink struct {
|
|
|
|
|
testkit.MemorySink
|
|
|
|
|
at string
|
|
|
|
|
aborts int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var errDiskFull = errors.New("disk full")
|
|
|
|
|
|
|
|
|
|
func (s *failSink) Begin(h *capsule.Head) error {
|
|
|
|
|
if s.at == "begin" {
|
|
|
|
|
return errDiskFull
|
|
|
|
|
}
|
|
|
|
|
return s.MemorySink.Begin(h)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *failSink) Create(i int) (io.WriteCloser, error) {
|
|
|
|
|
if s.at == "create" && i == 1 {
|
|
|
|
|
return nil, errDiskFull
|
|
|
|
|
}
|
|
|
|
|
w, err := s.MemorySink.Create(i)
|
|
|
|
|
return &failWriter{WriteCloser: w, fail: i == 1, at: s.at}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *failSink) Commit() error {
|
|
|
|
|
if s.at == "commit" {
|
|
|
|
|
return errDiskFull
|
|
|
|
|
}
|
|
|
|
|
return s.MemorySink.Commit()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *failSink) Abort() { s.aborts++; s.MemorySink.Abort() }
|
|
|
|
|
|
|
|
|
|
type failWriter struct {
|
|
|
|
|
io.WriteCloser
|
|
|
|
|
fail bool
|
|
|
|
|
at string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *failWriter) Write(b []byte) (int, error) {
|
|
|
|
|
if w.fail && w.at == "write" {
|
|
|
|
|
return 0, errDiskFull
|
|
|
|
|
}
|
|
|
|
|
return w.WriteCloser.Write(b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *failWriter) Close() error {
|
|
|
|
|
if w.fail && w.at == "close" {
|
|
|
|
|
return errDiskFull
|
|
|
|
|
}
|
|
|
|
|
return w.WriteCloser.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A failure of the Sink is the caller's own error with ERR_INTEGRITY, as a
|
|
|
|
|
// failure of dst is in formats 1 and 2, and after Begin it gets Abort, once.
|
|
|
|
|
func TestOpen3SinkFailures(t *testing.T) {
|
|
|
|
|
dkc, _ := capsule3{paths: []string{"a.txt", "b.txt"}, contents: [][]byte{[]byte("uno"), []byte("dos")}}.build(t)
|
|
|
|
|
for _, at := range []string{"begin", "create", "write", "close", "commit"} {
|
|
|
|
|
s := &failSink{at: at}
|
|
|
|
|
res := open3(t, dkc, s)
|
|
|
|
|
wantAborts := 1
|
|
|
|
|
if at == "begin" {
|
|
|
|
|
wantAborts = 0
|
|
|
|
|
}
|
|
|
|
|
switch {
|
|
|
|
|
case !errors.Is(res.err, errDiskFull) || datekeys.Code(res.err) != datekeys.Code(datekeys.ErrIntegrity):
|
|
|
|
|
t.Errorf("%s: %v", at, res.err)
|
|
|
|
|
case s.aborts != wantAborts || s.Committed || res.opened.Head != nil:
|
|
|
|
|
t.Errorf("%s: %d aborts, committed %v", at, s.aborts, s.Committed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if res := open3(t, dkc, &failSink{}); res.err != nil {
|
|
|
|
|
t.Fatal(res.err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spec §57: a reader reserves no memory by what BODY declares before it
|
|
|
|
|
// receives the bytes. Each capsule declares 16 MiB, in HEAD_LEN or in the
|
|
|
|
|
// size of a file, and holds a plaintext of 2000 bytes; Open fails, and
|
|
|
|
|
// allocates about what a small capsule makes it allocate.
|
|
|
|
|
func TestOpen3DeclaredLengths(t *testing.T) {
|
|
|
|
|
const declared = 16 << 20
|
|
|
|
|
cut := func(p []byte) []byte { return p[:2000] }
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
c capsule3
|
|
|
|
|
}{
|
|
|
|
|
{"HEAD_LEN", capsule3{plain: cut, body: func(b []byte) []byte {
|
|
|
|
|
binary.BigEndian.PutUint32(b[8:], declared)
|
|
|
|
|
return append(b, make([]byte, declared)...)
|
|
|
|
|
}}},
|
|
|
|
|
{"size", capsule3{plain: cut, paths: []string{"a.bin"}, contents: [][]byte{make([]byte, declared)}}},
|
|
|
|
|
} {
|
|
|
|
|
dkc, _ := tc.c.build(t)
|
|
|
|
|
var before, after runtime.MemStats
|
|
|
|
|
runtime.GC()
|
|
|
|
|
runtime.ReadMemStats(&before)
|
|
|
|
|
res := open3(t, dkc, &testkit.MemorySink{})
|
|
|
|
|
runtime.ReadMemStats(&after)
|
|
|
|
|
if n := after.TotalAlloc - before.TotalAlloc; n > 4<<20 || !errors.Is(res.err, datekeys.ErrIntegrity) {
|
|
|
|
|
t.Errorf("%s: %d bytes allocated, %v", tc.name, n, res.err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|