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>
v0.10
dev 1 week ago
parent 72e86b8d79
commit 7249ef4cd1

@ -23,7 +23,7 @@ import (
// The tests of this file cover format 2 (spec v0.9): the changes of spec §76
// "Cambios normativos de la v0.9" and the tests it names.
// Spec §22, §23, §70: a reader opens both formats and reports which; any
// Spec §22, §23, §70: a reader opens every format and reports which; any
// other VERSION is rejected at step 2, without a request.
func TestFormatDispatch(t *testing.T) {
for _, name := range fixtureNames {
@ -35,7 +35,7 @@ func TestFormatDispatch(t *testing.T) {
}
}
f := loadFixture(t, "format2_time_only")
for _, v := range []byte{0, 3, 0x80, 0xff} {
for _, v := range []byte{0, 4, 0x80, 0xff} {
dkc := bytes.Clone(f.dkc)
dkc[4] = v
step, calls, err := openStep(t, dkc, capsule.OpenOptions{Now: testkit.Fixed(f.unlock(t))})
@ -44,10 +44,39 @@ func TestFormatDispatch(t *testing.T) {
t.Errorf("VERSION %d: %d release requests", v, calls)
}
}
// VERSION 3 is format 3 since v0.10: it passes step 2. Without a Sink,
// Open stops right there with ErrSinkRequired, a caller error with no code,
// no failed step and no request; with one, the version 2 control fails at
// step 14.
dkc := bytes.Clone(f.dkc)
dkc[4] = 3
src := testkit.NewSource(f.release)
opened, err := capsule.Open(context.Background(), &bytes.Buffer{}, bytes.NewReader(dkc),
capsule.OpenOptions{Registry: testkit.Registry(), Source: src, Now: testkit.Fixed(f.unlock(t))})
if !errors.Is(err, capsule.ErrSinkRequired) || datekeys.Code(err) != "" || src.Calls != 0 || opened.Format != capsule.Format3 {
t.Errorf("VERSION 3 without a Sink: %v, code %q, %d requests, format %d", err, datekeys.Code(err), src.Calls, opened.Format)
}
if c := opened.Inspection.Checks; len(c) != 2 || !c[0].OK || !c[1].OK || c[1].Step != 2 {
t.Errorf("VERSION 3 without a Sink: checks %+v", c)
}
step, calls, err := openStep(t, dkc, capsule.OpenOptions{Now: testkit.Fixed(f.unlock(t)), Sink: testkit.DiscardSink{}})
expectStep(t, "VERSION 3 with a Sink", step, err, datekeys.ErrUnsupportedVersion, 14)
if calls != 1 {
t.Errorf("VERSION 3 with a Sink: %d release requests", calls)
}
// A capsule of format 1 or 2 needs dst, whatever the Sink.
src = testkit.NewSource(f.release)
o := f.openOptions(t)
o.Source, o.Sink = src, testkit.DiscardSink{}
if _, err := capsule.Open(context.Background(), nil, bytes.NewReader(f.dkc), o); err == nil || datekeys.Code(err) != "" || src.Calls != 0 {
t.Errorf("format 2 without dst: %v, %d requests", err, src.Calls)
}
}
// Spec §22, §76 change 1: VERSION is public and anyone can edit it. A capsule
// relabeled to the other format fails at step 12 or 14, after the release,
// relabeled to another format fails at step 12 or 14, after the release,
// and nothing is written.
func TestFormatRelabel(t *testing.T) {
relabel := func(dkc []byte, v byte) []byte {
@ -86,8 +115,15 @@ func TestFormatRelabel(t *testing.T) {
{"format 1 time_only relabeled 2", relabel(to1.dkc, 2), nil, to1, datekeys.ErrUnsupportedVersion, 14},
{"format 1 time_and_key with one stanza relabeled 2", relabel(tk1.dkc, 2), identity(tk1), tk1, datekeys.ErrPolicyStructureMismatch, 12},
{"format 1 time_and_key with 16 stanzas relabeled 2", relabel(b.DKC, 2), []age.Identity{stranger}, nil, datekeys.ErrUnsupportedVersion, 14},
// Format 3 has the 16 stanzas and the padding of format 2 (spec §29.1).
{"format 2 time_only relabeled 3", relabel(to2.dkc, 3), nil, to2, datekeys.ErrUnsupportedVersion, 14},
{"format 2 time_and_key relabeled 3", relabel(tk2.dkc, 3), identity(tk2), tk2, datekeys.ErrUnsupportedVersion, 14},
{"format 1 time_only relabeled 3", relabel(to1.dkc, 3), nil, to1, datekeys.ErrUnsupportedVersion, 14},
{"format 1 time_and_key with one stanza relabeled 3", relabel(tk1.dkc, 3), identity(tk1), tk1, datekeys.ErrPolicyStructureMismatch, 12},
{"format 1 time_and_key with 16 stanzas relabeled 3", relabel(b.DKC, 3), []age.Identity{stranger}, nil, datekeys.ErrUnsupportedVersion, 14},
} {
o := capsule.OpenOptions{Identities: tc.ids}
// The Sink plays no part in formats 1 and 2.
o := capsule.OpenOptions{Identities: tc.ids, Sink: testkit.DiscardSink{}}
if tc.now != nil {
o.Now = testkit.Fixed(tc.now.unlock(t))
}

@ -134,9 +134,7 @@ func ParsePrelude(b []byte) (Prelude, error) {
if len(b) < PreludeSize {
return Prelude{}, fmt.Errorf("capsule: truncated prelude: %w", datekeys.ErrIntegrity)
}
// Format 3 is read from step 3 of the plan of format 3 on, together with
// the test data that expect VERSION 3 to be rejected here.
if f := Format(b[4]); !f.valid() || f == Format3 {
if !Format(b[4]).valid() {
return Prelude{}, fmt.Errorf("capsule: framing version %d: %w", b[4], datekeys.ErrUnsupportedVersion)
}
if b[5] != 0 || b[6] != 0 || b[7] != 0 {

@ -94,11 +94,13 @@ type parsed struct {
// the payload; it does not read the rest of the payload. On failure it
// returns the partial Inspection together with the error.
func Inspect(r io.Reader, opts InspectOptions) (*Inspection, error) {
in, _, err := inspect(r, opts)
in, _, err := inspect(r, opts, nil)
return in, err
}
func inspect(r io.Reader, opts InspectOptions) (*Inspection, *parsed, error) {
// inspect runs steps 1 to 8. afterPrelude, when not nil, runs right after
// step 2 passes: its error, a caller's one, ends inspect there.
func inspect(r io.Reader, opts InspectOptions, afterPrelude func(Prelude) error) (*Inspection, *parsed, error) {
in := &Inspection{}
if opts.Registry == nil {
return in, nil, errors.New("capsule: InspectOptions.Registry is required")
@ -122,6 +124,11 @@ func inspect(r io.Reader, opts InspectOptions) (*Inspection, *parsed, error) {
in.PayloadOffset = prelude.PayloadOffset()
in.pass(2, "prelude", fmt.Sprintf("DKC1 v%d, PUBLIC_HEADER_LEN=%d, SEALED_CONTROL_LEN=%d",
prelude.Format, prelude.PublicHeaderLen, prelude.SealedControlLen))
if afterPrelude != nil {
if err := afterPrelude(prelude); err != nil {
return in, nil, err
}
}
// Step 3: read the exact PUBLIC_HEADER bytes.
hb, err := readExactly(r, int64(prelude.PublicHeaderLen))

@ -55,6 +55,10 @@ type OpenOptions struct {
// clock on its own. Open does not ask Source for a round whose time has
// not been reached.
Now func() time.Time
// Sink receives the files of a format 3 capsule; dst receives the
// content of formats 1 and 2. Open fails right after step 2 with
// ErrSinkRequired when a format 3 capsule has no Sink.
Sink Sink
}
// Opened describes a capsule that Open decrypted completely.
@ -73,6 +77,17 @@ type Opened struct {
PayloadLength uint64
Padding Padding
PaddedLength uint64
// In format 3, Head is the head, as step 17.4 validated it, Verdicts
// are the verdicts of the security area, which a caller shows before the
// declared author and the comment (spec §29.7), and AreaLen is the size
// of that area. PayloadLength is then the length of BODY.
Head *Head
Verdicts Verdicts
AreaLen uint32
// UnusableHeadExtensions are the known noncritical extensions of the
// head that OpenOptions.Extensions rejects, as UnusableControlExtensions
// are for the control (spec §29.4, §54).
UnusableHeadExtensions []extension.Unusable
// ControlCritical and ControlNoncritical are the extensions of the sealed
// CONTROL_CBOR, only visible after opening.
ControlCritical []extension.Extension
@ -118,8 +133,17 @@ func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*O
}
}
// Steps 1 to 8.
in, st, err := inspect(r, InspectOptions{Registry: opts.Registry, Extensions: opts.Extensions})
// Steps 1 to 8. Right after step 2, the format decides where the content
// goes: a format 3 capsule needs a Sink, and the others dst.
in, st, err := inspect(r, InspectOptions{Registry: opts.Registry, Extensions: opts.Extensions}, func(p Prelude) error {
switch {
case p.Format == Format3 && opts.Sink == nil:
return ErrSinkRequired
case p.Format != Format3 && dst == nil:
return errWriterRequired
}
return nil
})
out := &Opened{Inspection: in, Format: in.Prelude.Format}
if err != nil {
return out, err
@ -264,7 +288,7 @@ func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*O
return out, in.fail(16, "payload identity", err)
}
detail := "I_PAYLOAD recovered"
if format == Format2 {
if format.padded() {
if out.PaddedLength, err = PaddedLength(control.PayloadLength, control.Padding); err != nil {
return out, in.fail(16, "payload identity", err)
}
@ -280,6 +304,15 @@ func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*O
if err != nil {
return out, in.fail(17, "open payload", classify("PAYLOAD_AGE", ageHeaderFailure, err))
}
if format == Format3 {
out.PayloadLength = control.PayloadLength
if err := openBody(pr, control.PayloadLength, out.PaddedLength, opts.Sink, opts.Extensions, out); err != nil {
return out, in.fail(17, "open payload", err)
}
in.pass(17, "open payload", fmt.Sprintf("payload authenticated; BODY of %d bytes, %d files, area of %d bytes", control.PayloadLength, len(out.Head.Files), out.AreaLen))
in.pass(18, "commit", fmt.Sprintf("%d files", len(out.Head.Files)))
return out, nil
}
w := &plaintextWriter{w: dst}
var n int64
if format == Format2 {
@ -317,7 +350,7 @@ func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*O
// accessSlots is the number of stanzas INNER_ACCESS_AGE must hold in a
// capsule of format f, 0 meaning one or more (spec §33, §39).
func accessSlots(f Format) int {
if f == Format2 {
if f.padded() {
return agewrap.AccessSlots
}
return 0

@ -0,0 +1,250 @@
package capsule
import (
"crypto/hmac"
"crypto/sha256"
"errors"
"fmt"
"io"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/extension"
)
// Sink receives the files of a format 3 capsule (spec §56, §63 steps 17 and
// 18). Open calls Begin once the head is validated, then Create for each
// file in the order of the head, and then Commit, only after every check of
// step 17 has passed. After any failure that follows a successful Begin, a
// failure of Commit included, it calls Abort, once; a Begin that fails
// cleans up after itself. Nothing a Sink receives before Commit may be
// presented as valid: write to a temporary place and publish it in Commit.
type Sink interface {
// Begin receives the validated head. Its files are h.Files.
Begin(h *Head) error
// Create returns the writer of file i of the head. Open closes it after
// writing its Size bytes, before checking its SHA-256. Size, Start and
// End are declared, not received: a Sink must not reserve memory or disk
// by them (spec §57).
Create(i int) (io.WriteCloser, error)
// Commit publishes the files: step 18.
Commit() error
// Abort discards everything the Sink received.
Abort()
}
// ErrSinkRequired is the error of Open for a format 3 capsule without
// OpenOptions.Sink: its content is several files, which one io.Writer cannot
// receive. It is an error of the caller, with no normative code: Open returns
// it right after step 2, before any request, and never reports the capsule
// as ErrUnsupportedVersion, because it is valid (spec §70).
var ErrSinkRequired = errors.New("capsule: a format 3 capsule holds files: OpenOptions.Sink is required")
// errWriterRequired is the error of Open for a capsule of format 1 or 2 with
// a nil dst.
var errWriterRequired = errors.New("capsule: a capsule of format 1 or 2 holds one content: dst is required")
// plainReader reads the plaintext of PAYLOAD_AGE and counts its bytes.
type plainReader struct {
r io.Reader
n uint64
}
func (p *plainReader) Read(b []byte) (int, error) {
n, err := p.r.Read(b)
p.n += uint64(n)
return n, err
}
// plaintextFailure is the error of a read of the plaintext that stopped
// before it wanted: a failure of age, or a plaintext that ends before P.
func plaintextFailure(err error, n, p uint64) error {
if err == io.EOF {
return fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is %d bytes, shorter than P = %d: %w", n, p, datekeys.ErrIntegrity)
}
return classify("PAYLOAD_AGE", ageStreamFailure, err)
}
// readN reads exactly n bytes of the plaintext. Its memory grows with the
// bytes received, not with n, a length that BODY declares (spec §57). Not
// io.ReadFull: it would turn the io.ErrUnexpectedEOF of a truncated STREAM
// into a short read.
func readN(r *plainReader, n int, p uint64) ([]byte, error) {
var buf []byte
part := make([]byte, min(n, 32<<10))
for len(buf) < n {
m, err := r.Read(part[:min(n-len(buf), len(part))])
buf = append(buf, part[:m]...)
if len(buf) == n {
break
}
if err != nil {
return nil, plaintextFailure(err, r.n, p)
}
}
return buf, nil
}
// drain reads the rest of the plaintext to EOF. A failure of age, or a
// plaintext whose length is not P, prevails over the failure of any substep
// of step 17 (spec §63).
func drain(r *plainReader, p uint64) error {
buf := make([]byte, 32<<10)
for {
_, err := r.Read(buf)
switch {
case err == io.EOF && r.n != p:
return fmt.Errorf("capsule: PAYLOAD_AGE: the plaintext is %d bytes, not P = %d: %w", r.n, p, datekeys.ErrIntegrity)
case err == io.EOF:
return nil
case err != nil:
return classify("PAYLOAD_AGE", ageStreamFailure, err)
}
}
}
// copyFile writes the next size bytes of the plaintext to w, hashing them.
func copyFile(w io.Writer, r *plainReader, size, p uint64) ([]byte, error) {
sum := sha256.New()
buf := make([]byte, 32<<10)
for left := size; left > 0; {
m := uint64(len(buf))
if m > left {
m = left
}
n, err := r.Read(buf[:m])
if n > 0 {
sum.Write(buf[:n])
if _, werr := w.Write(buf[:n]); werr != nil {
return nil, &sinkError{werr}
}
left -= uint64(n)
}
if left > 0 && err != nil {
return nil, plaintextFailure(err, r.n, p)
}
}
return sum.Sum(nil), nil
}
// sinkError is a failure of the caller's Sink, not of the capsule.
type sinkError struct{ err error }
func (e *sinkError) Error() string { return e.err.Error() }
func (e *sinkError) Unwrap() error { return e.err }
// sinkFailure reports a failure of the Sink as the caller's own error, with
// ErrIntegrity as its code, as a failure of dst is in formats 1 and 2.
func sinkFailure(what string, err error) error {
return fmt.Errorf("capsule: PAYLOAD_AGE: %s: %w: %w", what, err, datekeys.ErrIntegrity)
}
// openBody runs step 17 of a format 3 capsule and step 18 (spec §63): pr is
// the plaintext of PAYLOAD_AGE, whose BODY is l bytes long and whose
// padding goes up to p. The substeps, in order:
//
// 17.2 the frame of BODY and the area (§29.2), ErrIntegrity;
// 17.3 security, layers 2 and 3 (§29.3), without a code;
// 17.4 the head, layers 2 to 4 (§29.4 to §29.6);
// 17.5 the files fill CONTENT, ErrIntegrity;
// 17.6 the verdicts of security (§29.7), without a code;
// 17.7 the SHA-256 of each file, ErrIntegrity;
// 17.8 the padding, ErrIntegrity.
//
// A failure of age after its header, or a plaintext whose length is not P,
// is ErrIntegrity and prevails; otherwise the first substep that fails
// decides. openBody stops early only with ErrIntegrity when that is already
// the final code: on a failure of age, or of a substep of ErrIntegrity with
// no earlier failure of another code. After a failure of another code, at
// 17.4, it reads PAYLOAD_AGE to EOF before reporting it.
func openBody(pr io.Reader, l, p uint64, sink Sink, reg extension.Registry, out *Opened) (err error) {
r := &plainReader{r: pr}
begun := false
defer func() {
if err != nil && begun {
sink.Abort()
}
}()
// 17.2: the frame of BODY and the area.
b, err := readN(r, BodyFrameSize, p)
if err != nil {
return err
}
frame, err := ParseBodyFrame(b, l)
if err != nil {
return err
}
area, err := readN(r, int(frame.AreaLen), p)
if err != nil {
return err
}
if err := CheckArea(area, frame.SecurityLen); err != nil {
return err
}
out.AreaLen = frame.AreaLen
// 17.3 and 17.6: security never fails; its verdicts are shown after
// step 18 only.
verdicts := EvaluateSecurity(area[:frame.SecurityLen])
// 17.4: the head. Its codes other than ErrIntegrity are reported only
// after reading to EOF.
hb, err := readN(r, int(frame.HeadLen), p)
if err != nil {
return err
}
h, err := DecodeHead(hb, reg)
if err != nil {
if derr := drain(r, p); derr != nil {
return derr
}
return err
}
// 17.5: the files fill CONTENT.
if err := CheckHeadEnd(h, frame.ContentLength(l)); err != nil {
return err
}
// 17.7: each file, to the Sink, with its SHA-256.
if err := sink.Begin(h); err != nil {
return sinkFailure("beginning the files", err)
}
begun = true
for i := range h.Files {
f := &h.Files[i]
w, err := sink.Create(i)
if err != nil {
return sinkFailure(fmt.Sprintf("creating file %d", i+1), err)
}
sum, err := copyFile(w, r, f.Size, p)
if cerr := w.Close(); err == nil && cerr != nil {
err = &sinkError{cerr}
}
var se *sinkError
switch {
case errors.As(err, &se):
return sinkFailure(fmt.Sprintf("writing file %d", i+1), se.err)
case err != nil:
return err
case !hmac.Equal(sum, f.SHA256[:]):
return fmt.Errorf("capsule: PAYLOAD_AGE: file %d: its SHA-256 is not the one of the head: %w", i+1, datekeys.ErrIntegrity)
}
}
// 17.8: the padding, up to P, and nothing after it.
if err := checkPadding(r, r.n, p); err != nil {
if datekeys.Code(err) == "" {
err = classify("PAYLOAD_AGE", ageStreamFailure, err)
}
return err
}
// Step 18.
if err := sink.Commit(); err != nil {
return sinkFailure("committing the files", err)
}
out.Head, out.Verdicts = h, verdicts
out.UnusableHeadExtensions = extension.CheckNoncriticalIn(extension.Head, h.Noncritical, reg)
return nil
}

@ -0,0 +1,398 @@
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)
}
}
}

@ -28,8 +28,9 @@ import (
type Build struct {
Profile *profile.Profile // default Quicknet
Round uint64 // default 1000
// Format is the format written: capsule.Format2 by default, or
// capsule.Format1, the one of spec v0.8.2.
// Format is the format written: capsule.Format2 by default,
// capsule.Format1, the one of spec v0.8.2, or capsule.Format3, whose
// Plaintext is BODY (spec §29.2; see Body3).
Format capsule.Format
Declared capsule.Policy // access_policy written in PUBLIC_HEADER
Structure capsule.Policy // construction actually used
@ -45,6 +46,10 @@ type Build struct {
Plaintext []byte
// Padding is the padding rule of format 2; 0 means capsule.Reforzado.
Padding capsule.Padding
// EditPlaintext, when not nil, edits the plaintext of PAYLOAD_AGE, with
// its padding in formats 2 and 3, before it is encrypted. The control
// still declares L = len(Plaintext).
EditPlaintext func(p []byte) []byte
DateKeyString string // overrides the canonical dk1_ string in PUBLIC_HEADER
// RawPolicy, when not zero, is the access_policy value written in
// PUBLIC_HEADER instead of Declared, which may be outside V1. A header
@ -89,7 +94,7 @@ func (b Build) Make() (*Built, error) {
ctrl := &capsule.Control{Critical: b.ControlCritical, Noncritical: b.ControlNoncritical}
plaintext := b.Plaintext
access := b.AccessRecipients
if format == capsule.Format2 {
if format == capsule.Format2 || format == capsule.Format3 {
ctrl.PayloadLength, ctrl.Padding = uint64(len(b.Plaintext)), b.Padding
if ctrl.Padding == 0 {
ctrl.Padding = capsule.Reforzado
@ -105,6 +110,9 @@ func (b Build) Make() (*Built, error) {
}
}
}
if b.EditPlaintext != nil {
plaintext = b.EditPlaintext(bytes.Clone(plaintext))
}
out := &Built{}
if _, err := rand.Read(out.CapsuleID[:]); err != nil {
return nil, err

@ -0,0 +1,75 @@
package testkit
import (
"crypto/sha256"
"io"
"g.activething.com/go/DateKeys/capsule"
)
// DiscardSink is a capsule.Sink that discards the files.
type DiscardSink struct{}
func (DiscardSink) Begin(*capsule.Head) error { return nil }
func (DiscardSink) Create(int) (io.WriteCloser, error) { return nopCloser{io.Discard}, nil }
func (DiscardSink) Commit() error { return nil }
func (DiscardSink) Abort() {}
type nopCloser struct{ io.Writer }
func (nopCloser) Close() error { return nil }
// MemorySink is a capsule.Sink that keeps the files in memory.
type MemorySink struct {
Head *capsule.Head
Files [][]byte
Committed bool
Aborted bool
}
func (s *MemorySink) Begin(h *capsule.Head) error {
s.Head, s.Files = h, make([][]byte, len(h.Files))
return nil
}
func (s *MemorySink) Create(i int) (io.WriteCloser, error) { return &memoryFile{s, i}, nil }
func (s *MemorySink) Commit() error { s.Committed = true; return nil }
func (s *MemorySink) Abort() { s.Aborted = true }
type memoryFile struct {
s *MemorySink
i int
}
func (f *memoryFile) Write(b []byte) (int, error) {
f.s.Files[f.i] = append(f.s.Files[f.i], b...)
return len(b), nil
}
func (f *memoryFile) Close() error { return nil }
// Head3 returns the head of the given files, laid out one after another in
// the order given, with their SHA-256, no mtime and a zero salt.
func Head3(comment, author string, paths []string, contents [][]byte) *capsule.Head {
h := &capsule.Head{Comment: comment, Author: author}
var end uint64
for i, p := range paths {
n := uint64(len(contents[i]))
h.Files = append(h.Files, capsule.File{Path: p, Size: n, Start: end, End: end + n, SHA256: sha256.Sum256(contents[i])})
end += n
}
return h
}
// Body3 assembles the BODY of a format 3 capsule (spec §29.2): the frame,
// security in an area of areaLen bytes, the head and the files.
func Body3(areaLen uint32, security, head []byte, contents ...[]byte) []byte {
frame := capsule.BodyFrame{AreaLen: areaLen, SecurityLen: uint32(len(security)), HeadLen: uint32(len(head))}.Bytes()
body := append(frame[:], security...)
body = append(body, make([]byte, int(areaLen)-len(security))...)
body = append(body, head...)
for _, c := range contents {
body = append(body, c...)
}
return body
}

@ -27,12 +27,12 @@ import (
"g.activething.com/go/DateKeys/provider"
)
// Numbers of mutations of spec §64: the first two lists apply to both
// formats, and the third one is of format 2. Its first entry, VERSION 3, is
// Numbers of mutations of spec §64: the first two lists apply to formats 1
// and 2, and the third one is of format 2. Its first entry, VERSION 4, is
// the format 2 case of "version changed", in the first list.
const (
SpecMutationsPerFormat = 33
Format2SpecMutations = 22
Format2SpecMutations = 23
)
// Mutation is one entry of the mutation corpus (spec §64): a capsule, and
@ -166,6 +166,7 @@ func (in *MutationInput) Open() (Verdict, error) {
}
o.Identities = append(o.Identities, id)
}
o.Sink = DiscardSink{}
opened, err := capsule.Open(context.Background(), discard{}, bytes.NewReader(in.DKC), o)
v := Verdict{Err: err, Calls: src.calls}
if err == nil {
@ -530,11 +531,11 @@ func specMutations(f capsule.Format) []Mutation {
other := strings.Repeat("ab", 32)
return ok(to.input(bytes.Replace(to.DKC, []byte(profile.Quicknet().ChainHashHex()), []byte(other), 1)))
}},
// VERSION 3, which no format defines (spec §22, §23). In format 2 it is
// VERSION 4, which no format defines (spec §22, §23). In format 2 it is
// also the first entry of the third list of spec §64.
{Name: name("version changed"), Spec: true, Want: datekeys.ErrUnsupportedVersion, Step: 2,
Make: func(e *MutationEnv) (*MutationInput, error) {
return ok(e.timeOnly(f).input(set(e.timeOnly(f).DKC, 4, 3)))
return ok(e.timeOnly(f).input(set(e.timeOnly(f).DKC, 4, 4)))
}},
{Name: name("flags != 0"), Spec: true, Want: datekeys.ErrInvalidFlags, Step: 2,
Make: func(e *MutationEnv) (*MutationInput, error) {
@ -937,7 +938,7 @@ func paddedPlaintext(f *LoadedFixture, dir string) ([]byte, error) {
}
// format2Mutations returns the mutations of the third list of spec §64,
// except VERSION 3, which is "format 2: version changed", and the companions
// except VERSION 4, which is "format 2: version changed", and the companions
// of those that edit a time_and_key capsule: the same capsule with its .dkk,
// whose capsule_digest no longer matches, fails at step 9 (spec §43, §76).
// Every capsule derives from a fixture without randomness, sealed again with
@ -1003,6 +1004,10 @@ func format2Mutations() []Mutation {
two := func(e *MutationEnv) (*MutationInput, error) { return stanzasOf(e, twice) }
return []Mutation{
// Format 3 exists since v0.10: VERSION 3 passes step 2, and the
// version 2 control fails at step 14.
{Name: "format 2 time_only relabeled format 3", Spec: true, Want: datekeys.ErrUnsupportedVersion, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(relabeled(e.TimeOnly2, 3)) }},
{Name: "format 1 time_only relabeled format 2", Spec: true, Want: datekeys.ErrUnsupportedVersion, Step: 14, Network: true,
Make: func(e *MutationEnv) (*MutationInput, error) { return ok(relabeled(e.TimeOnly, 2)) }},
{Name: "format 1 time_and_key with one stanza relabeled format 2, with the identity", Spec: true, Want: datekeys.ErrPolicyStructureMismatch, Step: 12, Network: true, Make: relabel1},

23
testdata/README.md vendored

@ -40,7 +40,7 @@ Conventions for every file:
| `vectors/cbor.json` | the CBOR profile, and one block of vectors per schema, CONTROL_CBOR in both formats | §58, CDDL |
| `vectors/tlock_ibe.json` | H2 of the tlock IBE: the serialization of an element of GT | §63 step 11 |
| `vectors/padding.json` | the padding of format 2: P for each content length L, and the length of PAYLOAD_AGE | §29.1 |
| `vectors/mutations.json` | the mutation corpus: the 88 mutations of §64 and further cases | §63, §64 |
| `vectors/mutations.json` | the mutation corpus: the 89 mutations of §64 and further cases | §63, §64 |
| `vectors/inspect_differential.json` | 4380 mutations of the fixtures with the verdict of steps 1 to 8 | §63 |
| `fixtures/<name>.dkc`, `<name>.json` | official capsules and every intermediate value | §67 |
| `fixtures/<name>.dkk`, `<name>.dkk.json` | official access keys | §68 |
@ -74,7 +74,7 @@ decrypt offline.
edits of a base file, not as its full bytes:
```json
{ "base": "time_only.dkc", "edits": [[4, 1, "03"]] }
{ "base": "time_only.dkc", "edits": [[4, 1, "04"]] }
```
- `base` is a file of `testdata/fixtures`. In `mutations.json` it may be absent:
@ -267,7 +267,7 @@ reading flow (`capsule.Open`, §63) must fail.
{
"name": "version changed",
"spec": true,
"dkc": { "base": "time_only.dkc", "edits": [[4, 1, "03"]] },
"dkc": { "base": "time_only.dkc", "edits": [[4, 1, "04"]] },
"release": { "round": 1000, "signature": "b446…" },
"now": "2023-08-23T15:59:24Z",
"registry": "default",
@ -279,12 +279,12 @@ reading flow (`capsule.Open`, §63) must fail.
```
- `name`: unique, stable.
- `spec`: true for the 88 mutations listed in spec §64, false for the further
- `spec`: true for the 89 mutations listed in spec §64, false for the further
cases of the reference. The cases come in this order: the 33 mutations of
the first two lists of §64 on the format 1 fixtures (cases 1 to 33), 32
further cases (34 to 65), the same 33 mutations on the format 2 fixtures,
named "format 2: …" (66 to 98), the 22 of the third list (99 to 120), and 5
further cases (121 to 125).
named "format 2: …" (66 to 98), the 23 of the third list (99 to 121), and 5
further cases (122 to 126).
- `dkc`: the capsule, as edits of a fixture (see above). The reader gets it as a
seekable file, so that the `capsule_digest` of an offered `.dkk` is checked
before any release request (spec §63 step 9.a).
@ -373,12 +373,13 @@ x + p case has a frozen format 2 capsule for round 1004.
### Format 2: the third list of §64
The 22 cases of the third list of §64 test what format 2 adds:
The 23 cases of the third list of §64 test what format 2 adds:
- the format against the rest of the capsule, with the other `VERSION`, a
- the format against the rest of the capsule, with another `VERSION`, a
public byte (§22, §76): a format 1 `time_and_key` capsule of one stanza
relabeled format 2 fails at step 12, and the other three relabelings at step
14, where the schema version of the control is not the format;
relabeled format 2 fails at step 12, and the other relabelings, format 2 as
1 or 3 and format 1 as 2, at step 14, where the schema version of the
control is not the format;
- the 16 stanzas of INNER_ACCESS_AGE: 15, 17, two for one recipient, and an
identity that opens none of them, at steps 12 and 13;
- keys 6 and 7 of CONTROL_CBOR, at step 14;
@ -406,7 +407,7 @@ ends the flow, and within one object the first failing layer decides the code
| Step | Rules | Spec |
|---|---|---|
| 1, 2 | magic, truncated prelude, `VERSION` 1 or 2 (the format), FLAGS and RESERVED, `PUBLIC_HEADER_LEN` in 1 to 1048576 and `SEALED_CONTROL_LEN` in 1 to 67108864 | §22, §23 |
| 1, 2 | magic, truncated prelude, `VERSION` 1, 2 or 3 (the format), FLAGS and RESERVED, `PUBLIC_HEADER_LEN` in 1 to 1048576 and `SEALED_CONTROL_LEN` in 1 to 67108864 | §22, §23 |
| 3 | the PUBLIC_HEADER bytes are present | §23 |
| 4 | PUBLIC_HEADER: layers 2 to 4, the `public_header` decoder of the schema vectors, then the pinned profile of the DateKey and the critical extensions; with no extension known, every `critical_extensions` array fails | §63, §69.1 |
| 5 | the SEALED_CONTROL bytes are present, then its age header, parsed within `SEALED_CONTROL_LEN` bytes: malformed is `ERR_INTEGRITY`, one tlock stanza is required | §28.1 |

@ -84,7 +84,7 @@
"dkc": {
"base": "time_only.dkc",
"edits": [
[4, 1, "03"]
[4, 1, "04"]
]
},
"release": {
@ -1380,7 +1380,7 @@
"dkc": {
"base": "format2_time_only.dkc",
"edits": [
[4, 1, "03"]
[4, 1, "04"]
]
},
"release": {
@ -1947,6 +1947,26 @@
"error": "ERR_RELEASE_INVALID",
"step": 10
},
{
"name": "format 2 time_only relabeled format 3",
"spec": true,
"dkc": {
"base": "format2_time_only.dkc",
"edits": [
[4, 1, "03"]
]
},
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"now": "2023-08-23T15:59:24Z",
"registry": "default",
"network": true,
"frozen": false,
"error": "ERR_UNSUPPORTED_VERSION",
"step": 14
},
{
"name": "format 1 time_only relabeled format 2",
"spec": true,

Loading…
Cancel
Save

Powered by TurnKey Linux.