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.
136 lines
4.8 KiB
136 lines
4.8 KiB
package capsule_test
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
"filippo.io/age"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/accesskey"
|
|
"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"
|
|
)
|
|
|
|
// Spec §55.1: what each section proves, and what it never proves.
|
|
func TestTrustModel(t *testing.T) {
|
|
// PUBLIC_HEADER never proves authorship: from the public bytes of
|
|
// time_only.dkc, or of format2_time_only.dkc, anyone builds a capsule
|
|
// with the same PRELUDE, the same PUBLIC_HEADER and another plaintext,
|
|
// which passes all 18 steps (spec §36.1).
|
|
for _, c := range []struct {
|
|
fixture string
|
|
format capsule.Format
|
|
}{{"time_only", capsule.Format1}, {"format2_time_only", capsule.Format2}} {
|
|
g := loadFixture(t, c.fixture)
|
|
p, _ := parts(t, c.fixture)
|
|
rec, err := agewrap.NewTimeRecipient(profile.Quicknet(), 1000)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
payloadID, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := agewrap.RawX25519Identity(payloadID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content := []byte("forged")
|
|
ctrl := &capsule.Control{}
|
|
plaintext := content
|
|
if c.format == capsule.Format2 {
|
|
ctrl.PayloadLength, ctrl.Padding = uint64(len(content)), capsule.Reforzado
|
|
plaintext = append(bytes.Clone(content), make([]byte, 256-len(content))...)
|
|
}
|
|
payload, _, err := testkit.Encrypt(plaintext, payloadID.Recipient())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
draft, err := capsule.EncodeControl(ctrl, c.format)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sealedDraft, _, err := testkit.Encrypt(draft, rec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prelude := capsule.Prelude{Format: c.format, PublicHeaderLen: uint32(len(p.Header)), SealedControlLen: uint32(len(sealedDraft))}.Bytes()
|
|
ctrl.HeaderBinding = capsule.HeaderBinding(prelude, p.Header)
|
|
copy(ctrl.PayloadIdentity[:], raw)
|
|
control, err := capsule.EncodeControl(ctrl, c.format)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sealed, _, err := testkit.Encrypt(control, rec)
|
|
if err != nil || len(sealed) != len(sealedDraft) {
|
|
t.Fatalf("sealing: %v", err)
|
|
}
|
|
if !bytes.Equal(prelude[:], p.Prelude) {
|
|
t.Fatalf("%s: PRELUDE %x differs from the official %x", c.fixture, prelude, p.Prelude)
|
|
}
|
|
forged := testkit.Join(prelude[:], p.Header, sealed, payload)
|
|
if got, err := open(t, forged, g.openOptions(t)); err != nil || string(got) != "forged" {
|
|
t.Fatalf("%s: forged capsule: %q, %v", c.fixture, got, err)
|
|
}
|
|
}
|
|
|
|
// In format 2 the control fixes the length and the padding of the
|
|
// plaintext, not the original L: whoever can seal another control, as
|
|
// anyone can for time_only once the round is published, declares another
|
|
// L with the same P, here L + 1 over a padding byte that is zero, and the
|
|
// capsule opens to another content (spec §30.1, §55.1).
|
|
x, err := testkit.LoadFixture(fixtureDir, "format2_time_only_extensions")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
longer, err := x.WithControl(func(m map[uint64]any) { m[6] = payloadLength(x.PayloadLength + 1) })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
xf := loadFixture(t, "format2_time_only_extensions")
|
|
if got, err := open(t, longer.DKC, xf.openOptions(t)); err != nil || !bytes.Equal(got, append(bytes.Clone(xf.plaintext), 0)) {
|
|
t.Fatalf("control with L + 1: %q, %v", got, err)
|
|
}
|
|
|
|
// PUBLIC_HEADER is bound from step 15 only: other extension data passes
|
|
// steps 1 to 8 and fails header_binding.
|
|
e := loadFixture(t, "time_only_extensions")
|
|
ep, err := testkit.Split(e.dkc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
header := bytes.Replace(ep.Header, []byte("public label"), []byte("Public label"), 1)
|
|
if bytes.Equal(header, ep.Header) {
|
|
t.Fatal("the header data was not found")
|
|
}
|
|
edited := testkit.Join(ep.Prelude, header, ep.Sealed, ep.Payload)
|
|
if step, err := inspectStep(t, edited, nil); err != nil {
|
|
t.Fatalf("steps 1 to 8 rejected the edited header data at step %d: %v", step, err)
|
|
}
|
|
step, _, err := openStep(t, edited, capsule.OpenOptions{Now: testkit.Fixed(e.unlock(t))})
|
|
expectStep(t, "PUBLIC_HEADER data edited", step, err, datekeys.ErrHeaderBinding, 15)
|
|
|
|
// The data of .dkk extensions is bound to nothing: other data opens the
|
|
// capsule all the same (spec §72: advisory for its holder only).
|
|
f := loadFixture(t, "time_and_key_portable")
|
|
k := loadAccessKey(t, "time_and_key_portable_extension")
|
|
k.Noncritical = []extension.Extension{mustExt(t, "org.example.delivery", []byte("anything else"))}
|
|
var dkk bytes.Buffer
|
|
if err := accesskey.Encode(&dkk, k); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := accesskey.Decode(&dkk)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
o := f.openOptions(t)
|
|
o.AccessKey = back
|
|
if got, err := open(t, f.dkc, o); err != nil || !bytes.Equal(got, f.plaintext) {
|
|
t.Fatalf(".dkk with other extension data: %v", err)
|
|
}
|
|
}
|