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 testkit
import (
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
"time"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/datekey"
"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
)
Version constants: the specification and the module
- datekeys.SpecVersion ("0.8.2") names the specification the module
implements. A test ties it to the spec file, its title and
spec/README.md, and TestCatalogueMatchesSpec and the vector files use
it (testkit.SpecVersion now aliases it), so the vectors regenerate
unchanged.
- datekeys.Version() is the version of the module as the go command
recorded it. That is a tag, or for a binary built in a checkout the
pseudo-version of its commit (for example
v0.0.0-20260928105528-9ac9cd952f04), or (devel) when it is unknown, as
in tests or under a replace directive to a directory. It works as the
main module and as a dependency, whatever the module path, which it
reads from the root package.
- `datekeys version` (also -version and --version) prints both and the
Go toolchain.
- README.md and README.es.md explain the three versions (format,
specification, module) and what the code on main covers.
traceability §70 and CHANGELOG follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
// SpecVersion is the specification the vectors and fixtures implement: the
// one the module implements.
const SpecVersion = datekeys . SpecVersion
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
// RoundVector is one Quicknet resolution vector (spec §65).
type RoundVector struct {
Name string ` json:"name" `
Requested string ` json:"requested" `
Round uint64 ` json:"round,omitempty" `
Effective string ` json:"effective,omitempty" `
Error string ` json:"error,omitempty" `
}
// RoundVectorFile is testdata/vectors/quicknet_rounds.json.
type RoundVectorFile struct {
Spec string ` json:"spec" `
Profile string ` json:"profile" `
Description string ` json:"description" `
Vectors [ ] RoundVector ` json:"vectors" `
}
// DK1Vector is one dk1_ vector (spec §66). Valid vectors carry the logical
// object and every intermediate encoding; invalid ones carry the input and the
// expected error.
type DK1Vector struct {
Name string ` json:"name" `
Network string ` json:"network,omitempty" `
Round uint64 ` json:"round,omitempty" `
CanonicalJSON string ` json:"canonical_json,omitempty" `
Base64URL string ` json:"base64url,omitempty" `
Input string ` json:"input,omitempty" `
DK1 string ` json:"dk1,omitempty" `
Error string ` json:"error,omitempty" `
}
// DK1VectorFile is testdata/vectors/dk1.json.
type DK1VectorFile struct {
Spec string ` json:"spec" `
Description string ` json:"description" `
Vectors [ ] DK1Vector ` json:"vectors" `
}
// ProfileVector is testdata/vectors/profile_quicknet.json.
type ProfileVector struct {
Spec string ` json:"spec" `
Description string ` json:"description" `
ProfileID string ` json:"profile_id" `
Provider string ` json:"provider" `
Network string ` json:"network" `
ChainHash string ` json:"chain_hash" `
PublicKey string ` json:"public_key" `
PeriodSeconds uint64 ` json:"period_seconds" `
GenesisTime int64 ` json:"genesis_time" `
GenesisSeed string ` json:"genesis_seed" `
Scheme string ` json:"scheme" `
CanonicalCBOR string ` json:"canonical_cbor" `
ProfileHash string ` json:"profile_hash" `
}
// RoundVectors computes the Quicknet resolution vectors with the implementation.
func RoundVectors ( ) RoundVectorFile {
p := profile . Quicknet ( )
g := time . Unix ( p . GenesisTime , 0 ) . UTC ( )
r1000 , _ := datekey . RoundTime ( p , 1000 )
cases := [ ] struct {
name string
at time . Time
} {
{ "genesis exactly: round 1" , g } ,
{ "genesis + 1ns: next round" , g . Add ( time . Nanosecond ) } ,
{ "genesis + 1s" , g . Add ( time . Second ) } ,
{ "genesis + one period: round 2" , g . Add ( 3 * time . Second ) } ,
{ "genesis + one period + 1ns: round 3" , g . Add ( 3 * time . Second + time . Nanosecond ) } ,
{ "genesis - 1s: before the profile" , g . Add ( - time . Second ) } ,
{ "round 1000 boundary exactly" , r1000 } ,
{ "one second before the round 1000 boundary" , r1000 . Add ( - time . Second ) } ,
{ "one second after the round 1000 boundary" , r1000 . Add ( time . Second ) } ,
{ "1ns after the round 1000 boundary" , r1000 . Add ( time . Nanosecond ) } ,
{ "half a second after the round 1000 boundary" , r1000 . Add ( 500 * time . Millisecond ) } ,
{ "normative vector 2030-01-01 (spec §16)" , time . Date ( 2030 , 1 , 1 , 0 , 0 , 0 , 0 , time . UTC ) } ,
{ "1ns after 2030-01-01" , time . Date ( 2030 , 1 , 1 , 0 , 0 , 0 , 1 , time . UTC ) } ,
{ "normative vector round 66432123 (spec §16)" , time . Date ( 2029 , 12 , 16 , 7 , 15 , 33 , 0 , time . UTC ) } ,
{ "offset timezone equals UTC instant" , time . Date ( 2026 , 10 , 22 , 19 , 0 , 0 , 1_000_000 , time . FixedZone ( "" , 2 * 3600 ) ) } ,
{ "last representable round time" , time . Date ( 9999 , 12 , 31 , 23 , 59 , 57 , 0 , time . UTC ) } ,
{ "after the last representable round" , time . Date ( 9999 , 12 , 31 , 23 , 59 , 59 , 0 , time . UTC ) } ,
}
f := RoundVectorFile {
Spec : SpecVersion ,
Profile : p . ID ,
Description : "Quicknet date to round resolution (spec §15, §16, §65), generated by the reference implementation." ,
}
for _ , c := range cases {
v := RoundVector { Name : c . name , Requested : c . at . Format ( time . RFC3339Nano ) }
d , err := datekey . Resolve ( p , c . at )
if err != nil {
v . Error = datekeys . Code ( err )
} else {
v . Round = d . Round
v . Effective = d . UnlockAt ( p ) . Format ( time . RFC3339Nano )
}
f . Vectors = append ( f . Vectors , v )
}
return f
}
// DK1Vectors computes the dk1_ vectors with the implementation.
func DK1Vectors ( ) DK1VectorFile {
f := DK1VectorFile {
Spec : SpecVersion ,
Description : "Canonical dk1_ strings and rejected encodings (spec §18, §19, §66), generated by the reference implementation." ,
}
for _ , v := range [ ] struct {
name string
round uint64
} {
{ "round 1" , 1 } ,
{ "round 1000" , 1000 } ,
{ "normative 2030-01-01 round" , 66884212 } ,
{ "last Quicknet round" , profile . Quicknet ( ) . MaxRound ( ) } ,
} {
d := datekey . DateKey { ProfileID : profile . QuicknetID , Round : v . round }
j := d . CanonicalJSON ( )
f . Vectors = append ( f . Vectors , DK1Vector {
Name : v . name ,
Network : d . ProfileID ,
Round : d . Round ,
CanonicalJSON : string ( j ) ,
Base64URL : base64 . RawURLEncoding . EncodeToString ( j ) ,
DK1 : d . Compact ( ) ,
} )
}
enc := func ( s string ) string { return datekey . Prefix + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( s ) ) }
canon := datekey . DateKey { ProfileID : profile . QuicknetID , Round : 66884212 }
// The canonical JSON of round 1000 is 59 bytes: its Base64 form needs padding
// and has unused bits, unlike the 63-byte JSON of round 66884212.
r1000 := datekey . DateKey { ProfileID : profile . QuicknetID , Round : 1000 }
bad := [ ] struct { name , input string } {
{ "whitespace in JSON" , enc ( ` { "version": 1, "network": "datekeys:quicknet:v1", "round": 66884212} ` ) } ,
{ "keys reordered" , enc ( ` { "network":"datekeys:quicknet:v1","version":1,"round":66884212} ` ) } ,
{ "trailing whitespace" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1","round":66884212} ` + "\n" ) } ,
{ "exponent notation" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1","round":6.6884212e7} ` ) } ,
{ "fraction notation" , enc ( ` { "version":1.0,"network":"datekeys:quicknet:v1","round":66884212} ` ) } ,
{ "escaped character" , enc ( strings . Replace ( string ( canon . CanonicalJSON ( ) ) , "datekeys:" , "datekeys\\" + "u003a" , 1 ) ) } ,
{ "duplicate key" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1","round":1,"round":66884212} ` ) } ,
{ "padded Base64URL" , datekey . Prefix + base64 . URLEncoding . EncodeToString ( r1000 . CanonicalJSON ( ) ) } ,
{ "non-zero trailing bits" , mangleLastChar ( r1000 . Compact ( ) ) } ,
{ "extra field" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1","round":66884212,"public_key":"00"} ` ) } ,
{ "missing field" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1"} ` ) } ,
{ "version 2" , enc ( ` { "version":2,"network":"datekeys:quicknet:v1","round":66884212} ` ) } ,
{ "round 0" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1","round":0} ` ) } ,
{ "negative round" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1","round":-1} ` ) } ,
{ "fractional round" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1","round":1.5} ` ) } ,
{ "round above 2^53-1" , enc ( fmt . Sprintf ( ` { "version":1,"network":"datekeys:quicknet:v1","round":%d} ` , uint64 ( datekey . MaxRound ) + 1 ) ) } ,
{ "round as string" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1","round":"66884212"} ` ) } ,
{ "uppercase network" , enc ( ` { "version":1,"network":"DATEKEYS:QUICKNET:V1","round":66884212} ` ) } ,
{ "trailing data" , enc ( ` { "version":1,"network":"datekeys:quicknet:v1","round":66884212}x ` ) } ,
{ "byte order mark" , enc ( "\ufeff" + ` { "version":1,"network":"datekeys:quicknet:v1","round":66884212} ` ) } ,
{ "not Base64" , datekey . Prefix + "!!!" } ,
{ "missing prefix" , canon . Compact ( ) [ len ( datekey . Prefix ) : ] } ,
{ "uppercase prefix" , "DK1_" + canon . Compact ( ) [ len ( datekey . Prefix ) : ] } ,
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 §19 (v0.8.2 refinements, §76): CR and LF are outside both
// Base64 alphabets, and a JSON number is read by its exact decimal
// value, never as an IEEE 754 double (which would read 1 here).
{ "line feed inside the Base64" , datekey . Prefix + canon . Compact ( ) [ len ( datekey . Prefix ) : ] [ : 8 ] + "\n" + canon . Compact ( ) [ len ( datekey . Prefix ) + 8 : ] } ,
{ "carriage return and line feed after the Base64" , canon . Compact ( ) + "\r\n" } ,
{ "version 1.0000000000000001: its exact value, not a double" , enc ( ` { "version":1.0000000000000001,"network":"datekeys:quicknet:v1","round":66884212} ` ) } ,
// Spec §19 step 2: invalid UTF-8 fails step 2 even inside a member
// that a repeated name overwrites.
{ "invalid UTF-8 in a member a repeated name overwrites" , enc ( ` { "version":1,"network":" ` + "\xff" + ` ","network":"datekeys:quicknet:v1","round":66884212} ` ) } ,
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
}
for _ , b := range bad {
_ , err := datekey . Parse ( b . input )
code := datekeys . Code ( err )
if err == nil {
code = "accepted"
}
f . Vectors = append ( f . Vectors , DK1Vector { Name : b . name , Input : b . input , Error : code } )
}
return f
}
// mangleLastChar changes the last Base64 character to one that decodes to the
// same bytes but has non-zero unused bits.
func mangleLastChar ( s string ) string {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
last := s [ len ( s ) - 1 ]
for i := 0 ; i < len ( alphabet ) ; i ++ {
c := alphabet [ i ]
if c == last {
continue
}
cand := s [ : len ( s ) - 1 ] + string ( c )
a , errA := base64 . RawURLEncoding . DecodeString ( s [ len ( datekey . Prefix ) : ] )
b , errB := base64 . RawURLEncoding . DecodeString ( cand [ len ( datekey . Prefix ) : ] )
if errA == nil && errB == nil && string ( a ) == string ( b ) {
return cand
}
}
return s
}
// QuicknetProfileVector computes the profile vector with the implementation.
func QuicknetProfileVector ( ) ( ProfileVector , error ) {
p := profile . Quicknet ( )
b , err := p . CanonicalCBOR ( )
if err != nil {
return ProfileVector { } , err
}
h , err := p . Hash ( )
if err != nil {
return ProfileVector { } , err
}
return ProfileVector {
Spec : SpecVersion ,
Description : "Quicknet Provider Profile V1: exact Deterministic CBOR and profile_hash (spec §11, §12, §75 item 2), generated by the reference implementation." ,
ProfileID : p . ID ,
Provider : p . Provider ,
Network : p . Network ,
ChainHash : p . ChainHashHex ( ) ,
PublicKey : hex . EncodeToString ( p . PublicKey ) ,
PeriodSeconds : uint64 ( p . Period / time . Second ) ,
GenesisTime : p . GenesisTime ,
GenesisSeed : hex . EncodeToString ( p . GenesisSeed [ : ] ) ,
Scheme : p . Scheme ,
CanonicalCBOR : hex . EncodeToString ( b ) ,
ProfileHash : hex . EncodeToString ( h [ : ] ) ,
} , nil
}