Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
package capsule_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
|
|
|
"g.activething.com/go/DateKeys/accesskey"
|
|
|
|
|
"g.activething.com/go/DateKeys/capsule"
|
|
|
|
|
"g.activething.com/go/DateKeys/datekey"
|
|
|
|
|
"g.activething.com/go/DateKeys/extension"
|
|
|
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
|
|
|
"g.activething.com/go/DateKeys/profile"
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func seedFixtures(f *testing.F, part func(testkit.Parts) []byte) {
|
|
|
|
|
for _, name := range fixtureNames {
|
|
|
|
|
b, err := os.ReadFile(filepath.Join(fixtureDir, name+".dkc"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
f.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
p, err := testkit.Split(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
f.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
f.Add(part(p))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// whole keeps the first 512 bytes of the payload: the pre-unlock checks read
|
|
|
|
|
// only its age header, and small inputs keep the fuzzer fast.
|
|
|
|
|
func whole(p testkit.Parts) []byte {
|
|
|
|
|
payload := p.Payload
|
|
|
|
|
if len(payload) > 512 {
|
|
|
|
|
payload = payload[:512]
|
|
|
|
|
}
|
|
|
|
|
return testkit.Join(p.Prelude, p.Header, p.Sealed, payload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FuzzParsePrelude(f *testing.F) {
|
|
|
|
|
seedFixtures(f, func(p testkit.Parts) []byte { return p.Prelude })
|
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
|
|
|
p, err := capsule.ParsePrelude(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if datekeys.Code(err) == "" {
|
|
|
|
|
t.Fatalf("error without a normative code: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
got := p.Bytes()
|
|
|
|
|
if !bytes.Equal(got[:], b[:capsule.PreludeSize]) {
|
|
|
|
|
t.Fatal("accepted a prelude that does not re-encode to its input")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FuzzDecodeHeader(f *testing.F) {
|
|
|
|
|
seedFixtures(f, func(p testkit.Parts) []byte { return p.Header })
|
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
|
|
|
h, err := capsule.DecodeHeader(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if datekeys.Code(err) == "" {
|
|
|
|
|
t.Fatalf("error without a normative code: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
re, err := capsule.EncodeHeader(h)
|
|
|
|
|
if err != nil || !bytes.Equal(re, b) {
|
|
|
|
|
t.Fatal("accepted a PUBLIC_HEADER that does not re-encode to its input")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FuzzDecodeControl(f *testing.F) {
|
|
|
|
|
for _, name := range fixtureNames {
|
|
|
|
|
fx := loadFixture(f, name)
|
|
|
|
|
b, _ := hexDecode(fx.ControlCBOR)
|
|
|
|
|
f.Add(b)
|
|
|
|
|
}
|
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
|
|
|
c, err := capsule.DecodeControl(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if datekeys.Code(err) == "" {
|
|
|
|
|
t.Fatalf("error without a normative code: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
re, err := capsule.EncodeControl(c)
|
|
|
|
|
if err != nil || !bytes.Equal(re, b) {
|
|
|
|
|
t.Fatal("accepted a CONTROL_CBOR that does not re-encode to its input")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FuzzInspect feeds whole capsules to the pre-unlock validation, and to Open
|
|
|
|
|
// with a source that never has the release: a capsule that Inspect rejects
|
|
|
|
|
// must not cause a request, and nothing may pass the release step. The
|
|
|
|
|
// cryptographic steps after it are exercised by the mutation corpus; keeping
|
|
|
|
|
// them out of this target keeps it fast.
|
|
|
|
|
func FuzzInspect(f *testing.F) {
|
|
|
|
|
seedFixtures(f, whole)
|
|
|
|
|
reg := testkit.Registry()
|
|
|
|
|
far := testkit.Fixed(testkit.Genesis().AddDate(5, 0, 0))
|
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
|
|
|
_, err := capsule.Inspect(bytes.NewReader(b), capsule.InspectOptions{Registry: reg})
|
|
|
|
|
if err != nil && datekeys.Code(err) == "" {
|
|
|
|
|
t.Fatalf("error without a normative code: %v", err)
|
|
|
|
|
}
|
|
|
|
|
src := testkit.NewSource()
|
|
|
|
|
o := capsule.OpenOptions{Registry: reg, Source: src, Now: far}
|
|
|
|
|
_, openErr := capsule.Open(context.Background(), io.Discard, bytes.NewReader(b), o)
|
|
|
|
|
switch {
|
|
|
|
|
case openErr == nil:
|
|
|
|
|
t.Fatal("opened without a release")
|
|
|
|
|
case datekeys.Code(openErr) == "":
|
|
|
|
|
t.Fatalf("error without a normative code: %v", openErr)
|
|
|
|
|
case err != nil && src.Calls != 0:
|
|
|
|
|
t.Fatal("a capsule rejected by Inspect caused a release request")
|
|
|
|
|
case err == nil && !errors.Is(openErr, datekeys.ErrReleaseUnavailable) && !errors.Is(openErr, datekeys.ErrAccessRequired):
|
|
|
|
|
t.Fatalf("a capsule accepted by Inspect failed before the release step: %v", openErr)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FuzzEncodeImpliesDecode: whatever EncodeHeader, EncodeControl and
|
|
|
|
|
// accesskey.Encode accept, the matching decoder accepts and re-encodes to the
|
|
|
|
|
// same bytes. An encoder that writes what its reader rejects makes capsules
|
|
|
|
|
// that cannot be opened (spec §72, §76 case 5).
|
|
|
|
|
func FuzzEncodeImpliesDecode(f *testing.F) {
|
|
|
|
|
f.Add("org.example.label", uint64(1), []byte("public label"), "org.example.note", uint64(2), []byte{0xa2, 0x00, 0x07}, uint8(0), uint8(0))
|
|
|
|
|
f.Add("a", uint64(1)<<32, []byte{}, "a", uint64(0), []byte{0x81, 0x81, 0x00}, uint8(0b1011), uint8(63))
|
|
|
|
|
f.Add("org.\xff", uint64(0), []byte{0xf6}, "z", uint64(1)<<53, []byte(nil), uint8(0b0100), uint8(64))
|
|
|
|
|
dk := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}
|
|
|
|
|
f.Fuzz(func(t *testing.T, id1 string, v1 uint64, d1 []byte, id2 string, v2 uint64, d2 []byte, mode, filler uint8) {
|
|
|
|
|
// mode bit 0: first extension critical; bit 1: second critical;
|
|
|
|
|
// bit 2: first without data; bit 3: second without data. filler
|
|
|
|
|
// adds extensions to the noncritical array, up to past the limit.
|
|
|
|
|
e1 := extension.Extension{ID: id1, Version: v1, Data: d1}
|
|
|
|
|
e2 := extension.Extension{ID: id2, Version: v2, Data: d2}
|
|
|
|
|
if mode&4 != 0 {
|
|
|
|
|
e1.Data = nil
|
|
|
|
|
}
|
|
|
|
|
if mode&8 != 0 {
|
|
|
|
|
e2.Data = nil
|
|
|
|
|
}
|
|
|
|
|
var crit, non []extension.Extension
|
|
|
|
|
for i, e := range []extension.Extension{e1, e2} {
|
|
|
|
|
if mode&(1<<i) != 0 {
|
|
|
|
|
crit = append(crit, e)
|
|
|
|
|
} else {
|
|
|
|
|
non = append(non, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for i := range int(filler % 72) {
|
|
|
|
|
non = append(non, extension.Extension{ID: fmt.Sprintf("x.%02d", i), Version: 1})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h := &capsule.Header{DateKey: dk, Policy: capsule.Policy(mode >> 4 & 1), Critical: crit, Noncritical: non}
|
|
|
|
|
if b, err := capsule.EncodeHeader(h); err == nil {
|
|
|
|
|
back, err := capsule.DecodeHeader(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("EncodeHeader wrote a PUBLIC_HEADER that DecodeHeader rejects: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if re, err := capsule.EncodeHeader(back); err != nil || !bytes.Equal(re, b) {
|
|
|
|
|
t.Fatal("PUBLIC_HEADER does not re-encode to itself")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
c := &capsule.Control{Critical: crit, Noncritical: non}
|
|
|
|
|
if b, err := capsule.EncodeControl(c); err == nil {
|
|
|
|
|
back, err := capsule.DecodeControl(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("EncodeControl wrote a CONTROL_CBOR that DecodeControl rejects: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if re, err := capsule.EncodeControl(back); err != nil || !bytes.Equal(re, b) {
|
|
|
|
|
t.Fatal("CONTROL_CBOR does not re-encode to itself")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
k := &accesskey.AccessKey{Type: accesskey.TypeX25519, Material: make([]byte, 32), Critical: crit, Noncritical: non}
|
|
|
|
|
var dkk bytes.Buffer
|
|
|
|
|
if err := accesskey.Encode(&dkk, k); err == nil {
|
|
|
|
|
back, err := accesskey.Decode(bytes.NewReader(dkk.Bytes()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("accesskey.Encode wrote a .dkk that Decode rejects: %v", err)
|
|
|
|
|
}
|
|
|
|
|
var re bytes.Buffer
|
|
|
|
|
if err := accesskey.Encode(&re, back); err != nil || !bytes.Equal(re.Bytes(), dkk.Bytes()) {
|
|
|
|
|
t.Fatal(".dkk does not re-encode to itself")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
|
|
|
func hexDecode(s string) ([]byte, error) { return hex.DecodeString(s) }
|