package capsule_test
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
Spec v0.8.2: corrections from the formal review
A formal review of the whole v0.8.2 text found it approvable after
these corrections, recorded in §76 ("Correcciones de la revisión
formal"):
- §27 no longer calls header_binding the authenticity of PUBLIC_HEADER:
it binds the header to the opened control, never authorship or date
(§55.1); the age MAC only protects against whoever lacks the file key.
- §63 steps 9 and 10: a network source (relay, Release API, cache) MUST
verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when
none verifies; the step-10 codes are for a directly supplied release.
The reference already behaved so; TestReleaseFromANetworkSource pins
both paths.
- §54 and §72: registrations declare the objects and arrays where an
extension may appear, and a known extension out of place counts as
unknown there. The reference gains the optional extension.Placement
interface, used at steps 4, 9.a and 14.
- §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order)
with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as
testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber.
- Step 5 makes the SEALED_CONTROL read mandatory, step 15 names
ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76
is made accurate (four dk1.json vectors, the §36 time_only rule, two
cases rewritten against the texts that really existed), and editorial
fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new
decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
"slices"
"strings"
"testing"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/cbortest"
"g.activething.com/go/DateKeys/internal/testkit"
)
// strictRegistry knows every org.example.* extension at version 1 and accepts
// only the data "ok" (spec §54, §72).
type strictRegistry struct { }
func ( strictRegistry ) Known ( id string , v uint64 ) bool {
return v == 1 && strings . HasPrefix ( id , "org.example." )
}
func ( strictRegistry ) ValidateData ( e extension . Extension ) error {
if string ( e . Data ) != "ok" {
return fmt . Errorf ( "data %x is not \"ok\"" , e . Data )
}
return nil
}
Spec v0.8.2: corrections from the formal review
A formal review of the whole v0.8.2 text found it approvable after
these corrections, recorded in §76 ("Correcciones de la revisión
formal"):
- §27 no longer calls header_binding the authenticity of PUBLIC_HEADER:
it binds the header to the opened control, never authorship or date
(§55.1); the age MAC only protects against whoever lacks the file key.
- §63 steps 9 and 10: a network source (relay, Release API, cache) MUST
verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when
none verifies; the step-10 codes are for a directly supplied release.
The reference already behaved so; TestReleaseFromANetworkSource pins
both paths.
- §54 and §72: registrations declare the objects and arrays where an
extension may appear, and a known extension out of place counts as
unknown there. The reference gains the optional extension.Placement
interface, used at steps 4, 9.a and 14.
- §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order)
with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as
testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber.
- Step 5 makes the SEALED_CONTROL read mandatory, step 15 names
ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76
is made accurate (four dk1.json vectors, the §36 time_only rule, two
cases rewritten against the texts that really existed), and editorial
fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new
decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
// place is one extension array of one object.
type place struct {
obj extension . Object
arr extension . Array
}
// placedRegistry is strictRegistry with the placement of its registrations
// (spec §72): each org.example.* extension is registered only in the places
// listed for it.
type placedRegistry struct {
strictRegistry
places map [ string ] [ ] place
}
func ( r placedRegistry ) RegisteredIn ( id string , _ uint64 , obj extension . Object , arr extension . Array ) bool {
return slices . Contains ( r . places [ id ] , place { obj , arr } )
}
func mustExt ( t * testing . T , id string , data [ ] byte ) extension . Extension {
t . Helper ( )
e , err := extension . New ( id , 1 , data )
if err != nil {
t . Fatal ( err )
}
return e
}
func mustUnhex ( t * testing . T , s string ) [ ] byte {
t . Helper ( )
b , err := hex . DecodeString ( s )
if err != nil {
t . Fatal ( err )
}
return b
}
// Spec §76, case 5: control data made of 14 or 15 nested CBOR arrays was
// sealed by Encrypt and rejected by Open at step 14, after the unlock, which
// made the capsule unrecoverable. Data is opaque now: it seals and opens.
func TestNestedDataSealsAndOpens ( t * testing . T ) {
for _ , depth := range [ ] int { 14 , 15 , 40 } {
data := mustUnhex ( t , strings . Repeat ( "81" , depth ) + "00" )
opts := past ( t , 1000 )
opts . Noncritical = [ ] extension . Extension { mustExt ( t , "org.example.nested" , data ) }
opts . ControlNoncritical = [ ] extension . Extension { mustExt ( t , "org.example.nested" , data ) }
var dkc bytes . Buffer
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
if _ , err := encrypt ( t , & dkc , "nested" , opts ) ; err != nil {
t . Fatalf ( "depth %d: %v" , depth , err )
}
var out bytes . Buffer
opened , err := capsule . Open ( context . Background ( ) , & out , bytes . NewReader ( dkc . Bytes ( ) ) , defaultOpen ( 1000 ) )
if err != nil || out . String ( ) != "nested" {
t . Fatalf ( "depth %d: sealed but does not open: %v" , depth , err )
}
if ! bytes . Equal ( opened . ControlNoncritical [ 0 ] . Data , data ) || ! bytes . Equal ( opened . Inspection . Header . Noncritical [ 0 ] . Data , data ) {
t . Fatalf ( "depth %d: data changed" , depth )
}
}
}
// Spec §76, case 3: header data {NaN: 0, NaN: 1} (a2f97e0000f97e0001) made
// the verdict on one capsule depend on map iteration order. The base protocol
// no longer decodes data, so every run gives the same verdict.
func TestNaNKeyedDataHasOneVerdict ( t * testing . T ) {
data := mustUnhex ( t , "a2f97e0000f97e0001" )
opts := past ( t , 1000 )
opts . Noncritical = [ ] extension . Extension { mustExt ( t , "org.example.nan" , data ) }
var b bytes . Buffer
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
if _ , err := encrypt ( t , & b , "nan" , opts ) ; err != nil {
t . Fatal ( err )
}
dkc := b . Bytes ( )
for i := range 500 {
in , err := capsule . Inspect ( bytes . NewReader ( dkc ) , capsule . InspectOptions { Registry : testkit . Registry ( ) } )
if err != nil || ! bytes . Equal ( in . Header . Noncritical [ 0 ] . Data , data ) {
t . Fatalf ( "Inspect run %d: %v" , i , err )
}
}
for i := range 200 {
if _ , err := capsule . Open ( context . Background ( ) , & bytes . Buffer { } , bytes . NewReader ( dkc ) , defaultOpen ( 1000 ) ) ; err != nil {
t . Fatalf ( "Open run %d: %v" , i , err )
}
}
}
// Spec §76, case 6: a PUBLIC_HEADER of about 880 KB with 40 000 + 40 000
// extensions took 8.3 s in the pairwise disjointness check. The 64-extension
// limit rejects it first.
func TestHugeExtensionArraysAreRejected ( t * testing . T ) {
f := loadFixture ( t , "time_only" )
parts , _ := testkit . Split ( f . dkc )
m , err := cbortest . UnmarshalMap ( parts . Header )
if err != nil {
t . Fatal ( err )
}
const n = 40_000
crit , non := make ( [ ] any , n ) , make ( [ ] any , n )
for i := range n {
crit [ i ] = map [ uint64 ] any { 0 : fmt . Sprintf ( "c%04x" , i ) , 1 : uint64 ( 1 ) }
non [ i ] = map [ uint64 ] any { 0 : fmt . Sprintf ( "n%04x" , i ) , 1 : uint64 ( 1 ) }
}
m [ 5 ] , m [ 6 ] = crit , non
h , err := cbortest . Marshal ( m )
if err != nil {
t . Fatal ( err )
}
if len ( h ) > capsule . MaxPublicHeaderLen {
t . Fatalf ( "header of %d bytes" , len ( h ) )
}
dkc := testkit . Reframe ( parts . Prelude , h , parts . Sealed , parts . Payload )
in , err := capsule . Inspect ( bytes . NewReader ( dkc ) , capsule . InspectOptions { Registry : testkit . Registry ( ) } )
if last := in . Checks [ len ( in . Checks ) - 1 ] ; ! errors . Is ( err , datekeys . ErrNonCanonicalCBOR ) || last . Step != 4 {
t . Fatalf ( "Inspect: %v at %+v" , err , last )
}
}
// Spec §54: a known noncritical extension with invalid data leaves the
// capsule valid; it is reported as unusable, in the object where it is.
func TestUnusableNoncriticalExtensions ( t * testing . T ) {
bad , good := [ ] byte ( "ko" ) , [ ] byte ( "ok" )
dkc , o := build ( t , testkit . Build {
HeaderNoncritical : [ ] extension . Extension { mustExt ( t , "org.example.header" , bad ) , mustExt ( t , "org.example.fine" , good ) } ,
ControlNoncritical : [ ] extension . Extension { mustExt ( t , "org.example.control" , bad ) , { ID : "org.example.nodata" , Version : 1 } , mustExt ( t , "org.other" , bad ) } ,
} )
o . Extensions = strictRegistry { }
var out bytes . Buffer
opened , err := capsule . Open ( context . Background ( ) , & out , bytes . NewReader ( dkc ) , o )
if err != nil || out . String ( ) != "malicious creator" {
t . Fatalf ( "a noncritical extension with invalid data failed the capsule: %v" , err )
}
in := opened . Inspection
if u := in . UnusableExtensions ; len ( u ) != 1 || u [ 0 ] . ID != "org.example.header" || ! errors . Is ( u [ 0 ] . Err , datekeys . ErrExtensionDataInvalid ) {
t . Fatalf ( "header: %+v" , u )
}
// An extension without data is known too: the registry rejects its
// absent data. org.other is unknown and ignored.
if u := opened . UnusableControlExtensions ; len ( u ) != 2 || u [ 0 ] . ID != "org.example.control" || u [ 1 ] . ID != "org.example.nodata" {
t . Fatalf ( "control: %+v" , u )
}
for _ , c := range in . Checks {
if ( c . Step == 4 || c . Step == 14 ) && ! strings . Contains ( c . Detail , "unusable noncritical" ) {
t . Fatalf ( "step %d does not report the unusable extensions: %s" , c . Step , c . Detail )
}
}
// Without a validating registry nothing is unusable.
o . Extensions = nil
opened , err = capsule . Open ( context . Background ( ) , & bytes . Buffer { } , bytes . NewReader ( dkc ) , o )
if err != nil || opened . Inspection . UnusableExtensions != nil || opened . UnusableControlExtensions != nil {
t . Fatalf ( "base protocol: %v" , err )
}
}
// The official .dkk with an extension (spec §68) opens its capsule; a
// registry that rejects its data reports it without refusing the credential.
func TestAccessKeyFixtureWithExtension ( t * testing . T ) {
f := loadFixture ( t , "time_and_key_portable" )
k := loadAccessKey ( t , "time_and_key_portable_extension" )
if len ( k . Noncritical ) != 1 || k . Noncritical [ 0 ] . ID != "org.example.delivery" {
t . Fatalf ( "extensions %+v" , k . Noncritical )
}
o := f . openOptions ( t )
o . AccessKey = k
var out bytes . Buffer
opened , err := capsule . Open ( context . Background ( ) , & out , bytes . NewReader ( f . dkc ) , o )
if err != nil || ! bytes . Equal ( out . Bytes ( ) , f . plaintext ) || opened . UnusableAccessKeyExtensions != nil {
t . Fatalf ( "%v" , err )
}
o . Extensions = strictRegistry { }
out . Reset ( )
opened , err = capsule . Open ( context . Background ( ) , & out , bytes . NewReader ( f . dkc ) , o )
if err != nil || ! bytes . Equal ( out . Bytes ( ) , f . plaintext ) {
t . Fatalf ( "%v" , err )
}
if u := opened . UnusableAccessKeyExtensions ; len ( u ) != 1 || u [ 0 ] . ID != "org.example.delivery" || ! errors . Is ( u [ 0 ] . Err , datekeys . ErrExtensionDataInvalid ) {
t . Fatalf ( "unusable: %+v" , u )
}
}
Spec v0.8.2: corrections from the formal review
A formal review of the whole v0.8.2 text found it approvable after
these corrections, recorded in §76 ("Correcciones de la revisión
formal"):
- §27 no longer calls header_binding the authenticity of PUBLIC_HEADER:
it binds the header to the opened control, never authorship or date
(§55.1); the age MAC only protects against whoever lacks the file key.
- §63 steps 9 and 10: a network source (relay, Release API, cache) MUST
verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when
none verifies; the step-10 codes are for a directly supplied release.
The reference already behaved so; TestReleaseFromANetworkSource pins
both paths.
- §54 and §72: registrations declare the objects and arrays where an
extension may appear, and a known extension out of place counts as
unknown there. The reference gains the optional extension.Placement
interface, used at steps 4, 9.a and 14.
- §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order)
with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as
testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber.
- Step 5 makes the SEALED_CONTROL read mandatory, step 15 names
ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76
is made accurate (four dk1.json vectors, the §36 time_only rule, two
cases rewritten against the texts that really existed), and editorial
fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new
decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
// Spec §54, §72: a known extension that appears in an object or array it is
// not registered for is treated there as unknown. An extension registered for
// the critical_extensions of CONTROL_CBOR only and copied into PUBLIC_HEADER,
// which anyone can write (§55.1), or into a .dkk, is rejected at step 4 or
// 9; one registered as noncritical only is rejected in a critical array, and
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
// a noncritical copy outside its registration, in PUBLIC_HEADER or in a
// .dkk, is ignored, its data unchecked. A registry that is not an
// extension.Placement keeps today's behaviour: it knows its extensions
// everywhere.
Spec v0.8.2: corrections from the formal review
A formal review of the whole v0.8.2 text found it approvable after
these corrections, recorded in §76 ("Correcciones de la revisión
formal"):
- §27 no longer calls header_binding the authenticity of PUBLIC_HEADER:
it binds the header to the opened control, never authorship or date
(§55.1); the age MAC only protects against whoever lacks the file key.
- §63 steps 9 and 10: a network source (relay, Release API, cache) MUST
verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when
none verifies; the step-10 codes are for a directly supplied release.
The reference already behaved so; TestReleaseFromANetworkSource pins
both paths.
- §54 and §72: registrations declare the objects and arrays where an
extension may appear, and a known extension out of place counts as
unknown there. The reference gains the optional extension.Placement
interface, used at steps 4, 9.a and 14.
- §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order)
with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as
testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber.
- Step 5 makes the SEALED_CONTROL read mandatory, step 15 names
ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76
is made accurate (four dk1.json vectors, the §36 time_only rule, two
cases rewritten against the texts that really existed), and editorial
fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new
decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
func TestExtensionPlacement ( t * testing . T ) {
sealed := [ ] extension . Extension { mustExt ( t , "org.example.sealed" , [ ] byte ( "ok" ) ) }
note := [ ] extension . Extension { mustExt ( t , "org.example.note" , [ ] byte ( "ok" ) ) }
badNote := [ ] extension . Extension { mustExt ( t , "org.example.note" , [ ] byte ( "ko" ) ) }
reg := placedRegistry { places : map [ string ] [ ] place {
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
"org.example.sealed" : { { extension . Control , extension . Critical } } ,
"org.example.note" : { { extension . Control , extension . Noncritical } } ,
"org.example.delivery" : { { extension . AccessKey , extension . Noncritical } } ,
Spec v0.8.2: corrections from the formal review
A formal review of the whole v0.8.2 text found it approvable after
these corrections, recorded in §76 ("Correcciones de la revisión
formal"):
- §27 no longer calls header_binding the authenticity of PUBLIC_HEADER:
it binds the header to the opened control, never authorship or date
(§55.1); the age MAC only protects against whoever lacks the file key.
- §63 steps 9 and 10: a network source (relay, Release API, cache) MUST
verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when
none verifies; the step-10 codes are for a directly supplied release.
The reference already behaved so; TestReleaseFromANetworkSource pins
both paths.
- §54 and §72: registrations declare the objects and arrays where an
extension may appear, and a known extension out of place counts as
unknown there. The reference gains the optional extension.Placement
interface, used at steps 4, 9.a and 14.
- §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order)
with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as
testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber.
- Step 5 makes the SEALED_CONTROL read mandatory, step 15 names
ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76
is made accurate (four dk1.json vectors, the §36 time_only rule, two
cases rewritten against the texts that really existed), and editorial
fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new
decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
} }
both := [ ] struct {
name string
reg extension . Registry
placed bool
} { { "placement" , reg , true } , { "no placement" , strictRegistry { } , false } }
// Where it is registered, the capsule opens.
dkc , _ := build ( t , testkit . Build { ControlCritical : sealed } )
if step , _ , err := openStep ( t , dkc , capsule . OpenOptions { Extensions : reg } ) ; err != nil {
t . Fatalf ( "CONTROL_CBOR extension in CONTROL_CBOR: %v at step %d" , err , step )
}
f := loadFixture ( t , "time_and_key_portable" )
k := * f . dkk
k . Critical = sealed
for _ , tc := range [ ] struct {
name string
dkc [ ] byte
o capsule . OpenOptions
step int
} {
{ "CONTROL_CBOR extension copied into the critical_extensions of PUBLIC_HEADER" , mustBuild ( t , testkit . Build { HeaderCritical : sealed , ControlCritical : sealed } ) , capsule . OpenOptions { } , 4 } ,
{ "CONTROL_CBOR extension in the critical_extensions of a .dkk" , f . dkc , capsule . OpenOptions { AccessKey : & k , Now : testkit . Fixed ( f . unlock ( t ) ) } , 9 } ,
{ "noncritical-only extension in the critical_extensions of CONTROL_CBOR" , mustBuild ( t , testkit . Build { ControlCritical : note } ) , capsule . OpenOptions { } , 14 } ,
} {
for _ , r := range both {
tc . o . Extensions = r . reg
step , calls , err := openStep ( t , tc . dkc , tc . o )
if ! r . placed {
if err != nil {
t . Errorf ( "%s, %s: %v at step %d" , tc . name , r . name , err , step )
}
continue
}
expectStep ( t , tc . name , step , err , datekeys . ErrExtensionCriticalUnknown , tc . step )
if tc . step < 10 && calls != 0 {
t . Errorf ( "%s: %d release requests" , tc . name , calls )
}
}
}
// A noncritical copy outside its registration is ignored: its invalid
// data is reported only in CONTROL_CBOR, where it is registered.
dkc , o := build ( t , testkit . Build { HeaderNoncritical : badNote , ControlNoncritical : badNote } )
for _ , r := range both {
o . Extensions = r . reg
opened , err := capsule . Open ( context . Background ( ) , & bytes . Buffer { } , bytes . NewReader ( dkc ) , o )
if err != nil {
t . Fatalf ( "%s: %v" , r . name , err )
}
header , control := opened . Inspection . UnusableExtensions , opened . UnusableControlExtensions
if len ( control ) != 1 || control [ 0 ] . ID != "org.example.note" || r . placed != ( header == nil ) {
t . Errorf ( "%s: unusable in PUBLIC_HEADER %+v, in CONTROL_CBOR %+v" , r . name , header , control )
}
}
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
// So is one in the noncritical_extensions of a .dkk: of two extensions
// with invalid data there, only the one registered for the .dkk is
// reported.
kn := * f . dkk
kn . Noncritical = [ ] extension . Extension { mustExt ( t , "org.example.delivery" , [ ] byte ( "ko" ) ) , badNote [ 0 ] }
o = f . openOptions ( t )
o . AccessKey = & kn
for _ , r := range both {
o . Extensions = r . reg
opened , err := capsule . Open ( context . Background ( ) , & bytes . Buffer { } , bytes . NewReader ( f . dkc ) , o )
if err != nil {
t . Fatalf ( "%s, .dkk: %v" , r . name , err )
}
want := [ ] string { "org.example.delivery" }
if ! r . placed {
want = append ( want , "org.example.note" )
}
var got [ ] string
for _ , u := range opened . UnusableAccessKeyExtensions {
got = append ( got , u . ID )
}
if ! slices . Equal ( got , want ) {
t . Errorf ( "%s: unusable in the .dkk %v, want %v" , r . name , got , want )
}
}
Spec v0.8.2: corrections from the formal review
A formal review of the whole v0.8.2 text found it approvable after
these corrections, recorded in §76 ("Correcciones de la revisión
formal"):
- §27 no longer calls header_binding the authenticity of PUBLIC_HEADER:
it binds the header to the opened control, never authorship or date
(§55.1); the age MAC only protects against whoever lacks the file key.
- §63 steps 9 and 10: a network source (relay, Release API, cache) MUST
verify every response and gives ERR_RELEASE_UNAVAILABLE at step 9 when
none verifies; the step-10 codes are for a directly supplied release.
The reference already behaved so; TestReleaseFromANetworkSource pins
both paths.
- §54 and §72: registrations declare the objects and arrays where an
extension may appear, and a known extension out of place counts as
unknown there. The reference gains the optional extension.Placement
interface, used at steps 4, 9.a and 14.
- §63 step 11 fixes the GT serialization hashed by H2 (kilic/kyber order)
with the frozen vector H2(e(G1, G2))[:16] = cb87319f..., shared as
testdata/vectors/tlock_ibe.json; H2-H4 are cited to drand/kyber.
- Step 5 makes the SEALED_CONTROL read mandatory, step 15 names
ERR_HEADER_BINDING, §21 makes capsule_id 16 CSPRNG bytes a MUST, §76
is made accurate (four dk1.json vectors, the §36 time_only rule, two
cases rewritten against the texts that really existed), and editorial
fixes in §5, §36, §55.1, §69.1 and §77. §73 lists the three new
decisions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
}
func mustBuild ( t * testing . T , b testkit . Build ) [ ] byte {
t . Helper ( )
dkc , _ := build ( t , b )
return dkc
}