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

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"
"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 ❤️",
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‮b"}), 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️", 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‮", 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, ERR_HEAD_INVALID included.
func codes(err error) int {
n := 0
for _, e := range append(datekeys.All(), datekeys.ErrHeadInvalid) {
if errors.Is(err, e) {
n++
}
}
return n
}

Powered by TurnKey Linux.