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.
562 lines
22 KiB
562 lines
22 KiB
package capsule_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"math/rand/v2"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"filippo.io/age"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
"g.activething.com/go/DateKeys/extension"
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
)
|
|
|
|
// 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 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 {
|
|
f := loadFixture(t, name)
|
|
var out bytes.Buffer
|
|
opened, err := capsule.Open(context.Background(), &out, bytes.NewReader(f.dkc), f.openOptions(t))
|
|
if err != nil || opened.Format != f.format() || opened.Inspection.Prelude.Format != f.format() {
|
|
t.Fatalf("%s: format %d, %v", name, opened.Format, err)
|
|
}
|
|
}
|
|
f := loadFixture(t, "format2_time_only")
|
|
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))})
|
|
expectStep(t, "VERSION "+string(rune('0'+v%10)), step, err, datekeys.ErrUnsupportedVersion, 2)
|
|
if calls != 0 {
|
|
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 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 {
|
|
c := bytes.Clone(dkc)
|
|
c[4] = v
|
|
return c
|
|
}
|
|
identity := func(f *fixture) []age.Identity { return f.credentials(t)[:1] }
|
|
to1, tk1 := loadFixture(t, "time_only"), loadFixture(t, "time_and_key_portable")
|
|
to2, tk2 := loadFixture(t, "format2_time_only"), loadFixture(t, "format2_time_and_key_portable")
|
|
|
|
// A format 1 capsule with exactly 16 stanzas, as only a generator of test
|
|
// vectors writes it: the stanza count passes step 12 of format 2, and the
|
|
// version of its control fails step 14.
|
|
stranger := testkit.Stranger()
|
|
sixteen := []age.Recipient{stranger.Recipient()}
|
|
for range 15 {
|
|
id, _ := age.GenerateX25519Identity()
|
|
sixteen = append(sixteen, id.Recipient())
|
|
}
|
|
b, err := testkit.Build{Format: capsule.Format1, Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey,
|
|
AccessRecipients: sixteen, Plaintext: []byte("sixteen")}.Make()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
dkc []byte
|
|
ids []age.Identity
|
|
now *fixture
|
|
want error
|
|
step int
|
|
}{
|
|
{"format 2 time_only relabeled 1", relabel(to2.dkc, 1), nil, to2, datekeys.ErrUnsupportedVersion, 14},
|
|
{"format 2 time_and_key relabeled 1", relabel(tk2.dkc, 1), identity(tk2), tk2, datekeys.ErrUnsupportedVersion, 14},
|
|
{"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},
|
|
} {
|
|
// 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))
|
|
}
|
|
step, _, err := openStep(t, tc.dkc, o)
|
|
expectStep(t, tc.name, step, err, tc.want, tc.step)
|
|
}
|
|
// Unrelabeled, the format 1 capsule with 16 stanzas opens.
|
|
if got, err := open(t, b.DKC, capsule.OpenOptions{Registry: testkit.Registry(), Source: testkit.NewSource(testkit.Release(1000)),
|
|
Now: testkit.Fixed(testkit.Genesis().AddDate(1, 0, 0)), Identities: []age.Identity{stranger}}); err != nil || string(got) != "sixteen" {
|
|
t.Fatalf("format 1 with 16 stanzas: %v", err)
|
|
}
|
|
}
|
|
|
|
// Spec §33, §70: format 1 keeps the rules of v0.8.2, one or more stanzas and
|
|
// no padding, and format 2 requires exactly 16 stanzas.
|
|
func TestFormat1Compatibility(t *testing.T) {
|
|
stranger := testkit.Stranger()
|
|
recipients := []age.Recipient{stranger.Recipient()}
|
|
for range 16 {
|
|
id, _ := age.GenerateX25519Identity()
|
|
recipients = append(recipients, id.Recipient())
|
|
}
|
|
o := capsule.OpenOptions{Identities: []age.Identity{stranger}}
|
|
for _, tc := range []struct {
|
|
name string
|
|
b testkit.Build
|
|
want error
|
|
step int
|
|
output string
|
|
}{
|
|
{"format 1 with 17 stanzas", testkit.Build{Format: capsule.Format1, Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey, AccessRecipients: recipients}, nil, 0, "malicious creator"},
|
|
{"format 1 with one stanza", testkit.Build{Format: capsule.Format1, Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey, AccessRecipients: recipients[:1]}, nil, 0, "malicious creator"},
|
|
{"format 2 with 17 stanzas", testkit.Build{Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey, AccessRecipients: recipients, Slots: 17}, datekeys.ErrPolicyStructureMismatch, 12, ""},
|
|
{"format 2 with one stanza", testkit.Build{Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey, AccessRecipients: recipients[:1], Slots: 1}, datekeys.ErrPolicyStructureMismatch, 12, ""},
|
|
{"format 2 with 16 stanzas", testkit.Build{Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey, AccessRecipients: recipients[:1]}, nil, 0, "malicious creator"},
|
|
} {
|
|
dkc, _ := build(t, tc.b)
|
|
step, _, err := openStep(t, dkc, o)
|
|
if tc.want == nil {
|
|
if err != nil {
|
|
t.Errorf("%s: %v", tc.name, err)
|
|
}
|
|
continue
|
|
}
|
|
expectStep(t, tc.name, step, err, tc.want, tc.step)
|
|
}
|
|
// The plaintext of a format 1 PAYLOAD_AGE is the content, without padding.
|
|
b, err := testkit.Build{Format: capsule.Format1, Plaintext: []byte("x")}.Make()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(b.Payload) != int(capsule.PayloadAgeLength(1)) {
|
|
t.Fatalf("format 1 PAYLOAD_AGE of %d bytes", len(b.Payload))
|
|
}
|
|
}
|
|
|
|
// Spec §29.1: the two rules, exact on integers, their properties and their
|
|
// limits, and the length of PAYLOAD_AGE they give.
|
|
func TestPaddingRules(t *testing.T) {
|
|
check := func(l uint64) {
|
|
b, err1 := capsule.PaddedLength(l, capsule.Bloque256)
|
|
r, err2 := capsule.PaddedLength(l, capsule.Reforzado)
|
|
if err := errors.Join(err1, err2); err != nil {
|
|
t.Fatalf("L = %d: %v", l, err)
|
|
}
|
|
for _, p := range []uint64{b, r} {
|
|
if p%256 != 0 || p < 256 || p < l {
|
|
t.Fatalf("L = %d: P = %d", l, p)
|
|
}
|
|
}
|
|
if l > 256 && b-l >= 256 {
|
|
t.Fatalf("L = %d: bloque256 adds %d bytes", l, b-l)
|
|
}
|
|
if l <= 8192 && b != r {
|
|
t.Fatalf("L = %d: bloque256 %d, reforzado %d", l, b, r)
|
|
}
|
|
if r < b {
|
|
t.Fatalf("L = %d: reforzado %d below bloque256 %d", l, r, b)
|
|
}
|
|
// Above 8192, reforzado adds less than L / 2^S.
|
|
if e := uint64(63 - clz(l)); l > 8192 && r-l >= l>>bitlen(e) {
|
|
t.Fatalf("L = %d: reforzado adds %d bytes", l, r-l)
|
|
}
|
|
}
|
|
for l := range uint64(20000) {
|
|
check(l)
|
|
}
|
|
rng := rand.New(rand.NewPCG(20260929, 1))
|
|
for range 20000 {
|
|
check(rng.Uint64N(capsule.MaxPayloadLength + 1))
|
|
}
|
|
check(capsule.MaxPayloadLength)
|
|
if b, _ := capsule.PaddedLength(8193, capsule.Bloque256); b != 8448 {
|
|
t.Fatalf("bloque256(8193) = %d", b)
|
|
}
|
|
if r, _ := capsule.PaddedLength(8193, capsule.Reforzado); r != 8704 {
|
|
t.Fatalf("reforzado(8193) = %d", r)
|
|
}
|
|
for _, tc := range []struct {
|
|
l uint64
|
|
p capsule.Padding
|
|
}{{0, 0}, {0, 3}, {capsule.MaxPayloadLength + 1, capsule.Bloque256}, {capsule.MaxPayloadLength + 1, capsule.Reforzado}, {1 << 63, capsule.Reforzado}} {
|
|
if _, err := capsule.PaddedLength(tc.l, tc.p); err == nil {
|
|
t.Errorf("L = %d, code %d accepted", tc.l, tc.p)
|
|
}
|
|
}
|
|
if capsule.Bloque256.String() != "bloque256" || capsule.Reforzado.String() != "reforzado" || capsule.Padding(9).String() != "padding(9)" {
|
|
t.Fatal("padding names")
|
|
}
|
|
|
|
// PAYLOAD_AGE: 184 + P + 16·max(1, ⌈P / 65536⌉), checked against age.
|
|
id, _ := age.GenerateX25519Identity()
|
|
for _, p := range []uint64{0, 256, 65536, 65536 + 256, 2 * 65536, 79872} {
|
|
file, _, err := testkit.Encrypt(make([]byte, p), id.Recipient())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if uint64(len(file)) != capsule.PayloadAgeLength(p) {
|
|
t.Fatalf("P = %d: age writes %d bytes, the formula gives %d", p, len(file), capsule.PayloadAgeLength(p))
|
|
}
|
|
}
|
|
}
|
|
|
|
func clz(x uint64) int {
|
|
n := 0
|
|
for i := 63; i >= 0 && x&(1<<i) == 0; i-- {
|
|
n++
|
|
}
|
|
return n
|
|
}
|
|
|
|
func bitlen(x uint64) uint64 { return uint64(64 - clz(x)) }
|
|
|
|
// Spec §29.1, §56, §63 step 17: the reader checks the length and the padding
|
|
// of the plaintext, whatever the moment it finds a fault, writes the content
|
|
// only and never the padding.
|
|
func TestPaddingChecksAtStep17(t *testing.T) {
|
|
f, err := testkit.LoadFixture(fixtureDir, "format2_time_only_extensions")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fx := loadFixture(t, "format2_time_only_extensions")
|
|
l, p := int(f.PayloadLength), int(f.PaddedLength)
|
|
padded := func() []byte { return append(bytes.Clone(fx.plaintext), make([]byte, p-l)...) }
|
|
set := func(i int, v byte) []byte { b := padded(); b[i] = v; return b }
|
|
for _, tc := range []struct {
|
|
name string
|
|
plaintext []byte
|
|
ok bool
|
|
}{
|
|
{"the content and its padding, sealed again", padded(), true},
|
|
{"first padding byte not zero", set(l, 0x01), false},
|
|
{"a padding byte not zero", set(100, 0x80), false},
|
|
{"last padding byte not zero", set(p-1, 0x01), false},
|
|
{"P - 1 bytes", padded()[:p-1], false},
|
|
{"P + 1 bytes", append(padded(), 0), false},
|
|
{"2P bytes", append(padded(), make([]byte, p)...), false},
|
|
{"L bytes, without padding", padded()[:l], false},
|
|
{"no plaintext", nil, false},
|
|
} {
|
|
in, err := f.WithPayloadPlaintext(tc.plaintext)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out bytes.Buffer
|
|
opened, err := capsule.Open(context.Background(), &out, bytes.NewReader(in.DKC), fx.openOptions(t))
|
|
if tc.ok {
|
|
if err != nil || !bytes.Equal(out.Bytes(), fx.plaintext) || opened.PayloadLength != uint64(l) {
|
|
t.Errorf("%s: %v", tc.name, err)
|
|
}
|
|
continue
|
|
}
|
|
expectStep(t, tc.name, failedStep(t, opened.Inspection.Checks, err), err, datekeys.ErrIntegrity, 17)
|
|
if out.Len() > l || !bytes.HasPrefix(fx.plaintext, out.Bytes()) {
|
|
t.Errorf("%s: %d bytes written, not a prefix of the content", tc.name, out.Len())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spec §67: a padding that spans several STREAM chunks, generated at run
|
|
// time rather than frozen in a 5 MB fixture: L = 5 000 000 with code 2 gives
|
|
// P = 5 111 808, more than a chunk of zeros after the content.
|
|
func TestPaddingAcrossChunks(t *testing.T) {
|
|
content := bytes.Repeat([]byte("0123456789"), 500000)
|
|
b, err := testkit.Build{Plaintext: content, Padding: capsule.Reforzado}.Make()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o := capsule.OpenOptions{Registry: testkit.Registry(), Source: testkit.NewSource(testkit.Release(1000)), Now: testkit.Fixed(testkit.Genesis().AddDate(1, 0, 0))}
|
|
var out bytes.Buffer
|
|
opened, err := capsule.Open(context.Background(), &out, bytes.NewReader(b.DKC), o)
|
|
if err != nil || !bytes.Equal(out.Bytes(), content) || opened.PaddedLength != 5111808 {
|
|
t.Fatalf("P = %d: %v", opened.PaddedLength, err)
|
|
}
|
|
|
|
// The last byte of the padding, in the last chunk, not zero.
|
|
id, err := agewrap.NewPayloadIdentity(b.PayloadIdentity)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stanzas, err := agewrap.Stanzas(bytes.NewReader(b.Payload))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fk, err := id.Unwrap(stanzas)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plaintext := append(bytes.Clone(content), make([]byte, 5111808-len(content))...)
|
|
plaintext[len(plaintext)-1] = 1
|
|
payload, err := testkit.ResealAge(b.Payload, fk, plaintext)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out.Reset()
|
|
opened, err = capsule.Open(context.Background(), &out, bytes.NewReader(testkit.Join(b.Prelude[:], b.PublicHeader, b.Sealed, payload)), o)
|
|
expectStep(t, "last padding byte not zero, 17 chunks away", failedStep(t, opened.Inspection.Checks, err), err, datekeys.ErrIntegrity, 17)
|
|
if !bytes.Equal(out.Bytes(), content) {
|
|
t.Fatalf("%d bytes written, want the content only", out.Len())
|
|
}
|
|
}
|
|
|
|
// Spec §31, §57, §76 change 6: L and P are not frame lengths. A time_only
|
|
// control that anyone seals can declare L = L_MAX over a PAYLOAD_AGE of 456
|
|
// bytes; the reader reserves nothing according to it and fails at step 17.
|
|
func TestDeclaredLengthIsNotAllocated(t *testing.T) {
|
|
f, err := testkit.LoadFixture(fixtureDir, "format2_empty_payload")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in, err := f.WithControl(func(m map[uint64]any) { m[6] = payloadLength(capsule.MaxPayloadLength) })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fx := loadFixture(t, "format2_empty_payload")
|
|
var before, after runtime.MemStats
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&before)
|
|
var out bytes.Buffer
|
|
opened, err := capsule.Open(context.Background(), &out, bytes.NewReader(in.DKC), fx.openOptions(t))
|
|
runtime.ReadMemStats(&after)
|
|
expectStep(t, "L = L_MAX over 456 bytes", failedStep(t, opened.Inspection.Checks, err), err, datekeys.ErrIntegrity, 17)
|
|
if grown := after.TotalAlloc - before.TotalAlloc; grown > 16<<20 {
|
|
t.Fatalf("%d bytes allocated", grown)
|
|
}
|
|
}
|
|
|
|
// Spec §62.1 rule 7 and its note, §76 change 8: SEALED_CONTROL_LEN is the
|
|
// exact length of SEALED_CONTROL, and follows the formulas of the note.
|
|
func TestSealedControlLength(t *testing.T) {
|
|
c := func(n int) int { return max(1, (n+65535)/65536) }
|
|
ext, _ := extension.New("org.example.note", 2, bytes.Repeat([]byte{7}, 100))
|
|
for _, tc := range []struct {
|
|
name string
|
|
setup func(o *capsule.EncryptOptions)
|
|
want func(control int) int
|
|
}{
|
|
{"time_only", func(*capsule.EncryptOptions) {}, func(n int) int { return 335 + 4 + n + 16*c(n) }},
|
|
{"time_and_key", func(o *capsule.EncryptOptions) { o.Policy, o.NewPortableKey = capsule.TimeAndKey, true }, func(n int) int {
|
|
inner := 86 + 98*16 + n + 16*c(n)
|
|
return 335 + 4 + inner + 16*c(inner)
|
|
}},
|
|
} {
|
|
for _, exts := range [][]extension.Extension{nil, {ext}} {
|
|
opts := past(t, 1000)
|
|
opts.ControlNoncritical = exts
|
|
tc.setup(&opts)
|
|
var dkc bytes.Buffer
|
|
if _, err := encrypt(t, &dkc, "length", opts); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
control, err := capsule.EncodeControl(&capsule.Control{Noncritical: exts, Padding: capsule.Reforzado}, capsule.Format2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := int(binary.BigEndian.Uint32(dkc.Bytes()[12:16]))
|
|
p, err := testkit.Split(dkc.Bytes())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != tc.want(len(control)) || got != len(p.Sealed) {
|
|
t.Errorf("%s, %d control extensions: SEALED_CONTROL_LEN %d, formula %d", tc.name, len(exts), got, tc.want(len(control)))
|
|
}
|
|
if len(exts) == 0 && ((tc.name == "time_only" && got != 458) || (tc.name == "time_and_key" && got != 2128)) {
|
|
t.Errorf("%s: %d, spec §62.1 gives 458 and 2128", tc.name, got)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spec §29, §62.1 rule 5, §76 change 9: I_PAYLOAD is new for every capsule,
|
|
// so that opening one capsule opens no other payload.
|
|
func TestPayloadIdentityReuse(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
var controls [2]*capsule.Control
|
|
var parts [2]testkit.Parts
|
|
for i := range 2 {
|
|
var dkc bytes.Buffer
|
|
if _, err := encrypt(t, &dkc, "same content", past(t, 1000)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var err error
|
|
if parts[i], err = testkit.Split(dkc.Bytes()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, _ := agewrap.NewTimeIdentity(p, 1000, testkit.Release(1000))
|
|
if controls[i], err = capsule.DecodeControl(decryptAge(t, parts[i].Sealed, id), capsule.Format2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if controls[0].PayloadIdentity == controls[1].PayloadIdentity {
|
|
t.Fatal("two capsules share I_PAYLOAD")
|
|
}
|
|
a, b := parts[0], parts[1]
|
|
step, _, err := openStep(t, testkit.Join(a.Prelude, a.Header, a.Sealed, b.Payload), capsule.OpenOptions{})
|
|
expectStep(t, "SEALED_CONTROL_A + PAYLOAD_AGE_B", step, err, datekeys.ErrIntegrity, 17)
|
|
}
|
|
|
|
// Spec §39, §76 change 2: INNER_ACCESS_AGE holds exactly 16 X25519 stanzas
|
|
// with distinct shares, whatever the number of credentials; each credential
|
|
// opens exactly one, and no credential opens the others.
|
|
func TestInnerHasSixteenStanzas(t *testing.T) {
|
|
for _, n := range []int{0, 2, 15} {
|
|
opts := past(t, 1000)
|
|
opts.Policy, opts.NewPortableKey = capsule.TimeAndKey, true
|
|
var ids []age.Identity
|
|
for range n {
|
|
id, _ := age.GenerateX25519Identity()
|
|
opts.Recipients = append(opts.Recipients, id.Recipient())
|
|
ids = append(ids, id)
|
|
}
|
|
var dkc bytes.Buffer
|
|
res, err := encrypt(t, &dkc, "slots", opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
k, _ := res.PortableKey.Identity()
|
|
ids = append(ids, k)
|
|
p, _ := testkit.Split(dkc.Bytes())
|
|
timeID, _ := agewrap.NewTimeIdentity(profile.Quicknet(), 1000, testkit.Release(1000))
|
|
st, err := agewrap.Stanzas(bytes.NewReader(decryptAge(t, p.Sealed, timeID)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := agewrap.CheckAccessStanzas(st, agewrap.AccessSlots); err != nil {
|
|
t.Fatalf("%d credentials: %v", len(ids), err)
|
|
}
|
|
opened := map[int]bool{}
|
|
for _, id := range ids {
|
|
for i, s := range st {
|
|
if _, err := id.Unwrap([]*age.Stanza{s}); err == nil {
|
|
if opened[i] {
|
|
t.Fatalf("stanza %d opened twice", i)
|
|
}
|
|
opened[i] = true
|
|
}
|
|
}
|
|
}
|
|
if len(opened) != len(ids) {
|
|
t.Fatalf("%d credentials open %d stanzas", len(ids), len(opened))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spec §39: a dummy wraps FK_ACCESS like a credential, so whoever kept the
|
|
// private key of a dummy would open the capsule: the writer MUST NOT keep it.
|
|
// Encrypt returns no key but the portable one, and its dummies are fresh: no
|
|
// ephemeral share repeats across capsules for the same credentials.
|
|
func TestDummyRecipients(t *testing.T) {
|
|
kept, _ := age.GenerateX25519Identity()
|
|
b, err := testkit.Build{Declared: capsule.TimeAndKey, Structure: capsule.TimeAndKey,
|
|
AccessRecipients: []age.Recipient{testkit.Stranger().Recipient(), kept.Recipient()}}.Make()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o := capsule.OpenOptions{Identities: []age.Identity{kept}}
|
|
if step, _, err := openStep(t, b.DKC, o); err != nil {
|
|
t.Fatalf("the kept key does not open the capsule: step %d, %v", step, err)
|
|
}
|
|
|
|
holder, _ := age.GenerateX25519Identity()
|
|
shares := map[string]bool{}
|
|
for range 2 {
|
|
opts := past(t, 1000)
|
|
opts.Policy, opts.Recipients = capsule.TimeAndKey, []age.Recipient{holder.Recipient()}
|
|
var dkc bytes.Buffer
|
|
res, err := encrypt(t, &dkc, "dummies", opts)
|
|
if err != nil || res.PortableKey != nil {
|
|
t.Fatalf("result %+v, %v", res, err)
|
|
}
|
|
p, _ := testkit.Split(dkc.Bytes())
|
|
timeID, _ := agewrap.NewTimeIdentity(profile.Quicknet(), 1000, testkit.Release(1000))
|
|
st, _ := agewrap.Stanzas(bytes.NewReader(decryptAge(t, p.Sealed, timeID)))
|
|
for _, s := range st {
|
|
if shares[s.Args[0]] {
|
|
t.Fatal("an ephemeral share repeats across capsules")
|
|
}
|
|
shares[s.Args[0]] = true
|
|
}
|
|
}
|
|
if len(shares) != 2*agewrap.AccessSlots {
|
|
t.Fatalf("%d shares", len(shares))
|
|
}
|
|
}
|
|
|
|
// The writer checks the format 2 rules it must follow through the reader:
|
|
// every capsule it writes opens with the rules of the reader, for both codes
|
|
// and both policies, whatever the content length.
|
|
func TestEncryptWritesFormat2(t *testing.T) {
|
|
for _, l := range []int{0, 1, 255, 256, 257, 8193, 65536, 65537} {
|
|
for _, code := range []capsule.Padding{capsule.Bloque256, capsule.Reforzado} {
|
|
content := strings.Repeat("d", l)
|
|
opts := past(t, 1000)
|
|
opts.Padding = code
|
|
var dkc bytes.Buffer
|
|
res, err := encrypt(t, &dkc, content, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want, _ := capsule.PaddedLength(uint64(l), code)
|
|
if dkc.Bytes()[4] != 2 || res.PaddedLength != want {
|
|
t.Fatalf("L = %d, %s: VERSION %d, P = %d", l, code, dkc.Bytes()[4], res.PaddedLength)
|
|
}
|
|
p, _ := testkit.Split(dkc.Bytes())
|
|
if uint64(len(p.Payload)) != capsule.PayloadAgeLength(want) {
|
|
t.Fatalf("L = %d, %s: PAYLOAD_AGE of %d bytes", l, code, len(p.Payload))
|
|
}
|
|
var out bytes.Buffer
|
|
opened, err := capsule.Open(context.Background(), &out, bytes.NewReader(dkc.Bytes()), defaultOpen(1000))
|
|
if err != nil || out.String() != content || opened.Padding != code || opened.PaddedLength != want {
|
|
t.Fatalf("L = %d, %s: %v", l, code, err)
|
|
}
|
|
}
|
|
}
|
|
}
|