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 accesskey_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"reflect"
|
|
|
|
|
"slices"
|
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
|
|
|
"strings"
|
|
|
|
|
"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/extension"
|
|
|
|
|
"g.activething.com/go/DateKeys/internal/cbortest"
|
|
|
|
|
"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
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const fixtures = "../testdata/fixtures"
|
|
|
|
|
|
|
|
|
|
func loadDKK(t *testing.T, name string) ([]byte, testkit.DKKFixture) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
var f testkit.DKKFixture
|
|
|
|
|
if err := testkit.ReadJSON(filepath.Join(fixtures, name+".dkk.json"), &f); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
b, err := os.ReadFile(filepath.Join(fixtures, f.File))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return b, f
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fixtureNames = []string{"time_and_key_portable", "time_and_key_recipients", "time_and_key_portable_extension"}
|
|
|
|
|
|
|
|
|
|
// fixtureExts lists extensions as the fixtures record them, with the exact
|
|
|
|
|
// data bytes.
|
|
|
|
|
func fixtureExts(k *accesskey.AccessKey) []testkit.FixtureExt {
|
|
|
|
|
var out []testkit.FixtureExt
|
|
|
|
|
for i, e := range append(slices.Clone(k.Critical), k.Noncritical...) {
|
|
|
|
|
out = append(out, testkit.FixtureExt{Critical: i < len(k.Critical), ID: e.ID, Version: e.Version, Data: hex.EncodeToString(e.Data)})
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// Spec §68: parse, validate and use the official .dkk fixtures.
|
|
|
|
|
func TestFixtures(t *testing.T) {
|
|
|
|
|
withData := 0
|
|
|
|
|
for _, name := range fixtureNames {
|
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
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
|
b, f := loadDKK(t, name)
|
|
|
|
|
k, err := accesskey.Decode(bytes.NewReader(b))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if hex.EncodeToString(k.CredentialID[:]) != f.CredentialID || hex.EncodeToString(k.CapsuleID[:]) != f.CapsuleID ||
|
|
|
|
|
k.Type != f.AccessType || hex.EncodeToString(k.Material) != f.Material ||
|
|
|
|
|
k.Verification == nil || hex.EncodeToString(k.Verification.CapsuleDigest) != f.CapsuleDigest {
|
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
|
|
|
t.Fatalf("decoded values differ from the fixture: %v", k)
|
|
|
|
|
}
|
|
|
|
|
if got := fixtureExts(k); !reflect.DeepEqual(got, f.Extensions) {
|
|
|
|
|
t.Fatalf("extensions differ:\n got %+v\nwant %+v", got, f.Extensions)
|
|
|
|
|
}
|
|
|
|
|
for _, e := range f.Extensions {
|
|
|
|
|
if e.Data != "" {
|
|
|
|
|
withData++
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
// Encode(Decode(x)) == x.
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
|
if err := accesskey.Encode(&out, k); err != nil || !bytes.Equal(out.Bytes(), b) {
|
|
|
|
|
t.Fatal("re-encoding differs from the fixture bytes")
|
|
|
|
|
}
|
|
|
|
|
// Expected result: the identity opens the INNER_ACCESS_AGE of its capsule.
|
|
|
|
|
var cf testkit.DKCFixture
|
|
|
|
|
if err := testkit.ReadJSON(filepath.Join(fixtures, strings.TrimSuffix(f.Capsule, ".dkc")+".json"), &cf); err != nil {
|
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
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
dkc, _ := os.ReadFile(filepath.Join(fixtures, f.Capsule))
|
|
|
|
|
parts, _ := testkit.Split(dkc)
|
|
|
|
|
timeID, _ := agewrap.NewTimeIdentity(testkitProfile(), cf.Release.Round, testkit.Release(cf.Release.Round))
|
|
|
|
|
inner := mustDecrypt(t, parts.Sealed, timeID)
|
|
|
|
|
id, err := k.Identity()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
acc, _ := agewrap.NewAccessIdentity(0, id)
|
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
|
|
|
if control := mustDecrypt(t, inner, acc); hex.EncodeToString(control) != cf.ControlCBOR {
|
|
|
|
|
t.Fatal("the .dkk does not yield the expected CONTROL_CBOR")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
// Spec §68: at least one official .dkk carries an extension with data.
|
|
|
|
|
if withData == 0 {
|
|
|
|
|
t.Fatal("no .dkk fixture carries extension data")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The .dkk with an extension is the credential of time_and_key_portable.dkk
|
|
|
|
|
// plus one noncritical extension, whose data the base protocol never decodes.
|
|
|
|
|
func TestFixtureWithExtension(t *testing.T) {
|
|
|
|
|
src, _ := loadDKK(t, "time_and_key_portable")
|
|
|
|
|
b, _ := loadDKK(t, "time_and_key_portable_extension")
|
|
|
|
|
k, err := accesskey.Decode(bytes.NewReader(b))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(k.Critical) != 0 || len(k.Noncritical) != 1 || k.Noncritical[0].ID != "org.example.delivery" || k.Noncritical[0].Version != 1 ||
|
|
|
|
|
hex.EncodeToString(k.Noncritical[0].Data) != "a1006468616e64" {
|
|
|
|
|
t.Fatalf("extension %+v", k.Noncritical)
|
|
|
|
|
}
|
|
|
|
|
k.Noncritical = nil
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
|
if err := accesskey.Encode(&out, k); err != nil || !bytes.Equal(out.Bytes(), src) {
|
|
|
|
|
t.Fatal("without its extension the .dkk differs from time_and_key_portable.dkk")
|
|
|
|
|
}
|
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 TestSecretsAreNotPrinted(t *testing.T) {
|
|
|
|
|
b, f := loadDKK(t, "time_and_key_portable")
|
|
|
|
|
k, _ := accesskey.Decode(bytes.NewReader(b))
|
|
|
|
|
for _, format := range []string{"%v", "%+v", "%#v", "%s"} {
|
|
|
|
|
for _, v := range []any{k, *k} {
|
|
|
|
|
if s := fmt.Sprintf(format, v); strings.Contains(s, f.Material) || !strings.Contains(s, "REDACTED") {
|
|
|
|
|
t.Fatalf("%s leaks or hides nothing: %s", format, s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func frame(body []byte) []byte {
|
|
|
|
|
pre := make([]byte, accesskey.PreludeSize)
|
|
|
|
|
copy(pre, accesskey.Magic)
|
|
|
|
|
pre[4] = accesskey.FramingVersion
|
|
|
|
|
binary.BigEndian.PutUint32(pre[8:], uint32(len(body)))
|
|
|
|
|
return append(pre, body...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDecodeRejects(t *testing.T) {
|
|
|
|
|
good, _ := loadDKK(t, "time_and_key_portable")
|
|
|
|
|
body := good[accesskey.PreludeSize:]
|
|
|
|
|
w, err := cbortest.UnmarshalMap(body)
|
|
|
|
|
if err != nil {
|
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
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
with := func(edit func(m map[uint64]any)) []byte {
|
|
|
|
|
m := map[uint64]any{}
|
|
|
|
|
for k, v := range w {
|
|
|
|
|
m[k] = v
|
|
|
|
|
}
|
|
|
|
|
edit(m)
|
|
|
|
|
b, err := cbortest.Marshal(m)
|
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
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return frame(b)
|
|
|
|
|
}
|
|
|
|
|
set := func(b []byte, i int, v byte) []byte { c := bytes.Clone(b); c[i] = v; return c }
|
|
|
|
|
bigLen := bytes.Clone(good)
|
|
|
|
|
binary.BigEndian.PutUint32(bigLen[8:], accesskey.MaxBodyLen+1)
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
in []byte
|
|
|
|
|
want error
|
|
|
|
|
}{
|
|
|
|
|
{"magic", set(good, 3, '2'), datekeys.ErrInvalidMagic},
|
|
|
|
|
{"a .dkc", append([]byte("DKC1"), good[4:]...), datekeys.ErrInvalidMagic},
|
|
|
|
|
{"empty", nil, datekeys.ErrInvalidMagic},
|
|
|
|
|
{"framing version", set(good, 4, 2), datekeys.ErrUnsupportedVersion},
|
|
|
|
|
{"flags", set(good, 5, 1), datekeys.ErrInvalidFlags},
|
|
|
|
|
{"reserved", set(good, 7, 1), datekeys.ErrInvalidFlags},
|
|
|
|
|
{"body length above the limit", bigLen, datekeys.ErrIntegrity},
|
|
|
|
|
{"truncated prelude", good[:10], datekeys.ErrIntegrity},
|
|
|
|
|
{"truncated body", good[:len(good)-1], datekeys.ErrIntegrity},
|
|
|
|
|
{"trailing data", append(bytes.Clone(good), 0), datekeys.ErrIntegrity},
|
|
|
|
|
{"schema version", with(func(m map[uint64]any) { m[1] = uint64(2) }), datekeys.ErrUnsupportedVersion},
|
|
|
|
|
{"type tag", with(func(m map[uint64]any) { m[0] = "datekeycap" }), datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
{"empty verification map", with(func(m map[uint64]any) { m[6] = map[uint64]any{} }), datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
{"empty extension array", with(func(m map[uint64]any) { m[8] = []any{} }), datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
{"null verification", with(func(m map[uint64]any) { m[6] = nil }), datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
{"unknown key", with(func(m map[uint64]any) { m[9] = "x" }), datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
{"short capsule_id", with(func(m map[uint64]any) { m[3] = make([]byte, 15) }), datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
{"short digest", with(func(m map[uint64]any) { m[6] = map[uint64]any{0: make([]byte, 31)} }), datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
{"unknown access type", with(func(m map[uint64]any) { m[4] = "mlkem768" }), datekeys.ErrAccessInvalid},
|
|
|
|
|
{"short material", with(func(m map[uint64]any) { m[5] = make([]byte, 31) }), datekeys.ErrAccessInvalid},
|
|
|
|
|
{"null material", with(func(m map[uint64]any) { m[5] = nil }), datekeys.ErrNonCanonicalCBOR},
|
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
|
|
|
{"non-canonical body", frame(append([]byte{0xb9, 0x00, 0x07}, body[1:]...)), datekeys.ErrNonCanonicalCBOR},
|
|
|
|
|
} {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
if _, err := accesskey.Decode(bytes.NewReader(tc.in)); !errors.Is(err, tc.want) {
|
|
|
|
|
t.Fatalf("got %v, want %v", err, tc.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEncodeRejectsAbsenceAsEmptyMap(t *testing.T) {
|
|
|
|
|
id, _ := age.GenerateX25519Identity()
|
|
|
|
|
raw, _ := agewrap.RawX25519Identity(id)
|
|
|
|
|
k := &accesskey.AccessKey{Type: accesskey.TypeX25519, Material: raw, Verification: &accesskey.Verification{}}
|
|
|
|
|
if err := accesskey.Encode(io.Discard, k); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
|
|
|
t.Fatalf("empty verification map encoded: %v", err)
|
|
|
|
|
}
|
|
|
|
|
k.Verification = nil
|
|
|
|
|
k.Noncritical = []extension.Extension{{ID: "org.example.delivery", Version: 1}}
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
|
if err := accesskey.Encode(&b, k); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
back, err := accesskey.Decode(&b)
|
|
|
|
|
if err != nil || back.Verification != nil || len(back.Noncritical) != 1 {
|
|
|
|
|
t.Fatalf("%v %v", back, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIdentityWipeAndEncodeErrors(t *testing.T) {
|
|
|
|
|
b, _ := loadDKK(t, "time_and_key_portable")
|
|
|
|
|
k, _ := accesskey.Decode(bytes.NewReader(b))
|
|
|
|
|
other := *k
|
|
|
|
|
other.Type = "mlkem768"
|
|
|
|
|
if _, err := other.Identity(); !errors.Is(err, datekeys.ErrAccessInvalid) {
|
|
|
|
|
t.Fatalf("unsupported type: %v", err)
|
|
|
|
|
}
|
|
|
|
|
dup := []extension.Extension{{ID: "a", Version: 1}, {ID: "a", Version: 2}}
|
|
|
|
|
for name, edit := range map[string]func(k *accesskey.AccessKey){
|
|
|
|
|
"short material": func(k *accesskey.AccessKey) { k.Material = k.Material[:31] },
|
|
|
|
|
"repeated critical": func(k *accesskey.AccessKey) { k.Critical = dup },
|
|
|
|
|
"repeated noncritical": func(k *accesskey.AccessKey) { k.Noncritical = dup },
|
|
|
|
|
"both arrays": func(k *accesskey.AccessKey) { k.Critical, k.Noncritical = dup[:1], dup[1:] },
|
|
|
|
|
} {
|
|
|
|
|
c := *k
|
|
|
|
|
c.Material = bytes.Clone(k.Material)
|
|
|
|
|
edit(&c)
|
|
|
|
|
if err := accesskey.Encode(io.Discard, &c); err == nil {
|
|
|
|
|
t.Errorf("%s: encoded", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := accesskey.Encode(failingWriter{}, k); err == nil {
|
|
|
|
|
t.Fatal("write error ignored")
|
|
|
|
|
}
|
|
|
|
|
k.Wipe()
|
|
|
|
|
if !bytes.Equal(k.Material, make([]byte, 32)) {
|
|
|
|
|
t.Fatal("material not wiped")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retainingWriter keeps the slices it is given, which io.Writer forbids, so
|
|
|
|
|
// that a test can see what the writer's caller leaves in them.
|
|
|
|
|
type retainingWriter struct{ writes [][]byte }
|
|
|
|
|
|
|
|
|
|
func (w *retainingWriter) Write(p []byte) (int, error) {
|
|
|
|
|
w.writes = append(w.writes, p)
|
|
|
|
|
return len(p), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retainingReader delivers data a few bytes at a time and keeps every slice
|
|
|
|
|
// it is given, up to its capacity, so that a test can see what the reader's
|
|
|
|
|
// caller leaves in them.
|
|
|
|
|
type retainingReader struct {
|
|
|
|
|
data []byte
|
|
|
|
|
chunk int
|
|
|
|
|
seen [][]byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *retainingReader) Read(p []byte) (int, error) {
|
|
|
|
|
r.seen = append(r.seen, p[:cap(p)])
|
|
|
|
|
if len(r.data) == 0 {
|
|
|
|
|
return 0, io.EOF
|
|
|
|
|
}
|
|
|
|
|
n := copy(p[:min(len(p), r.chunk)], r.data)
|
|
|
|
|
r.data = r.data[n:]
|
|
|
|
|
return n, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Encode and Decode leave no copy of access_material in the buffers they
|
|
|
|
|
// allocate, including the ones Decode outgrows while it reads the body.
|
|
|
|
|
func TestEncodeAndDecodeLeaveNoStaleMaterial(t *testing.T) {
|
|
|
|
|
material := bytes.Repeat([]byte{0xab}, 32)
|
|
|
|
|
big, err := extension.New("org.example.big", 1, make([]byte, 4096))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
k := &accesskey.AccessKey{Type: accesskey.TypeX25519, Material: bytes.Clone(material), Noncritical: []extension.Extension{big}}
|
|
|
|
|
w := &retainingWriter{}
|
|
|
|
|
var dkk bytes.Buffer
|
|
|
|
|
if err := accesskey.Encode(io.MultiWriter(&dkk, w), k); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for i, p := range w.writes {
|
|
|
|
|
if bytes.Contains(p, material) {
|
|
|
|
|
t.Errorf("Encode: slice %d written still holds the material", i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal(k.Material, material) {
|
|
|
|
|
t.Fatal("Encode wiped the material of its argument")
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Contains(dkk.Bytes(), material) {
|
|
|
|
|
t.Fatal("the .dkk does not hold the material")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r := &retainingReader{data: dkk.Bytes(), chunk: 100}
|
|
|
|
|
back, err := accesskey.Decode(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal(back.Material, material) {
|
|
|
|
|
t.Fatal("material not decoded")
|
|
|
|
|
}
|
|
|
|
|
back.Wipe()
|
|
|
|
|
for i, p := range r.seen {
|
|
|
|
|
if bytes.Contains(p, material[:8]) {
|
|
|
|
|
t.Errorf("Decode: read buffer %d of %d bytes still holds the material", i, len(p))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A truncated body is wiped too.
|
|
|
|
|
r = &retainingReader{data: dkk.Bytes()[:dkk.Len()-10], chunk: 100}
|
|
|
|
|
if _, err := accesskey.Decode(r); !errors.Is(err, datekeys.ErrIntegrity) {
|
|
|
|
|
t.Fatalf("truncated body: %v", err)
|
|
|
|
|
}
|
|
|
|
|
for i, p := range r.seen {
|
|
|
|
|
if bytes.Contains(p, material[:8]) {
|
|
|
|
|
t.Errorf("Decode of a truncated body: read buffer %d still holds the material", i)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A short .dkk that declares the largest BODY_LEN is rejected without an
|
|
|
|
|
// allocation of that size: the body buffer grows with the data read.
|
|
|
|
|
func TestDecodeShortBodyAllocatesLittle(t *testing.T) {
|
|
|
|
|
var pre [accesskey.PreludeSize]byte
|
|
|
|
|
copy(pre[:], accesskey.Magic)
|
|
|
|
|
pre[4] = accesskey.FramingVersion
|
|
|
|
|
binary.BigEndian.PutUint32(pre[8:12], accesskey.MaxBodyLen)
|
|
|
|
|
r := &retainingReader{data: append(pre[:], make([]byte, 5000)...), chunk: 1000}
|
|
|
|
|
if _, err := accesskey.Decode(r); !errors.Is(err, datekeys.ErrIntegrity) {
|
|
|
|
|
t.Fatalf("short body: %v", err)
|
|
|
|
|
}
|
|
|
|
|
for _, p := range r.seen {
|
|
|
|
|
if len(p) > 16<<10 {
|
|
|
|
|
t.Fatalf("a read buffer of %d bytes for 5000 bytes of body", len(p))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spec §57: the DKK BODY limit binds the encoder and every decoder entry
|
|
|
|
|
// point, whatever the framing says, with the code of a frame violation.
|
|
|
|
|
func TestBodyLimit(t *testing.T) {
|
|
|
|
|
if _, err := accesskey.DecodeBody(make([]byte, accesskey.MaxBodyLen+1)); !errors.Is(err, datekeys.ErrIntegrity) {
|
|
|
|
|
t.Fatalf("DecodeBody above 16 MiB: %v", err)
|
|
|
|
|
}
|
|
|
|
|
b, _ := loadDKK(t, "time_and_key_portable")
|
|
|
|
|
k, err := accesskey.Decode(bytes.NewReader(b))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
big, err := extension.New("org.example.big", 1, make([]byte, accesskey.MaxBodyLen))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
k.Noncritical = []extension.Extension{big}
|
|
|
|
|
if err := accesskey.Encode(io.Discard, k); !errors.Is(err, datekeys.ErrIntegrity) {
|
|
|
|
|
t.Fatalf("body above 16 MiB encoded: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 TestDecodeBodyExtensionRules(t *testing.T) {
|
|
|
|
|
good, _ := loadDKK(t, "time_and_key_portable")
|
|
|
|
|
m, err := cbortest.UnmarshalMap(good[accesskey.PreludeSize:])
|
|
|
|
|
if err != nil {
|
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
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
ext := func(id string, v uint64) map[uint64]any { return map[uint64]any{0: id, 1: v} }
|
|
|
|
|
for name, edit := range map[string]func(m map[uint64]any){
|
|
|
|
|
"critical out of order": func(m map[uint64]any) { m[7] = []any{ext("b", 1), ext("a", 1)} },
|
|
|
|
|
"noncritical repeated": func(m map[uint64]any) { m[8] = []any{ext("a", 1), ext("a", 2)} },
|
|
|
|
|
"both arrays": func(m map[uint64]any) { m[7] = []any{ext("a", 1)}; m[8] = []any{ext("a", 1)} },
|
|
|
|
|
// Spec §54: data is absent or a non-empty byte string in its
|
|
|
|
|
// shortest encoding; the extension map rejects anything else.
|
|
|
|
|
"data length not in shortest form": func(m map[uint64]any) {
|
|
|
|
|
m[8] = []any{map[uint64]any{0: "a", 1: uint64(1), 2: cbortest.Raw{0x58, 0x01, 0x00}}}
|
|
|
|
|
},
|
|
|
|
|
"data of type text": func(m map[uint64]any) { m[8] = []any{map[uint64]any{0: "a", 1: uint64(1), 2: "x"}} },
|
|
|
|
|
"data of type unsigned": func(m map[uint64]any) {
|
|
|
|
|
m[8] = []any{map[uint64]any{0: "a", 1: uint64(1), 2: cbortest.Raw{0x18, 0x01}}}
|
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
|
|
|
},
|
|
|
|
|
"empty data": func(m map[uint64]any) { m[7] = []any{map[uint64]any{0: "a", 1: uint64(1), 2: []byte{}}} },
|
|
|
|
|
"null data": func(m map[uint64]any) { m[8] = []any{map[uint64]any{0: "a", 1: uint64(1), 2: nil}} },
|
|
|
|
|
"version 2^32": func(m map[uint64]any) { m[8] = []any{ext("a", 1<<32)} },
|
|
|
|
|
"65 extensions": func(m map[uint64]any) {
|
|
|
|
|
var exts []any
|
|
|
|
|
for i := range 65 {
|
|
|
|
|
exts = append(exts, ext(fmt.Sprintf("x.%02d", i), 1))
|
|
|
|
|
}
|
|
|
|
|
m[8] = exts
|
|
|
|
|
},
|
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
|
|
|
"empty critical array": func(m map[uint64]any) { m[7] = []any{} },
|
|
|
|
|
"extension id not string": func(m map[uint64]any) { m[8] = []any{map[uint64]any{0: uint64(1), 1: uint64(1)}} },
|
|
|
|
|
} {
|
|
|
|
|
c := map[uint64]any{}
|
|
|
|
|
for k, v := range m {
|
|
|
|
|
c[k] = v
|
|
|
|
|
}
|
|
|
|
|
edit(c)
|
|
|
|
|
b, err := cbortest.Marshal(c)
|
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
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := accesskey.DecodeBody(b); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
|
|
|
t.Errorf("%s: %v", name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type failingWriter struct{}
|
|
|
|
|
|
|
|
|
|
func (failingWriter) Write([]byte) (int, error) { return 0, errors.New("disk full") }
|
|
|
|
|
|
|
|
|
|
func FuzzDecode(f *testing.F) {
|
|
|
|
|
for _, name := range fixtureNames {
|
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
|
|
|
b, err := os.ReadFile(filepath.Join(fixtures, name+".dkk"))
|
|
|
|
|
if err == nil {
|
|
|
|
|
f.Add(b)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
f.Add([]byte("DKK1\x01\x00\x00\x00\x00\x00\x00\x01\xa0"))
|
|
|
|
|
f.Fuzz(func(t *testing.T, in []byte) {
|
|
|
|
|
k, err := accesskey.Decode(bytes.NewReader(in))
|
|
|
|
|
if err != nil {
|
|
|
|
|
if datekeys.Code(err) == "" {
|
|
|
|
|
t.Fatalf("error without a normative code: %v", err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
|
if err := accesskey.Encode(&out, k); err != nil || !bytes.Equal(out.Bytes(), in) {
|
|
|
|
|
t.Fatal("accepted a .dkk that does not re-encode to its input")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func testkitProfile() *profile.Profile { return profile.Quicknet() }
|
|
|
|
|
|
|
|
|
|
func mustDecrypt(t *testing.T, file []byte, id age.Identity) []byte {
|
|
|
|
|
t.Helper()
|
|
|
|
|
r, err := age.Decrypt(bytes.NewReader(file), id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
b, err := io.ReadAll(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return b
|
|
|
|
|
}
|