|
|
|
|
package capsule_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Spec v0.11 §24.1, §62.1 rule 23: the public note is in PUBLIC_HEADER, in
|
|
|
|
|
// clear, and header_binding ties it to the control: changing it makes step 15
|
|
|
|
|
// fail.
|
|
|
|
|
func TestPublicNote(t *testing.T) {
|
|
|
|
|
opts := files3(t)
|
|
|
|
|
opts.PublicNote = "Cartas del viaje a Lisboa"
|
|
|
|
|
var dkc bytes.Buffer
|
|
|
|
|
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("nota.txt", "Hola.\n")}, opts); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
in, err := capsule.Inspect(bytes.NewReader(dkc.Bytes()), capsule.InspectOptions{Registry: testkit.Registry()})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if note, ok := in.Header.PublicNote(); !ok || note != "Cartas del viaje a Lisboa" {
|
|
|
|
|
t.Errorf("the note: %q %v", note, ok)
|
|
|
|
|
}
|
|
|
|
|
// The same bytes with another letter in the note: it still inspects, and
|
|
|
|
|
// Open fails at step 15 with ERR_HEADER_BINDING.
|
|
|
|
|
changed := bytes.Replace(dkc.Bytes(), []byte("Lisboa"), []byte("Lisbon"), 1)
|
|
|
|
|
if bytes.Equal(changed, dkc.Bytes()) {
|
|
|
|
|
t.Fatal("the note is not in clear in the capsule")
|
|
|
|
|
}
|
|
|
|
|
o := defaultOpen(1000)
|
|
|
|
|
o.Sink = &testkit.MemorySink{}
|
|
|
|
|
if _, err := capsule.Open(context.Background(), nil, bytes.NewReader(changed), o); !errors.Is(err, datekeys.ErrHeaderBinding) {
|
|
|
|
|
t.Errorf("a note changed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := capsule.Open(context.Background(), nil, bytes.NewReader(dkc.Bytes()), o); err != nil {
|
|
|
|
|
t.Errorf("the capsule with its note: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPublicNoteRules(t *testing.T) {
|
|
|
|
|
for name, text := range map[string]string{
|
|
|
|
|
"a tab": "a\tb",
|
|
|
|
|
"a line feed": "a\nb",
|
|
|
|
|
"a space at the end": "a ",
|
|
|
|
|
"a space at the start": " a",
|
|
|
|
|
"too long": strings.Repeat("a", extension.MaxNoteLen+1),
|
|
|
|
|
"not UTF-8": "\xff",
|
|
|
|
|
} {
|
|
|
|
|
if err := extension.CheckNote(text); err == nil {
|
|
|
|
|
t.Errorf("%s: accepted", name)
|
|
|
|
|
}
|
|
|
|
|
opts := files3(t)
|
|
|
|
|
opts.PublicNote = text
|
|
|
|
|
if _, err := capsule.EncryptFiles(&bytes.Buffer{}, []capsule.Source{source("a", "x")}, opts); err == nil {
|
|
|
|
|
t.Errorf("%s: written", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := extension.CheckNote(strings.Repeat("a", extension.MaxNoteLen)); err != nil {
|
|
|
|
|
t.Errorf("1024 bytes: %v", err)
|
|
|
|
|
}
|
|
|
|
|
// An unusable note shows nothing.
|
|
|
|
|
if _, ok := extension.Note([]extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("a\nb")}}); ok {
|
|
|
|
|
t.Error("an unusable note was shown")
|
|
|
|
|
}
|
|
|
|
|
// The registry knows it only in PUBLIC_HEADER, noncritical.
|
|
|
|
|
var reg extension.Standard
|
|
|
|
|
if !extension.KnownIn(reg, extension.NoteID, 1, extension.PublicHeader, extension.Noncritical) ||
|
|
|
|
|
extension.KnownIn(reg, extension.NoteID, 1, extension.Control, extension.Noncritical) ||
|
|
|
|
|
extension.KnownIn(reg, extension.NoteID, 1, extension.PublicHeader, extension.Critical) ||
|
|
|
|
|
extension.KnownIn(reg, extension.NoteID, 2, extension.PublicHeader, extension.Noncritical) {
|
|
|
|
|
t.Error("registration of datekeys.note")
|
|
|
|
|
}
|
|
|
|
|
if us := extension.CheckNoncriticalIn(extension.PublicHeader, []extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("a\tb")}}, reg); len(us) != 1 {
|
|
|
|
|
t.Errorf("the note with a tab is not unusable: %v", us)
|
|
|
|
|
}
|
|
|
|
|
}
|
Review fixes: author keys, the writer, the CLI, extensions and the locator
Fixes of the review of the session of 1 and 2 October that the text of
spec v0.11 already asks for:
- authorkey: String and GoString hide the secret key, which only Secret
returns; ParsePublic refuses a key that is not a point of the curve
(ed25519strict.OnCurve, checked against the square root of testkit).
- capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never
a capsule without the signature or the seal that was asked for. A panic
while evaluating the signature or the seal fails only that part, F1 or
S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can
refuse to publish the files.
- extension.CheckWrite, the rule of encoders of spec 72: the writers of
capsules and .dkk files refuse datekeys.note and datekeys.capsule outside
the arrays where they are registered, or with invalid data.
- CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE
before it signs (rule 20); decrypt -expect-author compares the key of an
F4 and writes nothing unless it matches; decrypt notifies a public note
that it does not show; the lines of the verdicts break at the last space
that fits, each row after the first behind a mark, so that the terminal
never breaks them; L is the payload, not the content.
- locator: a reader rejects an address that breaks 44.1 and keeps the
others; addresses refuse the special-purpose blocks of IANA, IPv6 outside
2000::/3, localhost and local names, characters outside RFC 3986, dot
segments, and a CID that does not decode to version 1 and a multihash;
ParseInfo checks that the locator is an age file with one tlock stanza
for the round of its DateKey; Info.Extension reads what it writes; its
errors carry no normative code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
|
|
|
|
|
// Review: spec §72 forbids an encoder to write a registered extension where
|
|
|
|
|
// it is not registered. A note given as an extension is checked like
|
|
|
|
|
// PublicNote, and no array of a capsule but the noncritical one of
|
|
|
|
|
// PUBLIC_HEADER takes it. In CONTROL_CBOR, critical, it would have inspected
|
|
|
|
|
// well and failed after the date at step 14. datekeys.capsule goes in none.
|
|
|
|
|
func TestRegisteredExtensionsWhereRegistered(t *testing.T) {
|
|
|
|
|
note := extension.Extension{ID: extension.NoteID, Version: 1, Data: []byte("Cartas")}
|
|
|
|
|
tab := extension.Extension{ID: extension.NoteID, Version: 1, Data: []byte("a\tb")}
|
|
|
|
|
capsuleExt := extension.Extension{ID: extension.CapsuleID, Version: 1, Data: []byte{0xa0}}
|
|
|
|
|
for name, set := range map[string]func(*capsule.EncryptOptions){
|
|
|
|
|
"a note with a tab": func(o *capsule.EncryptOptions) { o.Noncritical = []extension.Extension{tab} },
|
|
|
|
|
"a note in critical": func(o *capsule.EncryptOptions) { o.Critical = []extension.Extension{note} },
|
|
|
|
|
"a note in the control": func(o *capsule.EncryptOptions) { o.ControlNoncritical = []extension.Extension{note} },
|
|
|
|
|
"a note in the control, critical": func(o *capsule.EncryptOptions) { o.ControlCritical = []extension.Extension{note} },
|
|
|
|
|
"a note in the head": func(o *capsule.EncryptOptions) { o.HeadNoncritical = []extension.Extension{note} },
|
|
|
|
|
"datekeys.capsule": func(o *capsule.EncryptOptions) { o.Noncritical = []extension.Extension{capsuleExt} },
|
|
|
|
|
} {
|
|
|
|
|
opts := files3(t)
|
|
|
|
|
set(&opts)
|
|
|
|
|
if _, err := capsule.EncryptFiles(&bytes.Buffer{}, []capsule.Source{source("a", "x")}, opts); err == nil {
|
|
|
|
|
t.Errorf("%s: written", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
opts := files3(t)
|
|
|
|
|
opts.Noncritical = []extension.Extension{note}
|
|
|
|
|
var dkc bytes.Buffer
|
|
|
|
|
if _, err := capsule.EncryptFiles(&dkc, []capsule.Source{source("a", "x")}, opts); err != nil {
|
|
|
|
|
t.Fatalf("a valid note as an extension: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Review: a reader tells a note that breaks the rules of §24.1 from no note,
|
|
|
|
|
// so that it can say that it does not show one.
|
|
|
|
|
func TestUnusableNote(t *testing.T) {
|
|
|
|
|
for _, c := range []struct {
|
|
|
|
|
exts []extension.Extension
|
|
|
|
|
want bool
|
|
|
|
|
}{
|
|
|
|
|
{nil, false},
|
|
|
|
|
{[]extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("Cartas")}}, false},
|
|
|
|
|
{[]extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("a\tb")}}, true},
|
|
|
|
|
{[]extension.Extension{{ID: extension.NoteID, Version: 2, Data: []byte("a\tb")}}, false},
|
|
|
|
|
} {
|
|
|
|
|
h := &capsule.Header{Noncritical: c.exts}
|
|
|
|
|
if got := h.UnusableNote(); got != c.want {
|
|
|
|
|
t.Errorf("%+v: %v, want %v", c.exts, got, c.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|