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/open3_test.go

399 lines
16 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

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.AreaLen, sec, hb, c.contents...)
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"), {}},
comment: "Para ti ❤️", author: "Ana López",
}
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.AreaLen:
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.AreaLen-1] = 1; return b }
// 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},
{"comment with U+202E", with(two, func(c *capsule3) { c.comment = "a‮b" }), datekeys.ErrHeadInvalid, false, true},
{"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)
}
}
}

Powered by TurnKey Linux.