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

282 lines
13 KiB

package capsule_test
import (
"bytes"
"errors"
"strings"
"testing"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/extension"
)
// Spec §29.2: the frame of BODY and its limits. Every violation is
// ERR_INTEGRITY.
func TestBodyFrame(t *testing.T) {
frame := func(area, sec, head uint32) []byte {
b := capsule.BodyFrame{AreaLen: area, SecurityLen: sec, HeadLen: head}.Bytes()
return b[:]
}
// Valid, at the limits.
for _, c := range []struct {
area, sec, head uint32
l uint64
}{
{512, 22, 53, 577},
{512, 512, 1, 525},
{65536, 1, 1 << 24, 12 + 65536 + 1<<24},
{1024, 1024, 100, 1 << 40},
} {
f, err := capsule.ParseBodyFrame(frame(c.area, c.sec, c.head), c.l)
if err != nil {
t.Errorf("%+v: %v", c, err)
continue
}
if want := c.l - 12 - uint64(c.area) - uint64(c.head); f.ContentLength(c.l) != want {
t.Errorf("%+v: C = %d, want %d", c, f.ContentLength(c.l), want)
}
}
for name, c := range map[string]struct {
area, sec, head uint32
l uint64
}{
"L shorter than the frame": {512, 22, 53, 11},
"AREA_LEN 0": {0, 22, 53, 1000},
"AREA_LEN 511": {511, 22, 53, 1000},
"AREA_LEN 513": {513, 22, 53, 1000},
"AREA_LEN 66048": {66048, 22, 53, 100000},
"SECURITY_LEN 0": {512, 0, 53, 1000},
"SECURITY_LEN above AREA_LEN": {512, 513, 53, 1000},
"HEAD_LEN 0": {512, 22, 0, 1000},
"HEAD_LEN 2^24 + 1": {512, 22, 1<<24 + 1, 1 << 30},
"frame, area and head above L": {512, 22, 53, 576},
} {
if _, err := capsule.ParseBodyFrame(frame(c.area, c.sec, c.head), c.l); !errors.Is(err, datekeys.ErrIntegrity) {
t.Errorf("%s: %v", name, err)
}
}
area := make([]byte, 512)
copy(area, capsule.EncodeSecurity())
if err := capsule.CheckArea(area, 22); err != nil {
t.Fatal(err)
}
area[511] = 1
if err := capsule.CheckArea(area, 22); !errors.Is(err, datekeys.ErrIntegrity) {
t.Fatalf("a byte of the area that is not zero: %v", err)
}
}
// Spec §29.3, §29.7: the security area and its verdicts, which never fail.
func TestSecurityVerdicts(t *testing.T) {
empty := capsule.EncodeSecurity()
if len(empty) != 22 {
t.Fatalf("empty security is %d bytes, want 22", len(empty))
}
sig, err := capsule.EncodeAuthorSignature(1, make([]byte, 32), make([]byte, 64))
if err != nil {
t.Fatal(err)
}
if len(sig) != 105 {
t.Fatalf("an Ed25519 author-signature is %d bytes, want 105", len(sig))
}
seal, err := capsule.EncodeSeal(1, []byte{1, 2, 3})
if err != nil {
t.Fatal(err)
}
with := func(sig, seal []byte) []byte {
b, err := capsule.EncodeSecurityWith(sig, seal)
if err != nil {
t.Fatal(err)
}
return b
}
if n := len(with(sig, nil)); n != 130 {
t.Fatalf("security with a signature is %d bytes, want 130", n)
}
x := capsule.Verdicts{Signature: capsule.VerdictUnreadable, Seal: capsule.VerdictUnreadable}
for name, c := range map[string]struct {
b []byte
want capsule.Verdicts
}{
"empty": {empty, capsule.Verdicts{Signature: "F0", Seal: "S0"}},
"a signature of alg 1": {with(sig, nil), capsule.Verdicts{Signature: "F1", Seal: "S0"}},
"a seal of seal_type 1": {with(nil, seal), capsule.Verdicts{Signature: "F0", Seal: "S1"}},
"both": {with(sig, seal), capsule.Verdicts{Signature: "F1", Seal: "S1"}},
"alg 0": {with(mustMarshal(t, map[uint64]any{0: uint64(0), 1: []byte{}, 2: []byte{}}), nil), capsule.Verdicts{Signature: "F1", Seal: "S0"}},
"a signature that is no map": {with([]byte{0x01}, nil), capsule.Verdicts{Signature: "F1", Seal: "S0"}},
// The first row that holds decides: a seal with an unknown key and an
// unknown seal_type breaks its schema, S2, before its type is read.
"a seal with an unknown key": {with(nil, []byte{0xa3, 0x00, 0x07, 0x01, 0x41, 0x00, 0x02, 0x00}), capsule.Verdicts{Signature: "F0", Seal: "S2"}},
"seal_type 0": {with(nil, mustMarshal(t, map[uint64]any{0: uint64(0), 1: []byte{1}})), capsule.Verdicts{Signature: "F0", Seal: "S2"}},
"a seal that is not CBOR": {with(sig, []byte{0xff}), capsule.Verdicts{Signature: "F1", Seal: "S2"}},
"version 2": {mustMarshal(t, map[uint64]any{0: "datekeys-security", 1: uint64(2)}), x},
"another type tag": {mustMarshal(t, map[uint64]any{0: "datekeys-head", 1: uint64(1)}), x},
"an unknown key 4": {mustMarshal(t, map[uint64]any{0: "datekeys-security", 1: uint64(1), 4: []byte{1}}), x},
"key 2 not a byte string": {mustMarshal(t, map[uint64]any{0: "datekeys-security", 1: uint64(1), 2: uint64(1)}), x},
"an empty key 2": {mustMarshal(t, map[uint64]any{0: "datekeys-security", 1: uint64(1), 2: []byte{}}), x},
"a byte more": {append(bytes.Clone(empty), 0), x},
"not CBOR": {[]byte("security"), x},
} {
if got := capsule.EvaluateSecurity(c.b); got != c.want {
t.Errorf("%s: %+v, want %+v", name, got, c.want)
}
}
if got := x.Lines(); len(got) != 1 || !strings.HasPrefix(got[0], "No se han podido") {
t.Errorf("X shows %q", got)
}
if got := (capsule.Verdicts{Signature: "F0", Seal: "S0"}).Lines(); len(got) != 1 || got[0] != "Sin firma de autor." {
t.Errorf("F0 and S0 show %q", got)
}
if got := (capsule.Verdicts{Signature: "F1", Seal: "S1"}).Lines(); len(got) != 2 {
t.Errorf("F1 and S1 show %q", got)
}
}
func sampleHead() *capsule.Head {
h := &capsule.Head{
Comment: "Para ti ❤\ufe0f",
Author: "Ana López",
Files: []capsule.File{
{Path: "fotos/playa.jpg", Size: 10, Start: 0, End: 10, MTime: 1759190400, HasMTime: true},
{Path: "nota.txt", Size: 5, Start: 10, End: 15},
},
}
h.Salt[0] = 1
h.Files[0].SHA256[0] = 2
return h
}
// Spec §29.4: a head round-trips, and its sizes are those of §29.2.
func TestHeadRoundTrip(t *testing.T) {
h := sampleHead()
b, err := capsule.EncodeHead(h)
if err != nil {
t.Fatal(err)
}
got, err := capsule.DecodeHead(b, nil)
if err != nil {
t.Fatal(err)
}
if got.Comment != h.Comment || got.Author != h.Author || len(got.Files) != 2 || got.Files[0] != h.Files[0] || got.Files[1] != h.Files[1] || got.Salt != h.Salt {
t.Fatalf("round trip: %+v", got)
}
if err := capsule.CheckHeadEnd(got, 15); err != nil {
t.Fatal(err)
}
if err := capsule.CheckHeadEnd(got, 16); !errors.Is(err, datekeys.ErrIntegrity) {
t.Fatalf("files that do not fill the content: %v", err)
}
if err := capsule.CheckHeadEnd(&capsule.Head{}, 0); err != nil {
t.Fatal(err)
}
for _, c := range []struct {
h capsule.Head
size int
}{
{capsule.Head{}, 53},
{capsule.Head{Comment: "x"}, 56},
{capsule.Head{Files: []capsule.File{{Path: "nota.txt", Size: 1000, End: 1000, MTime: 1759190400, HasMTime: true}}}, 117},
{capsule.Head{Files: []capsule.File{{Path: "a"}}}, 100},
} {
b, err := capsule.EncodeHead(&c.h)
if err != nil {
t.Fatal(err)
}
if len(b) != c.size {
t.Errorf("%+v: %d bytes, want %d", c.h, len(b), c.size)
}
}
}
// head builds HEAD_CBOR with the test encoder, for heads that the capsule
// encoder refuses to write.
func head(t *testing.T, fields map[uint64]any) []byte {
m := map[uint64]any{0: "datekeys-head", 1: uint64(1), 2: make([]byte, 32)}
for k, v := range fields {
if v == nil {
delete(m, k)
} else {
m[k] = v
}
}
return mustMarshal(t, m)
}
func file(path string, size, start, end uint64) map[uint64]any {
return map[uint64]any{0: path, 1: size, 2: start, 3: end, 4: make([]byte, 32)}
}
// Spec §29.4, §69.1: the layers of the head and their codes, and the first
// failing layer decides.
func TestDecodeHeadLayers(t *testing.T) {
many := make([]any, 65536)
for i := range many {
many[i] = file(strings.Repeat("a", 1)+string(rune('a'+i%26))+strings.Repeat("x", i/26%3), 0, 0, 0)
}
for name, c := range map[string]struct {
b []byte
want error
}{
// Layer 2.
"another type tag": {mustMarshal(t, map[uint64]any{0: "datekeys-control", 1: uint64(1), 2: make([]byte, 32)}), datekeys.ErrNonCanonicalCBOR},
"version 2": {mustMarshal(t, map[uint64]any{0: "datekeys-head", 1: uint64(2), 2: make([]byte, 32)}), datekeys.ErrUnsupportedVersion},
"version 2 and ..": {mustMarshal(t, map[uint64]any{0: "datekeys-head", 1: uint64(2), 2: make([]byte, 32), 5: []any{file("..", 0, 0, 0)}}), datekeys.ErrUnsupportedVersion},
// Layer 3.
"no salt": {head(t, map[uint64]any{2: nil}), datekeys.ErrNonCanonicalCBOR},
"a salt of 31 bytes": {head(t, map[uint64]any{2: make([]byte, 31)}), datekeys.ErrNonCanonicalCBOR},
"an empty comment": {head(t, map[uint64]any{3: ""}), datekeys.ErrNonCanonicalCBOR},
"a comment of 16385 bytes": {head(t, map[uint64]any{3: strings.Repeat("a", 16385)}), datekeys.ErrNonCanonicalCBOR},
"an author of 257 bytes": {head(t, map[uint64]any{4: strings.Repeat("a", 257)}), datekeys.ErrNonCanonicalCBOR},
"an empty array of files": {head(t, map[uint64]any{5: []any{}}), datekeys.ErrNonCanonicalCBOR},
"65536 files": {head(t, map[uint64]any{5: many}), datekeys.ErrNonCanonicalCBOR},
"R1: an empty path": {head(t, map[uint64]any{5: []any{file("", 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR},
"R1: a path of 1025 bytes": {head(t, map[uint64]any{5: []any{file(strings.Repeat("a", 1025), 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR},
"R8: b before a": {head(t, map[uint64]any{5: []any{file("b", 0, 0, 0), file("a", 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR},
"R8: a repeated path": {head(t, map[uint64]any{5: []any{file("a", 0, 0, 0), file("a", 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR},
"R8 before R3: b/.. and a": {head(t, map[uint64]any{5: []any{file("b/..", 0, 0, 0), file("a", 0, 0, 0)}}), datekeys.ErrNonCanonicalCBOR},
"a size above L_MAX": {head(t, map[uint64]any{5: []any{file("a", capsule.MaxPayloadLength+1, 0, 0)}}), datekeys.ErrNonCanonicalCBOR},
"an mtime after 9999": {head(t, map[uint64]any{5: []any{map[uint64]any{0: "a", 1: uint64(0), 2: uint64(0), 3: uint64(0), 4: make([]byte, 32), 5: uint64(253402300800)}}}), datekeys.ErrNonCanonicalCBOR},
"an unknown key 8": {head(t, map[uint64]any{8: uint64(1)}), datekeys.ErrNonCanonicalCBOR},
"a byte more": {append(head(t, nil), 0), datekeys.ErrNonCanonicalCBOR},
// Layer 4, in key order.
"a comment with U+202E": {head(t, map[uint64]any{3: "a\u202eb"}), datekeys.ErrHeadInvalid},
"a comment with the tag U+E0041": {head(t, map[uint64]any{3: "a\U000E0041"}), datekeys.ErrHeadInvalid},
"an author with LF": {head(t, map[uint64]any{4: "a\nb"}), datekeys.ErrHeadInvalid},
"R3: ..": {head(t, map[uint64]any{5: []any{file("..", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"R2: /a": {head(t, map[uint64]any{5: []any{file("/a", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"R4b: a and VS16": {head(t, map[uint64]any{5: []any{file("a\ufe0f", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"R6: CON.txt": {head(t, map[uint64]any{5: []any{file("CON.txt", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"R10: .datekeys-x": {head(t, map[uint64]any{5: []any{file(".datekeys-x", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"layout: a first start that is not 0": {head(t, map[uint64]any{5: []any{file("a", 1, 1, 2)}}), datekeys.ErrHeadInvalid},
"layout: end minus start is not size": {head(t, map[uint64]any{5: []any{file("a", 2, 0, 1)}}), datekeys.ErrHeadInvalid},
"layout: a gap": {head(t, map[uint64]any{5: []any{file("a", 1, 0, 1), file("b", 1, 2, 3)}}), datekeys.ErrHeadInvalid},
"R7: A.txt and a.txt": {head(t, map[uint64]any{5: []any{file("A.txt", 0, 0, 0), file("a.txt", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"a comment and a path that break": {head(t, map[uint64]any{3: "a\u202e", 5: []any{file("..", 0, 0, 0)}}), datekeys.ErrHeadInvalid},
"a path that breaks, then an unknown critical extension": {head(t, map[uint64]any{5: []any{file("..", 0, 0, 0)}, 6: []any{map[uint64]any{0: "x.example", 1: uint64(1)}}}), datekeys.ErrHeadInvalid},
"an unknown critical extension": {head(t, map[uint64]any{6: []any{map[uint64]any{0: "x.example", 1: uint64(1)}}}), datekeys.ErrExtensionCriticalUnknown},
} {
if _, err := capsule.DecodeHead(c.b, nil); !errors.Is(err, c.want) {
t.Errorf("%s: %v, want %v", name, err, c.want)
} else if n := codes(err); n != 1 {
t.Errorf("%s: %d normative codes in %v", name, n, err)
}
}
// A known critical extension of the head passes.
b := head(t, map[uint64]any{6: []any{map[uint64]any{0: "x.example", 1: uint64(1)}}})
if _, err := capsule.DecodeHead(b, extension.Set{"x.example": {1}}); err != nil {
t.Fatal(err)
}
}
// codes counts the normative errors err wraps.
func codes(err error) int {
n := 0
for _, e := range datekeys.All() {
if errors.Is(err, e) {
n++
}
}
return n
}

Powered by TurnKey Linux.