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 implements the DateKeys Access Key, the portable .dkk
|
|
|
|
|
// credential (spec §38, §40-§44).
|
|
|
|
|
//
|
|
|
|
|
// A .dkk is a sensitive capability (spec §7.4). Its X25519 identity is stored
|
|
|
|
|
// as 32 raw bytes; the Bech32 AGE-SECRET-KEY-1... form is only an export
|
|
|
|
|
// format for humans (spec §38). No type in this package prints the material.
|
|
|
|
|
package accesskey
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
|
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
|
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
|
|
|
"g.activething.com/go/DateKeys/codec"
|
|
|
|
|
"g.activething.com/go/DateKeys/extension"
|
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
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Framing and schema constants (spec §40, §41).
|
|
|
|
|
const (
|
|
|
|
|
Magic = "DKK1"
|
|
|
|
|
FramingVersion = 1
|
|
|
|
|
PreludeSize = 12
|
|
|
|
|
// MaxBodyLen is the parser limit of spec §57, checked before allocating.
|
|
|
|
|
MaxBodyLen = 16 << 20
|
|
|
|
|
TypeTag = "datekeys-access-key"
|
|
|
|
|
SchemaVersion = 1
|
|
|
|
|
// TypeX25519 is the only access_type of V1 (spec §41).
|
|
|
|
|
TypeX25519 = "x25519"
|
|
|
|
|
|
|
|
|
|
idSize = 16
|
|
|
|
|
digestSize = 32
|
|
|
|
|
x25519Size = 32
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AccessKey is a decoded .dkk.
|
|
|
|
|
type AccessKey struct {
|
|
|
|
|
CredentialID [16]byte // key 2, random and opaque (spec §42)
|
|
|
|
|
CapsuleID [16]byte // key 3, the only capsule this credential is for (spec §38)
|
|
|
|
|
Type string // key 4, access_type
|
|
|
|
|
Material []byte // key 5, access_material: 32 raw X25519 identity bytes. SECRET.
|
|
|
|
|
// Verification is key 6, optional; nil when absent (spec §43, §58.1).
|
|
|
|
|
Verification *Verification
|
|
|
|
|
Critical []extension.Extension // key 7
|
|
|
|
|
Noncritical []extension.Extension // key 8
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verification is verification_metadata (spec §43). It supports fast failure
|
|
|
|
|
// and UX only; it is not a security property.
|
|
|
|
|
type Verification struct {
|
|
|
|
|
CapsuleDigest []byte // key 0, SHA-256 of the exact .dkc bytes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// bodyWire is BODY_CBOR as it is encoded: keys 2 to 8, keys 0 and 1 being
|
|
|
|
|
// the constants TypeTag and SchemaVersion.
|
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
|
|
|
type bodyWire struct {
|
|
|
|
|
CredentialID []byte // key 2
|
|
|
|
|
CapsuleID []byte // key 3
|
|
|
|
|
AccessType string // key 4
|
|
|
|
|
Material []byte // key 5, SECRET
|
|
|
|
|
// Digest is capsule_digest, the only key of verification_metadata
|
|
|
|
|
// (key 6); nil when key 6 is omitted.
|
|
|
|
|
Digest []byte
|
|
|
|
|
Critical []extension.Extension // key 7, omitted when empty
|
|
|
|
|
Noncritical []extension.Extension // key 8, omitted when empty
|
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 (w *bodyWire) encode(e *codec.Encoder) {
|
|
|
|
|
pairs := 6
|
|
|
|
|
for _, present := range []bool{w.Digest != nil, len(w.Critical) > 0, len(w.Noncritical) > 0} {
|
|
|
|
|
if present {
|
|
|
|
|
pairs++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
e.Map(pairs)
|
|
|
|
|
e.Uint(0)
|
|
|
|
|
e.Text(TypeTag)
|
|
|
|
|
e.Uint(1)
|
|
|
|
|
e.Uint(SchemaVersion)
|
|
|
|
|
e.Uint(2)
|
|
|
|
|
e.Bstr(w.CredentialID)
|
|
|
|
|
e.Uint(3)
|
|
|
|
|
e.Bstr(w.CapsuleID)
|
|
|
|
|
e.Uint(4)
|
|
|
|
|
e.Text(w.AccessType)
|
|
|
|
|
e.Uint(5)
|
|
|
|
|
e.Bstr(w.Material)
|
|
|
|
|
if w.Digest != nil {
|
|
|
|
|
e.Uint(6)
|
|
|
|
|
e.Map(1)
|
|
|
|
|
e.Uint(0)
|
|
|
|
|
e.Bstr(w.Digest)
|
|
|
|
|
}
|
|
|
|
|
if len(w.Critical) > 0 {
|
|
|
|
|
e.Uint(7)
|
|
|
|
|
extension.EncodeArray(e, w.Critical)
|
|
|
|
|
}
|
|
|
|
|
if len(w.Noncritical) > 0 {
|
|
|
|
|
e.Uint(8)
|
|
|
|
|
extension.EncodeArray(e, w.Noncritical)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// decode reads BODY_CBOR with every CDDL rule whose violation is
|
|
|
|
|
// ErrNonCanonicalCBOR; access_type and access_material, which have a code of
|
|
|
|
|
// their own (spec §57), are checked afterwards. The caller wipes Material,
|
|
|
|
|
// whatever the result.
|
|
|
|
|
func (w *bodyWire) decode(d *codec.Decoder) error {
|
|
|
|
|
pairs, err := d.Map(9)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
var seen uint
|
|
|
|
|
for range pairs {
|
|
|
|
|
k, err := d.Key()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
switch k {
|
|
|
|
|
case 0:
|
|
|
|
|
_, err = d.Text(len(TypeTag))
|
|
|
|
|
case 1:
|
|
|
|
|
_, err = d.Uint(SchemaVersion)
|
|
|
|
|
case 2:
|
|
|
|
|
w.CredentialID, err = d.Bstr(idSize, idSize)
|
|
|
|
|
case 3:
|
|
|
|
|
w.CapsuleID, err = d.Bstr(idSize, idSize)
|
|
|
|
|
case 4:
|
|
|
|
|
w.AccessType, err = d.Text(MaxBodyLen)
|
|
|
|
|
case 5:
|
|
|
|
|
w.Material, err = d.Bstr(0, MaxBodyLen)
|
|
|
|
|
case 6:
|
|
|
|
|
w.Digest, err = decodeVerification(d)
|
|
|
|
|
case 7:
|
|
|
|
|
w.Critical, err = extension.DecodeArray(d)
|
|
|
|
|
case 8:
|
|
|
|
|
w.Noncritical, err = extension.DecodeArray(d)
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("key %d: %w", k, err)
|
|
|
|
|
}
|
|
|
|
|
seen |= 1 << k
|
|
|
|
|
}
|
|
|
|
|
for k := range 6 {
|
|
|
|
|
if seen&(1<<k) == 0 {
|
|
|
|
|
return fmt.Errorf("key %d is missing: %w", k, datekeys.ErrNonCanonicalCBOR)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return d.EndMap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// decodeVerification reads verification_metadata, {0: capsule_digest}. It is
|
|
|
|
|
// present only when it holds a digest: an empty map is not a representation
|
|
|
|
|
// of absence (spec §43, §58.1).
|
|
|
|
|
func decodeVerification(d *codec.Decoder) ([]byte, error) {
|
|
|
|
|
pairs, err := d.Map(1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if pairs == 0 {
|
|
|
|
|
return nil, fmt.Errorf("empty verification_metadata; an absent one omits key 6: %w", datekeys.ErrNonCanonicalCBOR)
|
|
|
|
|
}
|
|
|
|
|
k, err := d.Key()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if k != 0 {
|
|
|
|
|
return nil, fmt.Errorf("verification_metadata key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
|
|
|
|
|
}
|
|
|
|
|
digest, err := d.Bstr(digestSize, digestSize)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("capsule_digest: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return digest, d.EndMap()
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// String describes k without its material.
|
|
|
|
|
func (k AccessKey) String() string {
|
|
|
|
|
return fmt.Sprintf("AccessKey{credential_id=%x capsule_id=%x type=%s material=REDACTED}", k.CredentialID, k.CapsuleID, k.Type)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GoString describes k without its material.
|
|
|
|
|
func (k AccessKey) GoString() string { return k.String() }
|
|
|
|
|
|
|
|
|
|
// Identity returns the age identity of an x25519 access key.
|
|
|
|
|
func (k *AccessKey) Identity() (age.Identity, error) {
|
|
|
|
|
if err := k.validateMaterial(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
id, err := agewrap.X25519IdentityFromRaw(k.Material)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: %v: %w", err, datekeys.ErrAccessInvalid)
|
|
|
|
|
}
|
|
|
|
|
return id, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wipe overwrites the material in place. It is best effort: Go may have made
|
|
|
|
|
// copies that cannot be reached.
|
|
|
|
|
func (k *AccessKey) Wipe() { clear(k.Material) }
|
|
|
|
|
|
|
|
|
|
func (k *AccessKey) validateMaterial() error {
|
|
|
|
|
if k.Type != TypeX25519 {
|
|
|
|
|
return fmt.Errorf("accesskey: access_type %q is not supported by V1: %w", k.Type, datekeys.ErrAccessInvalid)
|
|
|
|
|
}
|
|
|
|
|
if len(k.Material) != x25519Size {
|
|
|
|
|
return fmt.Errorf("accesskey: x25519 access_material is %d bytes, want %d: %w", len(k.Material), x25519Size, datekeys.ErrAccessInvalid)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MarshalBody returns BODY_CBOR, the Deterministic CBOR body of k (spec §41),
|
Spec v0.8.2: second-round corrections from the formal review
The second round of the formal review confirmed the nine corrections of
c57ed48 and asked for these, recorded in §76 as corrections 4 to 6 and
an editorial note:
- §72: an encoder MUST NOT write a registered extension in an object or
array it is not registered for; §54: a reader MUST NOT interpret the
data of a noncritical one it ignores for that reason. capsule.Encrypt
and accesskey.Encode take no Registry, so the application applies the
rule; their documentation and extension.Placement say so.
- §17 and §51 give the step-10 codes only for a directly supplied
release, as step 10 does; a network source discards a failing one at
step 9.
- Step 9 reports ERR_RELEASE_UNAVAILABLE and no other code, whatever the
failure of the source. provider/drand.Client keeps each relay's failure
as text only (errors.Join made a relay's ERR_ROUND_MISMATCH match with
errors.Is), and capsule.Open keeps only the text of a source error that
carries another code (a caller's source failing with
ERR_RELEASE_INVALID gave that code at step 9). A context that ended
stays detectable: Fetch now has a single failure path, so the canceled
and deadline cases are deterministic.
- TestExtensionPlacement covers the noncritical array of a .dkk: with
the object-blind extension.CheckNoncritical at step 9.a it fails.
- Editorial: §28.1 "analizan solo la cabecera age", one arrow at step 9,
two §76 introductions; §73 lines for release sources and placement.
- testdata/README.md says the corpus registers its extensions in both
arrays of every object; traceability, CHANGELOG and both READMEs
(integrity holds against whoever lacks the file keys, §27, §55.1)
follow. Spec dated 28 September 2026; new SHA-256 in spec/README.md.
No fixture or vector changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
|
|
|
// after checking that DecodeBody accepts it. The extensions of k are written
|
|
|
|
|
// as given, once they pass the rules of spec §54: MarshalBody, and Encode,
|
|
|
|
|
// take no extension.Registry, so the application writes a registered
|
|
|
|
|
// extension only in the arrays of a .dkk it is registered for (spec §72).
|
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 (k *AccessKey) MarshalBody() ([]byte, error) {
|
|
|
|
|
if err := k.validateMaterial(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
w := bodyWire{
|
|
|
|
|
CredentialID: k.CredentialID[:],
|
|
|
|
|
CapsuleID: k.CapsuleID[:],
|
|
|
|
|
AccessType: k.Type,
|
|
|
|
|
Material: k.Material,
|
|
|
|
|
}
|
|
|
|
|
if k.Verification != nil {
|
|
|
|
|
if len(k.Verification.CapsuleDigest) != digestSize {
|
|
|
|
|
// An empty map is not a canonical representation of absence (spec §43).
|
|
|
|
|
return nil, fmt.Errorf("accesskey: capsule_digest must be %d bytes: %w", digestSize, datekeys.ErrNonCanonicalCBOR)
|
|
|
|
|
}
|
|
|
|
|
w.Digest = k.Verification.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
|
|
|
}
|
|
|
|
|
var err error
|
|
|
|
|
if w.Critical, err = extension.Canonical(k.Critical); 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
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if w.Noncritical, err = extension.Canonical(k.Noncritical); 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
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := extension.CheckDisjoint(w.Critical, w.Noncritical); 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
|
|
|
return nil, err
|
|
|
|
|
}
|
Review fixes: author keys, the writer, the CLI, extensions and the locator
Fixes of the review of the session of 1 and 2 October that the text of
spec v0.11 already asks for:
- authorkey: String and GoString hide the secret key, which only Secret
returns; ParsePublic refuses a key that is not a point of the curve
(ed25519strict.OnCurve, checked against the square root of testkit).
- capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never
a capsule without the signature or the seal that was asked for. A panic
while evaluating the signature or the seal fails only that part, F1 or
S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can
refuse to publish the files.
- extension.CheckWrite, the rule of encoders of spec 72: the writers of
capsules and .dkk files refuse datekeys.note and datekeys.capsule outside
the arrays where they are registered, or with invalid data.
- CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE
before it signs (rule 20); decrypt -expect-author compares the key of an
F4 and writes nothing unless it matches; decrypt notifies a public note
that it does not show; the lines of the verdicts break at the last space
that fits, each row after the first behind a mark, so that the terminal
never breaks them; L is the payload, not the content.
- locator: a reader rejects an address that breaks 44.1 and keeps the
others; addresses refuse the special-purpose blocks of IANA, IPv6 outside
2000::/3, localhost and local names, characters outside RFC 3986, dot
segments, and a CID that does not decode to version 1 and a multihash;
ParseInfo checks that the locator is an age file with one tlock stanza
for the round of its DateKey; Info.Extension reads what it writes; its
errors carry no normative code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
|
|
|
// Spec §72: datekeys.capsule goes only in the noncritical array of a .dkk,
|
|
|
|
|
// and datekeys.note never in a .dkk. The data of datekeys.capsule is the
|
|
|
|
|
// locator's: its writer decodes what it writes (package locator).
|
|
|
|
|
if err := extension.CheckWrite(extension.Standard{}, extension.AccessKey, extension.Critical, w.Critical); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := extension.CheckWrite(extension.Standard{}, extension.AccessKey, extension.Noncritical, w.Noncritical); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
var e codec.Encoder
|
|
|
|
|
w.encode(&e)
|
|
|
|
|
b, err := e.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
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if len(b) > MaxBodyLen {
|
|
|
|
|
clear(b)
|
|
|
|
|
return nil, fmt.Errorf("accesskey: BODY_CBOR of %d bytes exceeds %d: %w", len(b), MaxBodyLen, datekeys.ErrIntegrity)
|
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
|
|
|
}
|
|
|
|
|
// Self-check (spec §72): the reader must accept what is written.
|
|
|
|
|
back, err := DecodeBody(b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
clear(b)
|
|
|
|
|
return nil, fmt.Errorf("accesskey: self-check: the reader rejects this body: %w", err)
|
|
|
|
|
}
|
|
|
|
|
back.Wipe()
|
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
|
|
|
return b, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Encode writes k as a complete .dkk: prelude and BODY_CBOR (spec §40). The
|
|
|
|
|
// body it encodes, which holds the material, is wiped once written.
|
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 Encode(w io.Writer, k *AccessKey) error {
|
|
|
|
|
body, err := k.MarshalBody()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer clear(body)
|
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
|
|
|
var pre [PreludeSize]byte
|
|
|
|
|
copy(pre[0:4], Magic)
|
|
|
|
|
pre[4] = FramingVersion
|
|
|
|
|
binary.BigEndian.PutUint32(pre[8:12], uint32(len(body)))
|
|
|
|
|
if _, err := w.Write(pre[:]); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
_, err = w.Write(body)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decode reads exactly one .dkk from r and validates its framing, its
|
|
|
|
|
// canonical body and its fields. Bytes after BODY_CBOR are rejected.
|
|
|
|
|
//
|
|
|
|
|
// Decode does not decide whether critical extensions are known; the consumer
|
|
|
|
|
// checks them against its extension.Registry (capsule.Open does).
|
|
|
|
|
func Decode(r io.Reader) (*AccessKey, error) {
|
|
|
|
|
var pre [PreludeSize]byte
|
|
|
|
|
n, err := io.ReadFull(r, pre[:])
|
|
|
|
|
if n < 4 || string(pre[0:4]) != Magic {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: %w", datekeys.ErrInvalidMagic)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: truncated prelude: %w", datekeys.ErrIntegrity)
|
|
|
|
|
}
|
|
|
|
|
if pre[4] != FramingVersion {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: framing version %d: %w", pre[4], datekeys.ErrUnsupportedVersion)
|
|
|
|
|
}
|
|
|
|
|
if pre[5] != 0 || pre[6] != 0 || pre[7] != 0 {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: flags %#x, reserved %#x%02x: %w", pre[5], pre[6], pre[7], datekeys.ErrInvalidFlags)
|
|
|
|
|
}
|
Spec v0.8.2 refinements: error precedence, trust model, strict order
Approved refinements, each recorded with its reproducible case in the
§76 v0.8.2 subsection:
- §69.1: layered error model with normative precedence (frame, type tag
and version, CBOR profile and CDDL, then fields with their own code in
ascending key order; across steps the §63 order decides), with a scope
paragraph for the optional steps 5, 6 and 8.
- §55.1: normative trust table per section (who can write it, from which
step it is bound, what it never proves); §72: security-relevant claims
go in CONTROL_CBOR or under a signature, .dkk data is advisory.
- §31/§54: extension arrays in strictly ascending unsigned byte order of
extension_id (one rule for order and uniqueness).
- Gaps a second implementation needed: §28.1 malformed age headers,
§15/§19 latest unlock time and dk1_ reading rules, §22/§23/§57 length
lower bounds, §63 step 8 tlock argument comparison and step 9 order,
§12.1 profile validation with the drand chain-hash formula, §74 table
of implementation limits.
Reference alignment: .dkk errors only at step 9.a (new
OpenOptions.AccessKeyFile, used by the CLI), CR/LF in dk1_ is
ERR_DATEKEY_INVALID, BODY_LEN 0 is ERR_INTEGRITY, nil identities are not
credentials, and AccessIdentity tries every identity on every stanza so
its verdict does not depend on their order. dk1.json gains three
vectors; every other testdata file is byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
// Spec §40, §57: BODY_LEN in 1..16 MiB. No empty frame holds a valid
|
|
|
|
|
// body, so 0 is a framing error, like a PUBLIC_HEADER_LEN of 0 (§22).
|
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
|
|
|
bodyLen := binary.BigEndian.Uint32(pre[8:12])
|
Spec v0.8.2 refinements: error precedence, trust model, strict order
Approved refinements, each recorded with its reproducible case in the
§76 v0.8.2 subsection:
- §69.1: layered error model with normative precedence (frame, type tag
and version, CBOR profile and CDDL, then fields with their own code in
ascending key order; across steps the §63 order decides), with a scope
paragraph for the optional steps 5, 6 and 8.
- §55.1: normative trust table per section (who can write it, from which
step it is bound, what it never proves); §72: security-relevant claims
go in CONTROL_CBOR or under a signature, .dkk data is advisory.
- §31/§54: extension arrays in strictly ascending unsigned byte order of
extension_id (one rule for order and uniqueness).
- Gaps a second implementation needed: §28.1 malformed age headers,
§15/§19 latest unlock time and dk1_ reading rules, §22/§23/§57 length
lower bounds, §63 step 8 tlock argument comparison and step 9 order,
§12.1 profile validation with the drand chain-hash formula, §74 table
of implementation limits.
Reference alignment: .dkk errors only at step 9.a (new
OpenOptions.AccessKeyFile, used by the CLI), CR/LF in dk1_ is
ERR_DATEKEY_INVALID, BODY_LEN 0 is ERR_INTEGRITY, nil identities are not
credentials, and AccessIdentity tries every identity on every stanza so
its verdict does not depend on their order. dk1.json gains three
vectors; every other testdata file is byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
|
|
|
if bodyLen == 0 || bodyLen > MaxBodyLen {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: BODY_LEN %d outside 1..%d: %w", bodyLen, MaxBodyLen, datekeys.ErrIntegrity)
|
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
|
|
|
}
|
|
|
|
|
body, err := readBody(r, int(bodyLen))
|
|
|
|
|
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
|
|
|
return nil, fmt.Errorf("accesskey: truncated body: %w", datekeys.ErrIntegrity)
|
|
|
|
|
}
|
|
|
|
|
defer clear(body)
|
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
|
|
|
var extra [1]byte
|
|
|
|
|
switch n, err := io.ReadFull(r, extra[:]); {
|
|
|
|
|
case n != 0:
|
|
|
|
|
return nil, fmt.Errorf("accesskey: data after BODY_CBOR: %w", datekeys.ErrIntegrity)
|
|
|
|
|
case !errors.Is(err, io.EOF):
|
|
|
|
|
return nil, fmt.Errorf("accesskey: reading after BODY_CBOR: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return DecodeBody(body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// readBody reads the n bytes of BODY_CBOR from r. The buffer grows with the
|
|
|
|
|
// data actually read, so a short file that declares a large BODY_LEN does not
|
|
|
|
|
// force an allocation of that size. The body holds access_material: every
|
|
|
|
|
// buffer it outgrows is wiped, and so is the partial body on error.
|
|
|
|
|
func readBody(r io.Reader, n int) ([]byte, error) {
|
|
|
|
|
buf := make([]byte, 0, min(n, bytes.MinRead))
|
|
|
|
|
for len(buf) < n {
|
|
|
|
|
if len(buf) == cap(buf) {
|
|
|
|
|
grown := make([]byte, len(buf), min(n, 2*cap(buf)))
|
|
|
|
|
copy(grown, buf)
|
|
|
|
|
clear(buf)
|
|
|
|
|
buf = grown
|
|
|
|
|
}
|
|
|
|
|
m, err := r.Read(buf[len(buf):cap(buf)])
|
|
|
|
|
buf = buf[:len(buf)+m]
|
|
|
|
|
if err != nil && len(buf) < n {
|
|
|
|
|
// Read may use the whole of its argument as scratch space.
|
|
|
|
|
clear(buf[:cap(buf)])
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return buf, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DecodeBody validates and decodes BODY_CBOR, which the DKK BODY limit of
|
|
|
|
|
// spec §57 bounds whatever the caller read it from.
|
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 DecodeBody(body []byte) (*AccessKey, error) {
|
|
|
|
|
if len(body) > MaxBodyLen {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: BODY_CBOR of %d bytes exceeds %d: %w", len(body), MaxBodyLen, datekeys.ErrIntegrity)
|
|
|
|
|
}
|
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 := codec.CheckSchema(body, TypeTag, SchemaVersion); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
var w bodyWire
|
|
|
|
|
defer func() { clear(w.Material) }()
|
|
|
|
|
if err := codec.Unmarshal(body, w.decode, w.encode); 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
|
|
|
return nil, fmt.Errorf("accesskey: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := extension.CheckDisjoint(w.Critical, w.Noncritical); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("accesskey: %w", 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
|
|
|
}
|
|
|
|
|
k := &AccessKey{Type: w.AccessType, Critical: w.Critical, Noncritical: w.Noncritical}
|
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
|
|
|
copy(k.CredentialID[:], w.CredentialID)
|
|
|
|
|
copy(k.CapsuleID[:], w.CapsuleID)
|
|
|
|
|
if w.Digest != nil {
|
|
|
|
|
k.Verification = &Verification{CapsuleDigest: w.Digest}
|
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
|
|
|
}
|
|
|
|
|
k.Material = bytes.Clone(w.Material)
|
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 := k.validateMaterial(); err != nil {
|
|
|
|
|
k.Wipe()
|
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
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return k, nil
|
|
|
|
|
}
|