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>
v0.8.2
dev 2 weeks ago
parent f6f2e9f55f
commit c57ed4869c

@ -6,9 +6,10 @@ semantic versioning; `v0.x` versions make no API stability promise.
## Unreleased — specification v0.8.2
Moves the module to the DateKeys Protocol Specification v0.8.2, whose one
normative change closes the extension format (spec §76), refined and amended
before release (see "Specification refinements" and "Specification
amendment: point canonicality"). Framing and schema versions do not change.
normative change closes the extension format (spec §76), refined, amended and
corrected before release (see "Specification refinements", "Specification
amendment: point canonicality" and "Specification corrections: formal
review"). Framing and schema versions do not change.
The CBOR library is replaced by a codec of the module's own, without
reflection or dependencies. Every valid object encodes to the same bytes as
@ -20,7 +21,7 @@ and inspection step of the test suite and the mutation corpus is the same.
v0.8.2 is unreleased, so these refinements amend it without a version change;
spec §76 records each with its reproducible cases. No valid object changes,
nor the verdict of any existing official vector or fixture; `dk1.json` gains
three vectors for the reading rules of §19.
four vectors for the reading rules of §19.
- Layered error precedence (spec §69.1, with §57 and §63): within one object
the code of the first failing layer is reported: frame, then type tag and
@ -31,9 +32,11 @@ three vectors for the reading rules of §19.
order (the `.dkk` as an object, then its binding to the capsule, then
credentials, then the round time, then the request), and the errors of a
`.dkk` come at step 9.a even when it is decoded earlier, and never under
`time_only`. Only the optional steps 5, 6 and 8 and the `capsule_digest`
check can change the code. Steps 10, 11, 13 and 17 give the codes of the
release verification (round, then signature) and of each identity.
`time_only`. The optional inspection of steps 5, 6 and 8 and the
`capsule_digest` check can change the code, and so could, until the
corrections of the formal review, whether a network source verified the
release. Steps 10, 11, 13 and 17 give the codes of the release
verification (round, then signature) and of each identity.
- Trust model (spec §55.1): who can write PUBLIC_HEADER, CONTROL_CBOR,
PAYLOAD_AGE and the `.dkk` body, from which step each is bound and by what,
and what none of them proves. Spec §72: an extension with security-relevant
@ -45,8 +48,9 @@ three vectors for the reading rules of §19.
- Rules that only `testdata/README.md` stated are now normative text: the age
header grammar of C2SP and its codes (§28.1, §36), the round-time bound of
9999-12-31T23:59:59Z and pre-genesis instants (§15), the reading rules of
`dk1_` (§19: either Base64 alphabet but no CR or LF, one JSON object
without a byte order mark, numbers by their exact decimal value), lengths
`dk1_` (§19: either Base64 alphabet but no CR or LF, one JSON object in
valid UTF-8 without a byte order mark, numbers by their exact decimal
value), lengths
of at least 1 (§22, §23, §40, §57), the comparison of the tlock stanza
arguments (§35, §63 step 8), the Provider Profile rules and the chain-hash
formula (§12.1, with `period` at most 2^32 − 1 and the name alphabets
@ -70,7 +74,8 @@ official vector holds:
failing to decode.
- `datekey.Parse` rejects CR and LF in a `dk1_` string with
`ERR_DATEKEY_INVALID`; the Go Base64 decoders skipped them, and the
result was `ERR_DATEKEY_NON_CANONICAL`.
result was `ERR_DATEKEY_NON_CANONICAL`. It also rejects invalid UTF-8 in
the JSON at step 2 (see "Fixed").
- `datekeys decrypt -dkk` hands the `.dkk` to `capsule.Open` still encoded,
through the new `OpenOptions.AccessKeyFile`, so its decoding errors come at
step 9 of a `time_and_key` capsule, after any failure of steps 1 to 8, and
@ -142,6 +147,62 @@ dependency update makes the decoders of drand lenient, and
point mutation differs from a capsule that opens only in one encoding;
`internal/testkit.TestPointReencodings`.
### Specification corrections: formal review
Corrections of the unreleased v0.8.2 from the formal review of the
specification, recorded with their cases in spec §76 ("Correcciones de la
revisión formal"). Three change normative text:
- A source that fetches releases over a network (a relay, the Release API or
a cache) must verify every response with the rules of step 10 and discard
the one that fails; when none passes, the code is `ERR_RELEASE_UNAVAILABLE`
at step 9 (spec §63). The codes of step 10 are those of a release supplied
directly, as in the official vectors. `provider/drand.Client` already did
this; the contract of `provider.ReleaseSource` and the documentation of
`capsule.OpenOptions.Source` now say so.
- Each registered extension declares the objects (PUBLIC_HEADER,
CONTROL_CBOR, `.dkk`) and the arrays where it may appear, and a known
extension elsewhere is treated as unknown there (spec §31, §54, §72): a
critical one is `ERR_EXTENSION_CRITICAL_UNKNOWN`, a noncritical one is
ignored. The new optional interface `extension.Placement` of a `Registry`
tells where each extension is registered; `capsule.Inspect` and
`capsule.Open` check the arrays of PUBLIC_HEADER (step 4), of the `.dkk`
(step 9.a) and of CONTROL_CBOR (step 14) with the new
`extension.CheckCriticalIn` and `extension.CheckNoncriticalIn`. A
`Registry` that does not implement it behaves as before.
- H2, H3 and H4 of step 11 are those of drand/kyber `encrypt/ibe`, and H2
hashes the element of GT in the order of `kilic/bls12-381`: c1 before c0 at
every level of the tower, each coordinate of Fp in 48 bytes big-endian. The
new vector file `testdata/vectors/tlock_ibe.json` freezes H2(e(G1, G2)) =
`cb87319f24560b5231579a09ad79f12e`; the order of `Fp12.toBytes` of noble,
c0 first, gives `0118eea9d5971745f71e3c94926f1717` and another file key.
The other findings are editorial: §27 defers the authenticity of
PUBLIC_HEADER to the trust model (§55.1) and says that the age header MAC
protects only against whoever does not know the file key; step 5 separates
the mandatory read of SEALED_CONTROL from the optional inspection of its age
header; step 15 names `ERR_HEADER_BINDING`; §21 makes the 16 CSPRNG bytes of
`capsule_id` a MUST; §77 gains drand/kyber, RFC 8259, RFC 8610 and RFC 4648;
§76 retitles its section "Cambios normativos de la v0.8.2" and corrects its
record: `dk1.json` gained four vectors, not three, and two cases of the
refinements quoted texts that no earlier version contained.
No error code of the reference changes, nor any fixture or existing vector.
`github.com/drand/kyber-bls12381`, already an indirect dependency through
drand and tlock, becomes a direct requirement: `internal/testkit` and a test
compute the pairing with it.
Tests: `capsule.TestReleaseFromANetworkSource` (a relay whose only answer is
a release of another round, or a negated signature, gives
`ERR_RELEASE_UNAVAILABLE` at step 9 through `provider/drand.Client`, and the
same release supplied directly the code of step 10);
`capsule.TestExtensionPlacement` (an extension registered for CONTROL_CBOR
only is unknown in PUBLIC_HEADER and in a `.dkk`, one registered as
noncritical only is unknown in a critical array, and a noncritical copy
outside its registration is ignored); `extension.TestPlacement`;
`agewrap.TestTlockH2Vector` (the frozen vector, and step 11 recomputed with
H2 and H4 against the file key that tlock unwraps).
### Breaking changes
- `extension.New(id, version, data []byte)` takes the opaque data bytes instead
@ -223,6 +284,15 @@ point mutation differs from a capsule that opens only in one encoding;
- `capsule.OpenOptions.AccessKeyFile`, a `.dkk` still encoded, which `Open`
decodes at step 9.a and only for a `time_and_key` capsule (§63, §69.1).
- `ErrExtensionDataInvalid` (`ERR_EXTENSION_DATA_INVALID`, §69).
- `extension.Placement`, an optional interface of a `Registry` that tells in
which objects and arrays each extension is registered, with
`extension.Object` (`PublicHeader`, `Control`, `AccessKey`),
`extension.Array` (`Critical`, `Noncritical`), `extension.KnownIn`,
`extension.CheckCriticalIn` and `extension.CheckNoncriticalIn` (§54, §72).
`CheckCritical` and `CheckNoncritical`, which do not know the object, keep
their behaviour and consult no `Placement`.
- `testdata/vectors/tlock_ibe.json`, the H2 vector of §63 step 11, generated
by `internal/testkit.IBEVectors` and documented in `testdata/README.md`.
- `extension.DataValidator`, an optional interface of a `Registry` that
validates the data of the extensions it knows: a known critical extension
with invalid data fails with `ErrExtensionDataInvalid` (§63 steps 4 and 14,

@ -2,8 +2,11 @@ package agewrap_test
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"reflect"
"strings"
"testing"
"time"
@ -11,6 +14,8 @@ import (
"filippo.io/age"
"github.com/drand/drand/v2/crypto"
"github.com/drand/kyber"
bls "github.com/drand/kyber-bls12381"
"github.com/drand/kyber/encrypt/ibe"
"github.com/drand/tlock"
datekeys "g.activething.com/go/DateKeys"
@ -227,6 +232,105 @@ func TestTimeIdentityRelease(t *testing.T) {
}
}
// Spec §63 step 11: H2 hashes the element of GT in the order of
// kilic/bls12-381, c1 before c0 at every level of the tower. The frozen
// vector H2(e(G1, G2)) of testdata/vectors/tlock_ibe.json pins the pairing and
// that serialization, and step 11 computed with them, sigma = V XOR
// H2(e(signature, U)) and FK_TIME = W XOR H4(sigma), recovers the file key
// that tlock unwraps. The reverse order, c0 first at every level as in the
// Fp12.toBytes of noble, gives another H2 and another key.
func TestTlockH2Vector(t *testing.T) {
var golden testkit.IBEVectorFile
if err := testkit.ReadJSON("../testdata/vectors/tlock_ibe.json", &golden); err != nil {
t.Fatal(err)
}
if got, err := testkit.IBEVectors(); err != nil || !reflect.DeepEqual(got, golden) {
t.Fatalf("testdata/vectors/tlock_ibe.json is stale: run genfixtures (%v)", err)
}
const (
g1 = "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb"
g2 = "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e" +
"024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8"
)
vec := golden.Vectors[0]
if len(golden.Vectors) != 1 || vec.G1 != g1 || vec.G2 != g2 || vec.H2 != "cb87319f24560b5231579a09ad79f12e" {
t.Fatalf("the frozen vector changed: %+v", golden.Vectors)
}
gt, err := hex.DecodeString(vec.GT)
if err != nil || len(gt) != testkit.GTLen {
t.Fatalf("gt of %d bytes: %v", len(gt), err)
}
if sum := sha256.Sum256(append([]byte("IBE-H2"), gt...)); hex.EncodeToString(sum[:testkit.H2Len]) != vec.H2 {
t.Fatal("h2 is not SHA-256(\"IBE-H2\" || gt) truncated to 16 bytes")
}
if h := hex.EncodeToString(testkit.H2(reversed(gt))); h != "0118eea9d5971745f71e3c94926f1717" {
t.Fatalf("H2 in the reverse order: %s", h)
}
// Step 11 on a stanza of round 1000 with the published release.
p := profile.Quicknet()
rec, _ := agewrap.NewTimeRecipient(p, 1000)
stanzas, err := agewrap.Stanzas(bytes.NewReader(encrypt(t, []byte("control"), rec)))
if err != nil {
t.Fatal(err)
}
release := testkit.Release(1000)
id, _ := agewrap.NewTimeIdentity(p, 1000, release)
fileKey, err := id.Unwrap(stanzas)
if err != nil {
t.Fatal(err)
}
scheme, err := p.DrandScheme()
if err != nil {
t.Fatal(err)
}
body := stanzas[0].Body
u, v, w := body[:len(body)-2*testkit.H2Len], body[len(body)-2*testkit.H2Len:len(body)-testkit.H2Len], body[len(body)-testkit.H2Len:]
sig, point := scheme.SigGroup.Point(), scheme.KeyGroup.Point()
if err := sig.UnmarshalBinary(release.Signature); err != nil {
t.Fatal(err)
}
if err := point.UnmarshalBinary(u); err != nil {
t.Fatal(err)
}
pairing, err := bls.NewBLS12381Suite().Pair(sig, point).MarshalBinary()
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
gt []byte
opens bool
}{
{"the order of kilic/bls12-381", pairing, true},
{"the reverse order", reversed(pairing), false},
} {
sigma := xor(v, testkit.H2(tc.gt))
h4 := sha256.Sum256(append(ibe.H4Tag(), sigma...))
if got := xor(w, h4[:testkit.H2Len]); bytes.Equal(got, fileKey) != tc.opens {
t.Errorf("%s: FK_TIME %x, tlock unwraps %x", tc.name, got, fileKey)
}
}
}
// reversed returns the twelve 48-byte coordinates of a serialization of GT in
// reverse order: c0 before c1 at every level of the tower.
func reversed(gt []byte) []byte {
out := make([]byte, 0, len(gt))
for i := len(gt) - testkit.CoordinateLen; i >= 0; i -= testkit.CoordinateLen {
out = append(out, gt[i:i+testkit.CoordinateLen]...)
}
return out
}
func xor(a, b []byte) []byte {
out := make([]byte, len(a))
for i := range a {
out[i] = a[i] ^ b[i]
}
return out
}
// U edits of a Quicknet tlock stanza body U || V || W (spec §12.2, §63 step
// 11).
var (

@ -6,6 +6,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"slices"
"strings"
"testing"
@ -31,6 +32,24 @@ func (strictRegistry) ValidateData(e extension.Extension) error {
return nil
}
// 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)
@ -190,3 +209,82 @@ func TestAccessKeyFixtureWithExtension(t *testing.T) {
t.Fatalf("unusable: %+v", u)
}
}
// 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
// a noncritical copy outside its registration is ignored, its data
// unchecked. A registry that is not an extension.Placement keeps today's
// behaviour: it knows its extensions everywhere.
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{
"org.example.sealed": {{extension.Control, extension.Critical}},
"org.example.note": {{extension.Control, extension.Noncritical}},
}}
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)
}
}
}
func mustBuild(t *testing.T, b testkit.Build) []byte {
t.Helper()
dkc, _ := build(t, b)
return dkc
}

@ -23,7 +23,9 @@ type InspectOptions struct {
// Extensions lists the extensions the application implements. Nil knows
// none, the state of the base protocol V1. When it is also an
// extension.DataValidator, the data of the known extensions is checked
// (spec §54).
// (spec §54). When it is also an extension.Placement, an extension it
// knows is known only in the objects and arrays it registers it for, and
// unknown elsewhere (spec §54, §72).
Extensions extension.Registry
}
@ -138,10 +140,10 @@ func inspect(r io.Reader, opts InspectOptions) (*Inspection, *parsed, error) {
return in, nil, in.fail(4, "header validation", fmt.Errorf("capsule: profile %q is not pinned: %w", h.DateKey.ProfileID, datekeys.ErrUnknownProfile))
}
in.Profile = p
if err := extension.CheckCritical(h.Critical, opts.Extensions); err != nil {
if err := extension.CheckCriticalIn(extension.PublicHeader, h.Critical, opts.Extensions); err != nil {
return in, nil, in.fail(4, "header validation", fmt.Errorf("capsule: PUBLIC_HEADER: %w", err))
}
in.UnusableExtensions = extension.CheckNoncritical(h.Noncritical, opts.Extensions)
in.UnusableExtensions = extension.CheckNoncriticalIn(extension.PublicHeader, h.Noncritical, opts.Extensions)
in.pass(4, "header validation", fmt.Sprintf("capsule_id=%s datekey=%s policy=%s profile=%s%s",
h.CapsuleIDHex(), h.DateKey.Compact(), h.Policy, p.ID, unusable(in.UnusableExtensions)))

@ -28,7 +28,11 @@ type OpenOptions struct {
// InspectOptions.Extensions.
Extensions extension.Registry
// Source fetches the release. Required. Its answer is always verified
// locally.
// locally, at step 10. A source that fetches releases over a network,
// like provider/drand.Client, verifies each response itself and discards
// the invalid ones, so that Open reports ErrReleaseUnavailable at step 9
// when none is valid; a release that a source hands over as the caller
// supplied it gets the codes of step 10 (spec §63).
Source provider.ReleaseSource
// Identities are the caller's own X25519 identities, for time_and_key
// capsules encrypted to known recipients. Nil entries are ignored.
@ -129,7 +133,7 @@ func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*O
if err != nil {
return out, in.fail(9, "access credential", err)
}
out.UnusableAccessKeyExtensions = extension.CheckNoncritical(k.Noncritical, opts.Extensions)
out.UnusableAccessKeyExtensions = extension.CheckNoncriticalIn(extension.AccessKey, k.Noncritical, opts.Extensions)
if k.Verification != nil && seekable {
payload, err := checkCapsuleDigest(r.(io.ReadSeeker), start, in.PayloadOffset, k.Verification.CapsuleDigest)
if err != nil {
@ -145,7 +149,9 @@ func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*O
in.pass(9, "access credential", fmt.Sprintf("%d identities to try%s", len(ids), unusable(out.UnusableAccessKeyExtensions)))
}
// Step 9: obtain the release, never before its round time.
// Step 9: obtain the release, never before its round time. A network
// source has verified each response with the rules of step 10 and
// discarded the invalid ones: none valid is ErrReleaseUnavailable here.
cond := provider.Condition{Round: h.DateKey.Round}
if now := opts.Now(); now.Before(in.UnlockAt) {
err := fmt.Errorf("capsule: round %d is published at %s, it is %s: %w", cond.Round,
@ -217,11 +223,11 @@ func Open(ctx context.Context, dst io.Writer, r io.Reader, opts OpenOptions) (*O
return out, in.fail(14, "control", err)
}
defer clear(control.PayloadIdentity[:])
if err := extension.CheckCritical(control.Critical, opts.Extensions); err != nil {
if err := extension.CheckCriticalIn(extension.Control, control.Critical, opts.Extensions); err != nil {
return out, in.fail(14, "control", fmt.Errorf("capsule: CONTROL_CBOR: %w", err))
}
out.ControlCritical, out.ControlNoncritical = control.Critical, control.Noncritical
out.UnusableControlExtensions = extension.CheckNoncritical(control.Noncritical, opts.Extensions)
out.UnusableControlExtensions = extension.CheckNoncriticalIn(extension.Control, control.Noncritical, opts.Extensions)
in.pass(14, "control", "canonical CONTROL_CBOR"+unusable(out.UnusableControlExtensions))
// Step 15: verify header_binding over the exact stored bytes.
@ -266,7 +272,7 @@ func checkAccessKey(k *accesskey.AccessKey, h *Header, reg extension.Registry) (
if err != nil {
return nil, err
}
if err := extension.CheckCritical(k.Critical, reg); err != nil {
if err := extension.CheckCriticalIn(extension.AccessKey, k.Critical, reg); err != nil {
return nil, fmt.Errorf("capsule: .dkk: %w", err)
}
if k.CapsuleID != h.CapsuleID {

@ -0,0 +1,79 @@
package capsule_test
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
"g.activething.com/go/DateKeys/provider/drand"
)
// Spec §63 steps 9 and 10: a source that fetches releases over a network
// verifies each response with the rules of step 10 and discards the one that
// fails, so a relay whose only answer is a release of another round, or a
// signature that does not verify, gives ERR_RELEASE_UNAVAILABLE at step 9.
// The codes of step 10 are those of a release supplied directly, as in the
// official vectors.
func TestReleaseFromANetworkSource(t *testing.T) {
f := loadFixture(t, "time_only")
for _, tc := range []struct {
name string
release provider.Release
direct error // the code of step 10, nil when the release is valid
}{
{"the published release", f.release, nil},
{"a release of another round, signed for that round", testkit.Release(1001), datekeys.ErrRoundMismatch},
{"a negated signature", provider.Release{Round: 1000, Signature: testkit.Negated(f.release.Signature)}, datekeys.ErrReleaseInvalid},
} {
var requests atomic.Int32
relay := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests.Add(1)
fmt.Fprintf(w, `{"round":%d,"signature":"%s"}`, tc.release.Round, hex.EncodeToString(tc.release.Signature))
}))
o := f.openOptions(t)
o.Source = drand.NewWithHTTPClient(relay.Client(), relay.URL)
step, err := openAt(t, f.dkc, o)
relay.Close()
switch {
case requests.Load() != 1:
t.Errorf("%s: %d relay requests", tc.name, requests.Load())
case tc.direct == nil && err != nil:
t.Errorf("%s, from a relay: %v at step %d", tc.name, err, step)
case tc.direct != nil && (datekeys.Code(err) != "ERR_RELEASE_UNAVAILABLE" || step != 9):
t.Errorf("%s, from a relay: got %v at step %d, want ERR_RELEASE_UNAVAILABLE at step 9", tc.name, err, step)
}
// The same release, supplied directly.
o.Source = provider.ReleaseSourceFunc(func(context.Context, *profile.Profile, provider.Condition) (provider.Release, error) {
return tc.release, nil
})
step, err = openAt(t, f.dkc, o)
if tc.direct == nil {
if err != nil {
t.Errorf("%s, supplied directly: %v at step %d", tc.name, err, step)
}
continue
}
expectStep(t, tc.name+", supplied directly", step, err, tc.direct, 10)
}
}
// openAt runs the whole flow and returns the step that failed, 0 on success.
func openAt(t *testing.T, dkc []byte, o capsule.OpenOptions) (int, error) {
t.Helper()
out, err := capsule.Open(context.Background(), discardWriter{}, bytes.NewReader(dkc), o)
if out == nil {
t.Fatalf("Open returned no result: %v", err)
}
return failedStep(t, out.Inspection.Checks, err), err
}

@ -30,13 +30,13 @@ Paths are relative to the repository root. `§` numbers refer to
| 18 | `dk1_` representation; the canonical JSON has no escapes, the `profile_id` alphabet needs none | `DateKey.CanonicalJSON`, `DateKey.Compact` | `datekey.TestGoldenDK1Vectors`, `TestNormativeRoundVector` |
| 19 | `dk1_` canonicality; steps 1 to 3 `ERR_DATEKEY_INVALID`, step 6 `ERR_DATEKEY_NON_CANONICAL`; step 1 accepts either Base64 alphabet, padding and non-zero trailing bits but no other character (CR and LF included); step 2 one RFC 8259 JSON object, no byte order mark; JSON numbers by their exact decimal value; round in 1..2^53−1 without the profile | `datekey.Parse` (`decodeBase64`, which rejects CR and LF before the Go decoders, `parseJSON`, which rejects invalid UTF-8 before `encoding/json` can replace it, `jsonUint`) | `datekey.TestGoldenDK1Vectors`, `TestReadingRules`, `TestNumberSpellings`, `FuzzParse`; mutation *non-canonical dk1_ JSON*; `testdata/vectors/dk1.json` (*byte order mark*, *line feed inside the Base64*, *carriage return and line feed after the Base64*, *version 1.0000000000000001: its exact value, not a double*, *invalid UTF-8 in a member a repeated name overwrites*) |
| 20 | File extensions and magic | magic checks in `capsule.ParsePrelude`, `accesskey.Decode` | mutation *a .dkk offered as a .dkc*; `accesskey.TestDecodeRejects` *a .dkc* |
| 21 | `capsule_id` | `capsule.Encrypt` (16 bytes from `crypto/rand`), `capsule.DecodeHeader` | `capsule.TestPortableKeysAreNeverReused` |
| 21 | `capsule_id`: exactly 16 bytes from a CSPRNG (MUST) | `capsule.Encrypt` (16 bytes from `crypto/rand`), `capsule.DecodeHeader` | `capsule.TestPortableKeysAreNeverReused` |
| 22 | `.dkc` framing; `PUBLIC_HEADER_LEN` in 1..1 MiB, `SEALED_CONTROL_LEN` in 1..64 MiB; PAYLOAD_AGE to EOF, at least an age header | `capsule.Prelude`, `capsule.ParsePrelude` | mutations *version changed*, *flags != 0*, *reserved != 0*, *magic*, length limits; `capsule.TestFrameLengthLowerBounds`, `FuzzParsePrelude` |
| 23 | PRELUDE; order of the checks of steps 1 and 2; section bytes present at steps 3 and 5 | `Prelude.Bytes`, `capsule.ParsePrelude`, `capsule.Inspect` | `capsule.TestConformanceFixtures`, `TestFrameLengthLowerBounds`, `TestPrecedenceAcrossSteps`; `testdata/vectors/inspect_differential.json` |
| 24 | PUBLIC_HEADER; keys 5 and 6 optional, 1 to 64 extensions each | `capsule.Header`, `EncodeHeader`, `DecodeHeader` (hand-written `headerWire` encode and decode; CDDL checked before the DateKey) | `capsule.TestConformanceFixtures` (exact extension data), `TestDecodeHeaderRejects`, `TestDecodeMapStructure`, `TestDecodeHeaderReportsTheCDDLFirst`, `FuzzDecodeHeader`, `FuzzEncodeImpliesDecode`; mutations *header schema version changed*, *unknown key in PUBLIC_HEADER* |
| 25 | Declared access policy | `capsule.Policy`; `capsule.DecodeHeader` (the value read, up to 2^53−1, must be 0 or 1 before any narrowing); `capsule.Open` step 12 | `capsule.TestDecodeMapStructure` (2, 255, 256, 257, 2^32, 2^53−256 and others), `FuzzDecodeHeader` (seeds 256, 257, 2^32); mutations *access_policy=… with … structure* (four cases), *undefined access_policy*, *access_policy 256 / 257 with a consistent header_binding* |
| 26 | Header binding | `capsule.HeaderBinding`; `capsule.Open` step 15 | `capsule.TestConformanceFixtures`; mutation *PUBLIC_HEADER_A + SEALED_CONTROL_B* |
| 27 | Pre-unlock validation | `capsule.Inspect` (steps 1–8), `agewrap.Stanzas` probe | `capsule.TestMutationCorpus` (no release request for any pre-unlock failure), `FuzzInspect` |
| 26 | Header binding; a mismatch at step 15 is `ERR_HEADER_BINDING` | `capsule.HeaderBinding`; `capsule.Open` step 15 | `capsule.TestConformanceFixtures`; mutation *PUBLIC_HEADER_A + SEALED_CONTROL_B* |
| 27 | Pre-unlock validation; the age header MAC authenticates only against whoever does not know the file key (anyone recomputes that of OUTER_TIME_AGE once the round is published), and `header_binding` gives internal coherence, not authorship or a date (§55.1) | `capsule.Inspect` (steps 1–8), `agewrap.Stanzas` probe | `capsule.TestMutationCorpus` (no release request for any pre-unlock failure), `FuzzInspect`, `TestTrustModel`; the U and stanza body mutations of §64, whose header MAC is recomputed |
| 28 | Three age files | `capsule.Encrypt`, `capsule.Open` | `capsule.TestEncryptRoundTripBothPolicies` |
| 28.1 | Age file format: the C2SP header grammar (at least one stanza); malformed OUTER_TIME_AGE and PAYLOAD_AGE headers `ERR_INTEGRITY` (steps 5 and 6, or 11 and 17), a malformed INNER_ACCESS_AGE `ERR_POLICY_STRUCTURE_MISMATCH` (step 12); wrong stanza count or type `ERR_POLICY_STRUCTURE_MISMATCH` | `agewrap.Stanzas` (the header parser of `filippo.io/age`), `capsule.classify`, `capsule.Open` step 12 | `capsule.TestMalformedAgeHeaders`, `agewrap.TestStanzasProbe`, `FuzzStanzas`; the 10 header-without-stanzas cases of `testdata/vectors/inspect_differential.json` (5 at step 5, 5 at step 6) |
| 29 | PAYLOAD_AGE | `capsule.Encrypt` step 4; `agewrap.PayloadIdentity`, `agewrap.CheckPayloadStanzas` | `agewrap.TestPayloadIdentityStrictness`; mutation *extra stanza in PAYLOAD_AGE* |
@ -47,7 +47,7 @@ Paths are relative to the repository root. `§` numbers refer to
| 33 | `time_and_key` | `capsule.Encrypt` (`seal`); `agewrap.AccessIdentity` | fixtures `time_and_key_*`; `capsule.TestEncryptRoundTripBothPolicies` |
| 34 | SEALED_CONTROL | `capsule.Encrypt`; `capsule.Open` step 11 | `capsule.TestConformanceFixtures` |
| 35 | tlock strict mode; exactly two stanza arguments compared as strings (§63 step 8) | `agewrap.TimeRecipient`, `agewrap.TimeIdentity`, `agewrap.CheckTimeStanzas` (pinned parameters only, exact stanza arguments) | `agewrap.TestTimeIdentityStrictness`, `TestInteroperabilityWithTlockLibrary`, `TestTimeIdentityRelease`; `capsule.TestTlockStanzaArgumentComparison` |
| 36 | Policy ↔ structure; `time_only`: a plaintext that starts with the age intro line is a mismatch, any other is read as CONTROL_CBOR at step 14; `time_and_key`: a malformed age header, or two one-argument stanzas with the same argument (a repeated X25519 ephemeral share), is a mismatch | `capsule.Open` step 12 (`looksLikeAge`, `agewrap.Stanzas`), `agewrap.CheckAccessStanzas` | mutations *access_policy=…* (four cases), *non-X25519 stanza in INNER_ACCESS_AGE*; `capsule.TestMalformedAgeHeaders`; `agewrap.TestMalformedX25519Stanzas` (*repeated stanza*) |
| 36 | Policy ↔ structure; `time_only`: a plaintext that starts with the age intro line is a mismatch, any other is read as CONTROL_CBOR at step 14; `time_and_key`: a malformed age header, or two stanzas with one argument after the type and the same argument (a repeated X25519 ephemeral share), is a mismatch | `capsule.Open` step 12 (`looksLikeAge`, `agewrap.Stanzas`), `agewrap.CheckAccessStanzas` | mutations *access_policy=…* (four cases), *non-X25519 stanza in INNER_ACCESS_AGE*; `capsule.TestMalformedAgeHeaders`; `agewrap.TestMalformedX25519Stanzas` (*repeated stanza*) |
| 36.1 | Authenticity semantics | documented in `README.md`, `SECURITY.md` | — (a property the protocol does not provide) |
| 37 | X25519 recipient V1 | `age.X25519Recipient`; `agewrap.X25519IdentityFromRaw` | `agewrap.TestRawKeys` |
| 38 | Portable Access Key | `EncryptOptions.NewPortableKey` (fresh `I_ACCESS` per capsule; no API accepts an existing one); `accesskey.AccessKey` | `capsule.TestPortableKeysAreNeverReused` |
@ -60,15 +60,15 @@ Paths are relative to the repository root. `§` numbers refer to
| 45 | Release API | `provider.ReleaseSource` interface only (server out of scope, plan §2) | — |
| 46 | Release Queue | out of scope (server) | — |
| 47 | Release Cache | every release is verified again: `capsule.Open` step 10 and `agewrap.TimeIdentity` | mutations *release of another round* |
| 48 | Multi-relay | `provider/drand.Client` (race, first *verified* release wins) | `drand.TestRaceWaitsForAValidSignature` |
| 48 | Multi-relay; a network source verifies every response with the rules of §63 step 10 and discards the invalid ones: none valid is `ERR_RELEASE_UNAVAILABLE` at step 9 | `provider/drand.Client` (race, first *verified* release wins), the `provider.ReleaseSource` contract | `drand.TestRaceWaitsForAValidSignature`, `TestRejectMalformedRelayResponses`; `capsule.TestReleaseFromANetworkSource` |
| 49 | Direct recovery from the provider | `provider/drand` | `drand.TestLiveRelays`, `capsule.TestLiveLifecycle` (`-tags integration`) |
| 50 | Historical release dependency | documented in `README.md` | — |
| 51 | Quicknet release verification; order and codes of §63 step 10: the round (`ERR_ROUND_MISMATCH`), then the signature, the canonical encoding of a point of G1 other than the identity (§12.2) that verifies as the BLS signature of the round (`ERR_RELEASE_INVALID`) | `provider.Verify` | `provider.TestVerifyPublishedReleases`, `TestVerifyRejects` (x + p, the point at infinity alone, with a payload or with the sort flag, no compression flag, the negated signature), `TestVerifyUsesThePinnedKeyOnly`; mutations *DateKey A + release of round B*, *release of another round*, *release signature …* |
| 51 | Quicknet release verification; order and codes of §63 step 10: the round (`ERR_ROUND_MISMATCH`), then the signature, the canonical encoding of a point of G1 other than the identity (§12.2) that verifies as the BLS signature of the round (`ERR_RELEASE_INVALID`); those codes for a release supplied directly, a network source discarding an invalid one at step 9 (`ERR_RELEASE_UNAVAILABLE`) | `provider.Verify` | `provider.TestVerifyPublishedReleases`, `TestVerifyRejects` (x + p, the point at infinity alone, with a payload or with the sort flag, no compression flag, the negated signature), `TestVerifyUsesThePinnedKeyOnly`; `capsule.TestReleaseFromANetworkSource`; mutations *DateKey A + release of round B*, *release of another round*, *release signature …* |
| 52 | DNS / MITM | `provider/drand` (no redirects, bounded responses, BLS) | `drand.TestRedirectsAreNotFollowed`, `TestRejectMalformedRelayResponses`, `TestRandomnessMustMatchWhenPresent` |
| 53 | Harvest now, decrypt later | `cmd/datekeys` warning beyond one year | `cmd/datekeys.TestLongHorizonWarning` |
| 54 | Extensions: data absent or a non-empty opaque byte string, never decoded; 1 to 64 per array; `extension_id` of at least 1 byte; `extension_version` ≤ 2^32−1; elements in strictly ascending unsigned bytewise order of the UTF-8 bytes of `extension_id` (a proper prefix first, never UTF-16 code units or a collation), so one `extension_id` per array, and none in both arrays | `extension.New`, `Canonical`, `EncodeArray` (refuses, through `codec.Encoder.Fail`, an array that `DecodeArray` rejects), `DecodeArray` (64 entries checked on the array head, explicit key 2 check), `CheckDisjoint` (linear merge), `CheckCritical`, `CheckNoncritical`, `Unusable` | `extension.TestNew`, `TestData`, `TestCanonicalSorts`, `TestOrderIsUnsignedBytewise`, `TestCanonicalRejects`, `TestEncodeArrayRejects`, `TestDecodeArrayRejects`, `TestCheckDisjoint`, `TestCheckDisjointIsLinear`, `TestCheckCritical`, `TestCheckNoncritical`, `FuzzDecodeArray`; `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`; mutations *unknown critical … extension*, *known critical … extension with invalid data*, *extension_version above 2^32-1*, *null extension data* |
| 54 | Extensions: data absent or a non-empty opaque byte string, never decoded; 1 to 64 per array; `extension_id` of at least 1 byte; `extension_version` ≤ 2^32−1; elements in strictly ascending unsigned bytewise order of the UTF-8 bytes of `extension_id` (a proper prefix first, never UTF-16 code units or a collation), so one `extension_id` per array, and none in both arrays; a known extension in an object or array it is not registered for is unknown there | `extension.New`, `Canonical`, `EncodeArray` (refuses, through `codec.Encoder.Fail`, an array that `DecodeArray` rejects), `DecodeArray` (64 entries checked on the array head, explicit key 2 check), `CheckDisjoint` (linear merge), `CheckCritical`, `CheckNoncritical`, `Unusable`; `Object`, `Array`, the optional `Placement` of a `Registry`, `KnownIn`, and `CheckCriticalIn` and `CheckNoncriticalIn`, which `capsule` runs at steps 4, 9.a and 14 | `extension.TestNew`, `TestData`, `TestCanonicalSorts`, `TestOrderIsUnsignedBytewise`, `TestCanonicalRejects`, `TestEncodeArrayRejects`, `TestDecodeArrayRejects`, `TestCheckDisjoint`, `TestCheckDisjointIsLinear`, `TestCheckCritical`, `TestCheckNoncritical`, `TestPlacement`, `FuzzDecodeArray`; `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestExtensionPlacement`; mutations *unknown critical … extension*, *known critical … extension with invalid data*, *extension_version above 2^32-1*, *null extension data* |
| 55 | Auxiliary integrity | `capsule_digest` treated as UX only | — |
| 55.1 | Trust model: who writes each section, from which step and by what it is bound, what it never proves | no code of its own: `header_binding` (step 15), the age header MACs (steps 11, 13 and 17), `capsule_id` and `capsule_digest` (step 9) | `capsule.TestTrustModel` (a capsule forged from the public bytes of `time_only.dkc` opens; edited PUBLIC_HEADER data passes steps 1 to 8 and fails step 15; other `.dkk` extension data opens the capsule) |
| 55.1 | Trust model: who writes each section, from which step and by what it is bound, what it never proves | no code of its own: `header_binding` (step 15), the age header MACs (steps 11, 13 and 17), `capsule_id` and `capsule_digest` (step 9.a) | `capsule.TestTrustModel` (a capsule forged from the public bytes of `time_only.dkc` opens; edited PUBLIC_HEADER data passes steps 1 to 8 and fails step 15; other `.dkk` extension data opens the capsule) |
| 56 | Atomic plaintext output | `capsule.Open` contract; `cmd/datekeys.writeAtomic` | `cmd/datekeys.TestOutputNotPublishedOnFailureOrOverwrite`, `TestDecryptFailuresLeaveNothing` |
| 57 | Parser limits, MUST for encoders and decoders; frame lengths of 0 or above the limits and objects above their frame → `ERR_INTEGRITY` on encode and decode, a field of the wrong CBOR type → `ERR_NON_CANONICAL_CBOR` before its own code, CDDL violations → `ERR_NON_CANONICAL_CBOR`, Provider Profile names and public key → `ERR_UNKNOWN_PROFILE`; implementation limits not normative | `capsule.MaxPublicHeaderLen` (`ParsePrelude`, `EncodeHeader`, `DecodeHeader`), `MaxSealedControlLen` (`ParsePrelude`, `Encrypt`), `accesskey.MaxBodyLen` (`Decode`, `DecodeBody`, `MarshalBody`), `extension.MaxExtensions`, `MaxDataLen`; the bounds each schema passes to `codec.Decoder` (`Map`, `Array`, `Uint`, `Bstr`, `Text`), with lengths checked against the remaining input before any copy; `profile.Validate` | mutations *…_LEN above the limit*, *65 extensions in one array*; `capsule.TestHeaderLimit`, `TestHugeExtensionArraysAreRejected`; `accesskey.TestDecodeRejects` *body length above the limit*, `TestBodyLimit`; `profile.TestValidateRejectsTamperedProfiles`, `TestIntegerRanges`; `codec.TestDecoderRejects` |
| 58 | Canonical CBOR and the protocol's CBOR profile (major types 0, 2, 3, 4, 5; unsigned integer keys; integers ≤ 2^53−1) | `codec`, without reflection or dependencies: `Encoder` (shortest heads, valid UTF-8, nil byte strings as empty, never `null`; a sticky first error, which `Fail` lets a schema encoder record), `Decoder` (strict cursor: profile major types only, shortest heads, definite lengths, strictly ascending unsigned keys per map, valid UTF-8, no trailing bytes), `Unmarshal` (re-encoding comparison), `Walk` (the profile only, for vectors, fuzzing and diagnostics); `codec.MaxSafeUint`; the profile covers the head of extension data only | `codec.TestDecoderAccepts`, `TestDecoderRejects` (negative integer, tag, float, simple values, indefinite lengths, non-shortest heads, text key, key order, UTF-8), `TestUnmarshalRejectsNonCanonical`, `TestWalk`, `TestEncoderAndWalkAgreeWithAReference` (against `internal/cbortest`), `TestSharedVectors`, `FuzzDecoder`, `FuzzUnmarshal`, `FuzzWalk`, `FuzzEncodeImpliesWalk`; `extension.TestData`; `internal/testkit.TestSchemaVectors`; `testdata/vectors/cbor.json` (generic vectors walked with `codec.Walk`, and one block per schema: Provider Profile, PUBLIC_HEADER, CONTROL_CBOR, `.dkk` body, `verification_metadata`, extension), generated by `internal/testkit.CBORVectors` |
@ -77,20 +77,20 @@ Paths are relative to the repository root. `§` numbers refer to
| 60 | Conceptual Go interfaces | `provider.ReleaseSource`, `provider.Verify`, `datekey.Resolve`, `datekey.RoundTime` | — |
| 61 | `time_only` encryption flow | `capsule.Encrypt` (steps numbered in comments) | `capsule.TestEncryptRoundTripBothPolicies` |
| 62 | `time_and_key` encryption flow | `capsule.Encrypt` | `capsule.TestEncryptRoundTripBothPolicies`, `TestPortableKeysAreNeverReused` |
| 63 | Decryption flow; steps 4 and 14 validate critical extensions (unknown, then invalid data); step 8 argument rules; step 9 order: the `.dkk` as an object (decoded there when still encoded), its `capsule_id` and `capsule_digest`, credentials (nil identities are none) before the clock, round time, request, and nothing of the credentials under `time_only`; step 10: round, then signature, a canonical point other than the identity (§12.2); step 11: the tlock stanza body `U \|\| V \|\| W` of \|U\| + 32 bytes (128 in Quicknet), U canonical and not the identity, the IBE check r·G == U, every failure `ERR_INTEGRITY`; the codes of the identities at steps 11, 13 (malformed X25519 stanza `ERR_INTEGRITY`, an identity that unwraps two stanzas `ERR_POLICY_STRUCTURE_MISMATCH` whatever the order, none `ERR_ACCESS_INVALID`) and 17 | `capsule.Inspect` (steps 1–8), `capsule.Open` (steps 9–18; `OpenOptions.AccessKeyFile`, `checkAccessKey`, `checkCapsuleDigest`), MUST rules inside `agewrap` identities (`AccessIdentity` tries every identity on every stanza; `TimeIdentity` checks the length of the tlock stanza body and U before `tlock.TimeUnlock`); no error copies the text of an error of age, tlock, kyber or drand (`agewrap`, `capsule.classify`), since kyber's IBE error carries the candidate plaintext and r; `cmd/datekeys` hands the `.dkk` over encoded; `datekeys inspect -json` rendered by `internal/inspectview` | `capsule.TestConformanceFixtures` (stage by stage), `TestTlockFailureDiagnosticsCarryNoSecrets`, `TestPlaintextWriterFailureKeepsItsText`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestPrecedenceAcrossSteps`, `TestControlCriticalBeforeHeaderBinding`, `agewrap.TestAccessIdentityStrictness`, `TestMalformedX25519Stanzas`, `cmd/datekeys.TestDecryptAccessKeyOrder`, `TestMutationCorpus`, `TestInspectDifferentialCorpus` (`testdata/vectors/inspect_differential.json`: 1825 deterministic mutations of the fixtures with the verdict of steps 1–8, generated by `internal/testkit.InspectDifferential`); `cmd/datekeys.TestInspectJSONGoldens` (`testdata/fixtures/*.inspect.json`) |
| 63 | Decryption flow; steps 4 and 14 validate critical extensions (unknown, then invalid data); step 5 reads SEALED_CONTROL, a MUST (`ERR_INTEGRITY`), and SHOULD inspect its age header; step 8 argument rules; step 9 order: the `.dkk` as an object (decoded there when still encoded), its `capsule_id` and `capsule_digest`, credentials (nil identities are none) before the clock, round time, request, and nothing of the credentials under `time_only`; a network source verifies each response with the rules of step 10 and discards the invalid ones (none valid: `ERR_RELEASE_UNAVAILABLE`, step 9); step 10: round, then signature, a canonical point other than the identity (§12.2), the codes of a release supplied directly; step 11: the tlock stanza body `U \|\| V \|\| W` of \|U\| + 32 bytes (128 in Quicknet), U canonical and not the identity, the IBE check r·G == U, every failure `ERR_INTEGRITY`, H2, H3 and H4 those of drand/kyber `encrypt/ibe`, H2 over the element of GT serialized in the order of kilic/bls12-381 (c1 before c0 at every level of the tower), with the frozen vector H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`; the codes of the identities at steps 11, 13 (malformed X25519 stanza `ERR_INTEGRITY`, an identity that unwraps two stanzas `ERR_POLICY_STRUCTURE_MISMATCH` whatever the order, none `ERR_ACCESS_INVALID`) and 17; step 15 `ERR_HEADER_BINDING` | `capsule.Inspect` (steps 1–8), `capsule.Open` (steps 9–18; `OpenOptions.AccessKeyFile`, `checkAccessKey`, `checkCapsuleDigest`), the `provider.ReleaseSource` contract and `provider/drand.Client` (step 9), `tlock.TimeUnlock` with the kyber-bls12381 pairing (step 11), MUST rules inside `agewrap` identities (`AccessIdentity` tries every identity on every stanza; `TimeIdentity` checks the length of the tlock stanza body and U before `tlock.TimeUnlock`); no error copies the text of an error of age, tlock, kyber or drand (`agewrap`, `capsule.classify`), since kyber's IBE error carries the candidate plaintext and r; `cmd/datekeys` hands the `.dkk` over encoded; `datekeys inspect -json` rendered by `internal/inspectview` | `capsule.TestConformanceFixtures` (stage by stage), `TestTlockFailureDiagnosticsCarryNoSecrets`, `TestPlaintextWriterFailureKeepsItsText`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestPrecedenceAcrossSteps`, `TestControlCriticalBeforeHeaderBinding`, `TestReleaseFromANetworkSource`, `agewrap.TestAccessIdentityStrictness`, `TestMalformedX25519Stanzas`, `TestTlockH2Vector` (`testdata/vectors/tlock_ibe.json`, generated by `internal/testkit.IBEVectors`, and step 11 recomputed with H2 and H4 against the file key tlock unwraps), `cmd/datekeys.TestDecryptAccessKeyOrder`, `TestMutationCorpus`, `TestInspectDifferentialCorpus` (`testdata/vectors/inspect_differential.json`: 1825 deterministic mutations of the fixtures with the verdict of steps 1–8, generated by `internal/testkit.InspectDifferential`); `cmd/datekeys.TestInspectJSONGoldens` (`testdata/fixtures/*.inspect.json`) |
| 64 | Mandatory mutation tests | `internal/testkit.Mutations` (the corpus), `internal/testkit.MutationCorpus` (its export) | `capsule.TestMutationCorpus`: the 33 listed mutations plus 32 more, built afresh; `capsule.TestExportedMutationCorpus`: `testdata/vectors/mutations.json`, the same 65 cases as frozen data (capsule, `.dkk`, identities, recorded release, clock, registry, known extensions), replayed with the recorded error and step; `capsule.TestPointMutationsChangeOnlyTheEncoding`: the ten point mutations keep a valid header MAC, and a decoder that reduces coordinates modulo p opens the c0 + p and x + p cases |
| 65 | Quicknet vectors | `internal/testkit.RoundVectors` | `datekey.TestGoldenRoundVectors` |
| 66 | `dk1_` vectors | `internal/testkit.DK1Vectors` | `datekey.TestGoldenDK1Vectors` |
| 67 | `.dkc` vectors | `testdata/fixtures/*.dkc` + `*.json`, `internal/testkit/genfixtures`; the frozen `datekeys inspect -json` output of each, `*.inspect.json`; formats in `testdata/README.md` | `capsule.TestConformanceFixtures`; `cmd/datekeys.TestInspectJSONGoldens` |
| 68 | `.dkk` vectors, with the exact extension data; one carries an extension with data | `testdata/fixtures/*.dkk` + `*.dkk.json`; `time_and_key_portable_extension.dkk` derived by `genfixtures` | `accesskey.TestFixtures`, `TestFixtureWithExtension`; `capsule.TestAccessKeyFixtureWithExtension` |
| 69 | Normative errors, including `ERR_EXTENSION_DATA_INVALID` | `errors.go` | `datekeys.TestCatalogueMatchesSpec`, `TestCode` |
| 69.1 | Error precedence: the first failing layer of each object (frame; type tag and schema version; CBOR profile and CDDL, except the rules with codes of their own; fields with codes of their own in ascending key order), the step order of §63 across objects and steps; only the optional steps 5, 6 and 8 and the `capsule_digest` check can change the code | `capsule.ParsePrelude`, `capsule.DecodeHeader`, `capsule.DecodeControl`, `accesskey.Decode`, `accesskey.DecodeBody`, `profile.Decode`, `codec.CheckSchema`, `codec.Unmarshal`, `extension.CheckCritical`, `capsule.checkAccessKey`, `OpenOptions.AccessKeyFile`, `agewrap.AccessIdentity` | `capsule.TestPrecedenceWithinPublicHeader`, `TestPrecedenceAcrossSteps`, `TestDecodeHeaderReportsTheCDDLFirst`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestControlCriticalBeforeHeaderBinding`; `accesskey.TestDecodePrecedence`; `agewrap.TestAccessIdentityStrictness`; `profile.TestDecodePrecedence`, `TestPinPathMatchesDecode`; `cmd/datekeys.TestDecryptAccessKeyOrder`; `extension.TestCheckCritical`; `codec.TestCheckSchemaVersionForms`; `testdata/vectors/cbor.json`, `inspect_differential.json` |
| 69.1 | Error precedence: the first failing layer of each object (frame, a truncated prelude before the version; type tag and schema version; CBOR profile and CDDL, except the rules with codes of their own; fields with codes of their own in ascending key order, an extension unknown in its object or array before invalid data), the step order of §63 across objects and steps; only the optional inspection of steps 5, 6 and 8 and the `capsule_digest` check can change the code; the codes of step 10 are those of a release supplied directly, one from a network source being discarded at step 9 | `capsule.ParsePrelude`, `capsule.DecodeHeader`, `capsule.DecodeControl`, `accesskey.Decode`, `accesskey.DecodeBody`, `profile.Decode`, `codec.CheckSchema`, `codec.Unmarshal`, `extension.CheckCriticalIn`, `capsule.checkAccessKey`, `OpenOptions.AccessKeyFile`, `agewrap.AccessIdentity`, `provider/drand.Client` | `capsule.TestPrecedenceWithinPublicHeader`, `TestPrecedenceAcrossSteps`, `TestDecodeHeaderReportsTheCDDLFirst`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestControlCriticalBeforeHeaderBinding`, `TestReleaseFromANetworkSource`, `TestExtensionPlacement`; `accesskey.TestDecodePrecedence`; `agewrap.TestAccessIdentityStrictness`; `profile.TestDecodePrecedence`, `TestPinPathMatchesDecode`; `cmd/datekeys.TestDecryptAccessKeyOrder`; `extension.TestCheckCritical`, `TestPlacement`; `codec.TestCheckSchemaVersionForms`; `testdata/vectors/cbor.json`, `inspect_differential.json` |
| 70 | Compatibility | magic and version checks; `codec.Peek` and `codec.CheckSchema` read keys 0 and 1 only, before strict decoding, with a type tag of at most `codec.MaxTypeTagLen` bytes | mutations; `codec.TestPeek`, `TestCheckSchema`, `TestCheckSchemaVersionForms`, `FuzzPeek`; `capsule.TestDecodeSchemaVersion` |
| 71 | Profile registry | `profile.Decode` + `profile.NewRegistry` with pinned hashes | `profile.TestRegistry` |
| 72 | Extension registry and registration rules; the encoder decodes its own output before sealing; security-relevant claims in CONTROL_CBOR or under a signature extension, `.dkk` extension data advisory | `extension.Registry`, `extension.Set`, `extension.DataValidator`; self-checks in `capsule.Encrypt` and `accesskey.MarshalBody` | `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestNestedDataSealsAndOpens`, `FuzzEncodeImpliesDecode` |
| 72 | Extension registry and registration rules, among them the objects and arrays where each extension may appear; the encoder decodes its own output before sealing; security-relevant claims in CONTROL_CBOR or under a signature extension, `.dkk` extension data advisory | `extension.Registry`, `extension.Set`, `extension.DataValidator`, `extension.Placement` (optional: a `Registry` without it knows its extensions in every object and array); self-checks in `capsule.Encrypt` and `accesskey.MarshalBody` | `capsule.TestKnownCriticalExtensions`, `TestUnusableNoncriticalExtensions`, `TestExtensionPlacement`, `TestNestedDataSealsAndOpens`, `FuzzEncodeImpliesDecode`; `extension.TestPlacement` |
| 74 | Provisional aspects; the implementation limits of the reference (name lengths, `public_key`, `period`, maximum `extension_id` length, `dk1_` length, age parser limits, `ERR_POLICY_STRUCTURE_MISMATCH` for INNER_ACCESS_AGE) | `profile.ValidID`, `validName`, `maxPublicKeyLen`, `maxPeriod`; `extension.MaxIDLen`; `datekey.MaxEncodedLen`; `filippo.io/age` | `profile.TestValidateRejectsTamperedProfiles`, `TestIntegerRanges`; `extension.TestNew`; the vectors of `cbor.json` named after the implementation limit |
| 75 | Blocking requirements before v1.0 | items 1–9 above; item 10 (external review) pending | — |
| 76 | Change policy; the v0.8.2 extension change and its reproducible cases; the v0.8.2 refinements and theirs; the v0.8.2 amendment on point canonicality and its case (a second implementation on `tlock-js` and `@noble/curves` 1.9.7 accepted U with c0 + p and a signature with x + p) | `extension`, `codec`, fixture `time_only_extensions` regenerated; refinements: the order of `capsule.checkAccessKey`, `BODY_LEN` 0 in `accesskey.Decode`, CR and LF in `datekey.Parse`, the `.dkk` decoded at step 9.a (`OpenOptions.AccessKeyFile`, the CLI), nil identities in `capsule.Open`, every identity tried in `agewrap.AccessIdentity`, `profile.NewRegistry` through `Decode`, `Profile.Validate` rule 1 first; three new `dk1.json` vectors | case 2: `extension.TestNew`; case 3: `capsule.TestNaNKeyedDataHasOneVerdict`; case 4: `capsule.TestExtensionFixtureData`; case 5: `capsule.TestNestedDataSealsAndOpens`; case 6: `capsule.TestHugeExtensionArraysAreRejected`, `extension.TestCheckDisjointIsLinear`; refinements: the tests of rows 12.1, 15, 17, 19, 22, 28.1, 35, 36, 40, 51, 55.1, 63 and 69.1, and `extension.TestOrderIsUnsignedBytewise`; amendment: the tests of rows 12.2 and 64 |
| 76 | Change policy; the normative changes of v0.8.2: the extension change and its reproducible cases; the refinements and theirs; the amendment on point canonicality and its case (a second implementation on `tlock-js` and `@noble/curves` 1.9.7 accepted U with c0 + p and a signature with x + p); the corrections of the formal review (an invalid release from a network source, the objects and arrays of each extension, the serialization of GT in H2) and their cases | `extension`, `codec`, fixture `time_only_extensions` regenerated; refinements: the order of `capsule.checkAccessKey`, `BODY_LEN` 0 in `accesskey.Decode`, CR and LF and invalid UTF-8 in `datekey.Parse`, the `.dkk` decoded at step 9.a (`OpenOptions.AccessKeyFile`, the CLI), nil identities in `capsule.Open`, every identity tried in `agewrap.AccessIdentity`, `profile.NewRegistry` through `Decode`, `Profile.Validate` rule 1 first; four new `dk1.json` vectors; corrections: `extension.Placement` and the object-aware checks, the `provider.ReleaseSource` contract, `testdata/vectors/tlock_ibe.json` | case 2: `extension.TestNew`; case 3: `capsule.TestNaNKeyedDataHasOneVerdict`; case 4: `capsule.TestExtensionFixtureData`; case 5: `capsule.TestNestedDataSealsAndOpens`; case 6: `capsule.TestHugeExtensionArraysAreRejected`, `extension.TestCheckDisjointIsLinear`; refinements: the tests of rows 12.1, 15, 17, 19, 22, 28.1, 35, 36, 40, 51, 55.1, 63 and 69.1, and `extension.TestOrderIsUnsignedBytewise`; amendment: the tests of rows 12.2 and 64; corrections: `capsule.TestReleaseFromANetworkSource`, `TestExtensionPlacement`, `extension.TestPlacement`, `agewrap.TestTlockH2Vector` |
## Error mapping
@ -105,14 +105,15 @@ decides which code is reported.
| Schema version other than 1, read as the second key, after a type tag within the profile, as an unsigned integer in its shortest form of at most 2^53−1; whatever follows it | `ERR_UNSUPPORTED_VERSION` | §69.1, §70 |
| Truncated framing, length fields of 0 or beyond the §57 limits, an object above its §57 frame on encode or decode, data after BODY_CBOR, a malformed OUTER_TIME_AGE or PAYLOAD_AGE (an age header against the C2SP grammar, without stanzas, or beyond the parser limits of `filippo.io/age`: 1024 stanzas, 128 arguments, 2 MiB), a tlock stanza body of a length other than \|U\| + 32, with a U that is not the canonical encoding of a point of the key group (§12.2) or is the identity, or that fails the IBE check r·G == U (step 11), a malformed X25519 stanza (steps 13 and 17), a failed header MAC, a truncated or modified STREAM, trailing data after PAYLOAD_AGE, a PAYLOAD_AGE that I_PAYLOAD cannot open (step 17) | `ERR_INTEGRITY` | §22, §23, §28.1, §40, §57, §63 steps 11, 13 and 17, §74 |
| Stanza count or type violations in OUTER_TIME_AGE, PAYLOAD_AGE or INNER_ACCESS_AGE in a header that parses; a repeated X25519 ephemeral share in INNER_ACCESS_AGE (step 12); an offered identity that unwraps more than one INNER_ACCESS_AGE stanza, whatever the order of the identities (step 13); a malformed INNER_ACCESS_AGE, including one beyond the parser limits, or none, under `time_and_key`; an age intro line under `time_only`; a tlock stanza without exactly two arguments | `ERR_POLICY_STRUCTURE_MISMATCH` | §28.1, §36, §63 steps 8, 12 and 13 |
| tlock stanza round argument not exactly the canonical decimal DateKey round (steps 8 and 11); a release for another round, checked before its signature (step 10) | `ERR_ROUND_MISMATCH` | §17, §63 steps 8, 10 and 11 |
| A release signature that is not the canonical encoding of a point of the signature group of the scheme (§12.2), is the identity, or does not verify under the pinned key for the DateKey round | `ERR_RELEASE_INVALID` | §12.2, §51, §63 step 10 |
| tlock stanza round argument not exactly the canonical decimal DateKey round (steps 8 and 11); a release for another round supplied directly, checked before its signature (step 10) | `ERR_ROUND_MISMATCH` | §17, §63 steps 8, 10 and 11 |
| A release supplied directly whose signature is not the canonical encoding of a point of the signature group of the scheme (§12.2), is the identity, or does not verify under the pinned key for the DateKey round | `ERR_RELEASE_INVALID` | §12.2, §51, §63 step 10 |
| tlock stanza chain hash not exactly the lowercase hex chain hash of the pinned profile; profile whose parameters do not hash to its chain hash; a pinned profile with another `profile_hash` | `ERR_PROFILE_MISMATCH` | §12.1, §63 step 8 |
| Instant before the profile genesis or whose round time would be after 9999-12-31T23:59:59Z; `dk1_` round outside 1..2^53−1; round time after 9999-12-31T23:59:59Z under the pinned profile (step 7) | `ERR_DATEKEY_INVALID` | §15, §19 |
| Provider Profile that cannot be pinned: name alphabets (§12.1) and lengths (§74), public key size (§74), `period` above 2^32−1 (preceded in the reference by its 86400 s limit, `ERR_NON_CANONICAL_CBOR`), `genesis_time` outside 1..253402300798, `provider` other than `drand`, a scheme tlock does not support, a public key that is not the canonical encoding of a point of the scheme's key group (§12.2) or is the identity | `ERR_UNKNOWN_PROFILE` | §12.1, §12.2 |
| Unknown `access_type`, wrong material length, `.dkk` for another `capsule_id`, `capsule_digest` mismatch (step 9.a); no offered identity is a recipient of INNER_ACCESS_AGE (step 13) | `ERR_ACCESS_INVALID` | §57, §63 steps 9 and 13 |
| `time_and_key` and no credential offered (nil identities are none), before the clock is consulted | `ERR_ACCESS_REQUIRED` | §63 step 9 |
| Round time not reached yet (no request is made), no source delivered the release | `ERR_RELEASE_UNAVAILABLE` | §63 step 9 |
| Round time not reached yet (no request is made), no source delivered the release, or a network source discarded every response for breaking the rules of step 10 | `ERR_RELEASE_UNAVAILABLE` | §63 step 9 |
| An unknown critical extension, or a known one in an object or array it is not registered for, before any invalid data (steps 4, 9.a and 14) | `ERR_EXTENSION_CRITICAL_UNKNOWN` | §54, §63, §69.1, §72 |
## Implementation decisions
@ -133,9 +134,11 @@ provisional (§74). None changes the protocol semantics.
4. **Reading `dk1_`.** JSON numbers are compared by their exact decimal
value, and the Base64 decoding accepts padding, the standard alphabet and
non-zero trailing bits, which the final comparison rejects as
non-canonical; CR and LF, which the Go decoders skip, and a byte order
mark are invalid: §19. Until the refinements CR and LF were
`ERR_DATEKEY_NON_CANONICAL`.
non-canonical; CR and LF, which the Go decoders skip, a byte order mark
and invalid UTF-8, which `encoding/json` replaces with U+FFFD, are
invalid: §19. Until the refinements CR and LF were
`ERR_DATEKEY_NON_CANONICAL`, and so was invalid UTF-8 in a member that a
repeated name overwrites until 692cf87.
5. **Strict tlock stanza arguments.** Exact string comparison; a round with
leading zeros or a sign, or an uppercase chain hash, is a mismatch: §63
step 8.
@ -178,6 +181,15 @@ provisional (§74). None changes the protocol semantics.
The reference already rejected every other encoding through the decoder
of `kilic/bls12-381`; a U at infinity, which only the IBE check used to
reject, is now refused before decryption, with the same code.
14. **Invalid releases from the network.** Settled by the corrections of the
formal review: `provider/drand.Client` verifies every relay response and
discards the invalid ones, so a relay that returns no valid release gives
`ERR_RELEASE_UNAVAILABLE` at step 9, and only a release supplied directly
gets the codes of step 10: §63 steps 9 and 10, §69.1.
15. **GT in H2.** Settled by the corrections of the formal review: H2 hashes
the element of GT as `kilic/bls12-381` serializes it, through tlock and
drand/kyber, with the frozen vector of `testdata/vectors/tlock_ibe.json`:
§63 step 11.
### Still implementation decisions
@ -186,7 +198,10 @@ provisional (§74). None changes the protocol semantics.
2. **Extension data.** `extension.MaxDataLen` is 64 MiB, the largest frame,
the container frame being the effective bound; an application validates
the data of the extensions it knows through the optional
`extension.DataValidator` of its `Registry`.
`extension.DataValidator` of its `Registry`, and declares the objects and
arrays each one is registered for (§72) through the optional
`extension.Placement`; a `Registry` without it knows its extensions in
every object and array, as before the formal review.
3. **`capsule_digest`.** Written by `Encrypt` for every portable key and
checked at step 9 when the capsule reader is seekable; it remains a UX
shortcut and an optional check (§43, §69.1).

@ -14,6 +14,9 @@
// Registry that also implements DataValidator checks the data of the
// extensions it knows: invalid data rejects a critical extension with
// ErrExtensionDataInvalid and makes a noncritical one Unusable (spec §54).
// A Registry that also implements Placement tells in which objects and
// arrays each extension is registered: elsewhere a known extension is
// treated as unknown (spec §54, §72).
package extension
import (
@ -79,7 +82,80 @@ type DataValidator interface {
ValidateData(e Extension) error
}
// Set is a simple Registry. It does not validate data.
// Object names an object that carries extension arrays (spec §54).
type Object int
// The objects that carry extensions.
const (
PublicHeader Object = iota + 1 // PUBLIC_HEADER, keys 5 and 6 (spec §24)
Control // CONTROL_CBOR, keys 4 and 5 (spec §31)
AccessKey // the body of a .dkk, keys 7 and 8 (spec §41)
)
// String returns the name of the object in the specification.
func (o Object) String() string {
switch o {
case PublicHeader:
return "PUBLIC_HEADER"
case Control:
return "CONTROL_CBOR"
case AccessKey:
return ".dkk"
}
return fmt.Sprintf("Object(%d)", int(o))
}
// Array names one of the two extension arrays of an object.
type Array int
// The two extension arrays of an object.
const (
Critical Array = iota + 1 // critical_extensions
Noncritical // noncritical_extensions
)
// String returns the name of the array in the specification.
func (a Array) String() string {
switch a {
case Critical:
return "critical_extensions"
case Noncritical:
return "noncritical_extensions"
}
return fmt.Sprintf("Array(%d)", int(a))
}
// Placement is an optional interface of a Registry. RegisteredIn reports
// whether (id, version) is registered for the array arr of the object obj:
// the registration of each extension declares the objects and arrays where
// it may appear (spec §72). It is called only for extensions the Registry
// knows. A known extension that appears in an object or array it is not
// registered for is treated there as unknown (spec §54): a critical one is
// rejected with ErrExtensionCriticalUnknown and a noncritical one is ignored,
// its data unchecked. A Registry that does not implement Placement knows
// each of its extensions in every object and array.
type Placement interface {
RegisteredIn(id string, version uint64, obj Object, arr Array) bool
}
// KnownIn reports whether reg knows (id, version) in the array arr of obj:
// reg knows it and, when reg is a Placement, registers it there (spec §54,
// §72). A nil Registry knows none. It is the rule of CheckCriticalIn and
// CheckNoncriticalIn, for an application that interprets the extensions of
// an object it has read: an extension unknown there is not interpreted.
func KnownIn(reg Registry, id string, version uint64, obj Object, arr Array) bool {
return reg != nil && reg.Known(id, version) && registered(reg, id, version, obj, arr)
}
// registered reports whether reg, which knows (id, version), registers it for
// the array arr of obj: always when reg is not a Placement.
func registered(reg Registry, id string, version uint64, obj Object, arr Array) bool {
p, ok := reg.(Placement)
return !ok || p.RegisteredIn(id, version, obj, arr)
}
// Set is a simple Registry. It does not validate data, and it knows each of
// its extensions in every object and array.
type Set map[string][]uint64
// Known reports whether (id, version) is in the set.
@ -289,12 +365,31 @@ func sorted(exts []Extension) []Extension {
// CheckCritical rejects every critical extension unknown to reg with
// ErrExtensionCriticalUnknown and, when reg is a DataValidator, every known
// one whose data it rejects with ErrExtensionDataInvalid (spec §54, §70).
// An unknown extension takes precedence over invalid data.
// An unknown extension takes precedence over invalid data. It does not know
// the object of the array and consults no Placement: CheckCriticalIn does.
func CheckCritical(critical []Extension, reg Registry) error {
return checkCritical(critical, reg, 0)
}
// CheckCriticalIn is CheckCritical for the critical_extensions of obj: an
// extension that reg knows but, as a Placement, does not register for that
// array of obj is unknown there, ErrExtensionCriticalUnknown (spec §54,
// §72). The reading flow of package capsule checks every critical array
// with it.
func CheckCriticalIn(obj Object, critical []Extension, reg Registry) error {
return checkCritical(critical, reg, obj)
}
// checkCritical checks the critical extensions of obj, or of any object when
// obj is 0: every unknown one first, then the data of the known ones.
func checkCritical(critical []Extension, reg Registry, obj Object) error {
for _, c := range critical {
if reg == nil || !reg.Known(c.ID, c.Version) {
return fmt.Errorf("extension %s v%d: %w", c.ID, c.Version, datekeys.ErrExtensionCriticalUnknown)
}
if obj != 0 && !registered(reg, c.ID, c.Version, obj, Critical) {
return fmt.Errorf("extension %s v%d: known, but not registered for the %s of %s: %w", c.ID, c.Version, Critical, obj, datekeys.ErrExtensionCriticalUnknown)
}
}
for _, c := range critical {
if err := validateData(c, reg); err != nil {
@ -316,11 +411,27 @@ type Unusable struct {
// CheckNoncritical returns the noncritical extensions that reg knows and whose
// data it rejects. It never fails the object: unknown noncritical extensions
// are ignored, and a Registry that is not a DataValidator rejects no data
// (spec §54).
// (spec §54). It does not know the object of the array and consults no
// Placement: CheckNoncriticalIn does.
func CheckNoncritical(noncritical []Extension, reg Registry) []Unusable {
return checkNoncritical(noncritical, reg, 0)
}
// CheckNoncriticalIn is CheckNoncritical for the noncritical_extensions of
// obj: an extension that reg knows but, as a Placement, does not register
// for that array of obj is unknown there and ignored, its data unchecked
// (spec §54, §72). The reading flow of package capsule checks every
// noncritical array with it.
func CheckNoncriticalIn(obj Object, noncritical []Extension, reg Registry) []Unusable {
return checkNoncritical(noncritical, reg, obj)
}
// checkNoncritical checks the noncritical extensions of obj, or of any
// object when obj is 0.
func checkNoncritical(noncritical []Extension, reg Registry, obj Object) []Unusable {
var out []Unusable
for _, n := range noncritical {
if reg == nil || !reg.Known(n.ID, n.Version) {
if reg == nil || !reg.Known(n.ID, n.Version) || obj != 0 && !registered(reg, n.ID, n.Version, obj, Noncritical) {
continue
}
if err := validateData(n, reg); err != nil {

@ -358,6 +358,92 @@ func TestCheckNoncritical(t *testing.T) {
}
}
// placed is validator with the placement of its registrations (spec §72):
// org.a only in the critical_extensions of CONTROL_CBOR, org.b only in the
// noncritical_extensions of PUBLIC_HEADER and of the .dkk.
type placed struct{ validator }
func (placed) RegisteredIn(id string, v uint64, obj extension.Object, arr extension.Array) bool {
switch id {
case "org.a":
return obj == extension.Control && arr == extension.Critical
case "org.b":
return arr == extension.Noncritical && obj != extension.Control
}
return false
}
// Spec §54, §72: a known extension that appears in an object or array it is
// not registered for is treated there as unknown. A Registry that is not a
// Placement knows its extensions everywhere, and CheckCritical and
// CheckNoncritical, which do not know the object, consult no Placement.
func TestPlacement(t *testing.T) {
objects := []extension.Object{extension.PublicHeader, extension.Control, extension.AccessKey}
arrays := []extension.Array{extension.Critical, extension.Noncritical}
for _, obj := range objects {
for _, arr := range arrays {
if !extension.KnownIn(validator{}, "org.a", 1, obj, arr) || extension.KnownIn(validator{}, "org.c", 1, obj, arr) || extension.KnownIn(nil, "org.a", 1, obj, arr) {
t.Fatalf("without a Placement, in the %s of %s", arr, obj)
}
if got, want := extension.KnownIn(placed{}, "org.a", 1, obj, arr), obj == extension.Control && arr == extension.Critical; got != want {
t.Errorf("org.a in the %s of %s: known %v", arr, obj, got)
}
if extension.KnownIn(placed{}, "org.a", 2, obj, arr) {
t.Errorf("org.a v2, which the registry does not know, in the %s of %s", arr, obj)
}
}
}
// A critical extension outside its registration is unknown there, with
// the object in the message.
crit := []extension.Extension{ext(t, "org.a", 1, []byte("ok"))}
if err := extension.CheckCriticalIn(extension.Control, crit, placed{}); err != nil {
t.Fatalf("where it is registered: %v", err)
}
for _, obj := range []extension.Object{extension.PublicHeader, extension.AccessKey} {
err := extension.CheckCriticalIn(obj, crit, placed{})
if !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) || !strings.Contains(err.Error(), obj.String()) {
t.Errorf("CONTROL_CBOR extension in the critical_extensions of %s: %v", obj, err)
}
if err := extension.CheckCriticalIn(obj, crit, validator{}); err != nil {
t.Errorf("a Registry that is not a Placement, in %s: %v", obj, err)
}
}
if err := extension.CheckCritical(crit, placed{}); err != nil {
t.Errorf("CheckCritical consulted the Placement: %v", err)
}
// Unknown there comes before the invalid data of another extension.
mixed := []extension.Extension{ext(t, "org.a", 1, []byte("ko")), ext(t, "org.b", 1, []byte("ok"))}
if err := extension.CheckCriticalIn(extension.Control, mixed, placed{}); !errors.Is(err, datekeys.ErrExtensionCriticalUnknown) {
t.Errorf("noncritical-only extension in a critical array, after invalid data: %v", err)
}
// A noncritical extension outside its registration is ignored, its data
// unchecked; where it is registered, invalid data makes it unusable.
non := []extension.Extension{ext(t, "org.b", 1, []byte("ko"))}
for _, obj := range objects {
u := extension.CheckNoncriticalIn(obj, non, placed{})
if obj == extension.Control && u != nil || obj != extension.Control && (len(u) != 1 || u[0].ID != "org.b") {
t.Errorf("org.b with invalid data in the noncritical_extensions of %s: %+v", obj, u)
}
if u := extension.CheckNoncriticalIn(obj, non, validator{}); len(u) != 1 {
t.Errorf("a Registry that is not a Placement, in %s: %+v", obj, u)
}
}
if u := extension.CheckNoncritical(non, placed{}); len(u) != 1 {
t.Errorf("CheckNoncritical consulted the Placement: %+v", u)
}
for v, want := range map[fmt.Stringer]string{
extension.PublicHeader: "PUBLIC_HEADER", extension.Control: "CONTROL_CBOR", extension.AccessKey: ".dkk", extension.Object(0): "Object(0)",
extension.Critical: "critical_extensions", extension.Noncritical: "noncritical_extensions", extension.Array(3): "Array(3)",
} {
if v.String() != want {
t.Errorf("%s, want %s", v, want)
}
}
}
// FuzzDecodeArray: whatever DecodeArray accepts is in canonical order and
// re-encodes to its input with EncodeArray, and every error carries
// ErrNonCanonicalCBOR.

@ -6,6 +6,7 @@ require (
filippo.io/age v1.3.2
github.com/drand/drand/v2 v2.1.7
github.com/drand/kyber v1.3.2
github.com/drand/kyber-bls12381 v0.3.4
github.com/drand/tlock v1.2.0
golang.org/x/crypto v0.57.0
)
@ -13,7 +14,6 @@ require (
require (
filippo.io/hpke v0.4.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/drand/kyber-bls12381 v0.3.4 // indirect
github.com/kilic/bls12-381 v0.1.0 // indirect
github.com/nikkolasg/hexjson v0.1.0 // indirect
go.dedis.ch/fixbuf v1.0.3 // indirect

@ -124,7 +124,14 @@ func vectors(dir string) error {
if err != nil {
return err
}
return testkit.WriteJSON(filepath.Join(dir, "cbor.json"), cv)
if err := testkit.WriteJSON(filepath.Join(dir, "cbor.json"), cv); err != nil {
return err
}
iv, err := testkit.IBEVectors()
if err != nil {
return err
}
return testkit.WriteJSON(filepath.Join(dir, "tlock_ibe.json"), iv)
}
// mutationsName is the -only name that rebuilds the capsules of the

@ -0,0 +1,73 @@
package testkit
import (
"crypto/sha256"
"encoding/hex"
bls "github.com/drand/kyber-bls12381"
"github.com/drand/kyber/encrypt/ibe"
)
// H2Len is the length of H2 in tlock: that of V, of W and of the file key it
// wraps (spec §63 step 11).
const H2Len = 16
// GTLen is the length of the serialization of an element of GT: twelve
// coordinates of Fp, 48 bytes each.
const GTLen = 576
// H2 is the H2 of the IBE-CCA of tlock (spec §63 step 11): SHA-256 of the
// tag IBE-H2 and the serialization of an element of GT, truncated to 16
// bytes. It restates the unexported gtToHash of drand/kyber encrypt/ibe, with
// its exported tag.
func H2(gt []byte) []byte {
h := sha256.New()
h.Write(ibe.H2Tag())
h.Write(gt)
return h.Sum(nil)[:H2Len]
}
// IBEVector is one vector of H2 (spec §63 step 11): two points, the
// serialization of their pairing and its H2.
type IBEVector struct {
Name string `json:"name"`
G1 string `json:"g1"` // a point of G1, compressed (spec §12.2)
G2 string `json:"g2"` // a point of G2, compressed (spec §12.2)
GT string `json:"gt"` // e(g1, g2), 576 bytes in the order of kilic/bls12-381
H2 string `json:"h2"` // H2(gt), 16 bytes
}
// IBEVectorFile is testdata/vectors/tlock_ibe.json.
type IBEVectorFile struct {
Spec string `json:"spec"`
Description string `json:"description"`
Vectors []IBEVector `json:"vectors"`
}
// IBEVectors computes the vectors of H2 with drand/kyber-bls12381, the
// pairing and the serialization of GT that tlock uses.
func IBEVectors() (IBEVectorFile, error) {
s := bls.NewBLS12381Suite()
g1, g2 := s.G1().Point().Base(), s.G2().Point().Base()
var enc [3][]byte
for i, m := range []interface{ MarshalBinary() ([]byte, error) }{g1, g2, s.Pair(g1, g2)} {
b, err := m.MarshalBinary()
if err != nil {
return IBEVectorFile{}, err
}
enc[i] = b
}
return IBEVectorFile{
Spec: SpecVersion,
Description: "H2 of the IBE-CCA of tlock (spec §63 step 11): SHA-256 of \"IBE-H2\" and the 576 bytes of an element of GT, " +
"c1 before c0 at every level of the tower and each coordinate of Fp in 48 bytes big-endian (the order of kilic/bls12-381), " +
"truncated to 16 bytes. Generated by the reference implementation with drand/kyber-bls12381, the pairing of tlock.",
Vectors: []IBEVector{{
Name: "H2(e(G1, G2)), the generators of G1 and G2",
G1: hex.EncodeToString(enc[0]),
G2: hex.EncodeToString(enc[1]),
GT: hex.EncodeToString(enc[2]),
H2: hex.EncodeToString(H2(enc[2])),
}},
}, nil
}

@ -2,7 +2,10 @@
// (spec §48, §49). HTTP is an untrusted transport: every response is verified
// locally with provider.Verify against the pinned profile before it is
// returned, and authenticity comes from the BLS signature, never from the
// hostname (spec §48, §52).
// hostname (spec §48, §52). A response that fails is discarded; when no relay
// returns a valid release, the code of the error of Fetch is
// datekeys.ErrReleaseUnavailable, that of spec §63 step 9, and the failure of
// each relay is joined to it for diagnosis.
package drand
import (

@ -28,8 +28,16 @@ type Release struct {
Signature []byte
}
// ReleaseSource fetches the release of a condition. Implementations need not
// verify it; callers always do, with Verify.
// ReleaseSource fetches the release of a condition. Callers always verify the
// release, with Verify (spec §63 step 10).
//
// A source that fetches releases over a network (a relay, the Release API or
// a cache) must also verify each response with Verify and discard the one
// that fails, and report datekeys.ErrReleaseUnavailable when no response
// passes (spec §63 step 9), as provider/drand.Client does: an invalid release
// from the network is then reported at step 9, not with the codes of step 10.
// A source that hands over a release the caller supplies directly need not
// verify it; its release gets the codes of step 10.
//
// Errors should wrap datekeys.ErrReleaseUnavailable when the release cannot be
// obtained, for example because the round is not published yet.

@ -100,7 +100,7 @@ El protocolo base no garantiza:
- revocación de una copia ya distribuida;
- anonimato absoluto;
- autoría legal por metadatos;
- fecha probatoria solo por `created_at`;
- fecha probatoria de creación;
- protección frente a un dispositivo ya comprometido;
- control del plaintext después de un descifrado legítimo.
@ -599,13 +599,13 @@ Debe ser:
- independiente de identidad, fecha o servicio;
- de al menos 128 bits de entropía.
V1 recomienda exactamente:
En V1, `capsule_id` MUST ser exactamente:
```text
16 random bytes
```
generados por CSPRNG.
generados por un CSPRNG. §24 y `datekeys.cddl` fijan esa longitud (§57).
---
@ -757,11 +757,11 @@ La implementación SHOULD inspeccionar, antes de utilizar secretos o realizar un
Esta inspección previa permite rechazar una cápsula mal formada antes de solicitar un release. Además de reducir trabajo innecesario, evita que una cápsula inválida genere una consulta observable en la Release API o en los relays.
Estas comprobaciones de stanzas previas al descifrado son estructurales. Su autenticidad frente a modificaciones de terceros queda confirmada únicamente cuando la cabecera `age` correspondiente supera la verificación del MAC durante la apertura. Frente a un creador que incluya vías alternativas de descifrado mediante stanzas adicionales, el MAC es válido por construcción y la comprobación estructural es la defensa del protocolo.
Estas comprobaciones de stanzas previas al descifrado son estructurales. Su autenticidad frente a modificaciones de terceros queda confirmada únicamente cuando la cabecera `age` correspondiente supera la verificación del MAC durante la apertura, y solo frente a quien no conoce la file key (§55.1): una vez publicada la ronda, cualquiera puede calcular `FK_TIME` y recalcular el MAC de `OUTER_TIME_AGE` (§64). Frente a un creador que incluya vías alternativas de descifrado mediante stanzas adicionales, el MAC es válido por construcción y la comprobación estructural es la defensa del protocolo.
La inspección pre-unlock es una optimización de validación y privacidad y, por tanto, es un SHOULD. La aplicación de las reglas de cardinalidad de las secciones 29, 32 y 33 es un MUST y DEBE realizarse, como mínimo, en el momento de abrir cada fichero `age`: la identity que desenvuelve la file key MUST rechazar el fichero si el conjunto completo de stanzas recibido viola la regla correspondiente.
La autenticidad criptográfica definitiva de `PUBLIC_HEADER` se comprueba mediante `header_binding` tras abrir el control, salvo que una extensión adicional aporte autenticidad previa.
`header_binding` vincula `PUBLIC_HEADER` al control abierto (§63, paso 15): aporta coherencia interna, no autoría ni fecha (§55.1). Solo una extensión de firma puede aportar autenticidad del creador (§36.1, §72).
---
@ -938,7 +938,8 @@ Reglas:
- un mismo `extension_id` MUST NOT aparecer simultáneamente en `critical_extensions` y `noncritical_extensions` dentro del mismo objeto;
- por esas dos reglas, un `extension_id` aparece como mucho una vez en cada objeto, aunque cambie `extension_version`; si un schema necesita varios valores, los lleva dentro de su `data`;
- una extensión crítica desconocida MUST provocar rechazo;
- una extensión no crítica desconocida MAY ignorarse.
- una extensión no crítica desconocida MAY ignorarse;
- una extensión conocida que aparece en un objeto o array para el que no está registrada se trata allí como desconocida (§54, §72).
El protocolo base no decodifica ni valida el contenido de `data` (§54).
@ -1075,7 +1076,7 @@ el resultado MUST ser directamente un `CONTROL_CBOR` canónico válido. Un resul
### Si `access_policy = time_and_key`
el resultado MUST ser un fichero age v1 válido cuyo header contenga uno o más stanzas, todos ellos de tipo X25519, y sin dos stanzas de un solo argumento con el mismo argumento, que en un stanza X25519 es su share efímero. Un resultado cuya cabecera no sigue la gramática de §28.1 no lo es.
el resultado MUST ser un fichero age v1 válido cuyo header contenga uno o más stanzas, todos ellos de tipo X25519, y sin dos stanzas de un solo argumento tras el tipo con el mismo argumento, que en un stanza X25519 es su share efímero. Un resultado cuya cabecera no sigue la gramática de §28.1 no lo es.
`age` genera un share efímero nuevo para cada stanza X25519: dos stanzas con el mismo share no los produce ningún encoder conforme y, para un mismo recipient, serían dos stanzas para él (§33). Es la parte de «exactamente un stanza por recipient» que se comprueba sin secretos; el resto, y la forma de cada stanza X25519, los comprueba el paso 13 de §63 con las identities ofrecidas.
@ -1450,6 +1451,7 @@ Reglas:
- una extensión crítica desconocida → MUST reject (`ERR_EXTENSION_CRITICAL_UNKNOWN`);
- una extensión no crítica desconocida → MAY ignore;
- una extensión conocida que aparece en un objeto o array para el que no está registrada (§72) se trata allí como desconocida: crítica → MUST reject (`ERR_EXTENSION_CRITICAL_UNKNOWN`); no crítica → MAY ignore, y su `data` no se interpreta;
- los elementos de cada array MUST estar en orden estrictamente ascendente de los bytes UTF-8 de `extension_id` (ver abajo), lo que también prohíbe repetir un `extension_id` dentro del array;
- un mismo `extension_id` MUST NOT aparecer simultáneamente en `critical_extensions` y `noncritical_extensions` dentro del mismo objeto;
- por esas dos reglas, un mismo `extension_id` no aparece más de una vez en el mismo objeto, aunque cambie `extension_version`; la multiplicidad, si un schema la necesita, va dentro de su `data`;
@ -1467,7 +1469,7 @@ Orden de `extension_id`: dos identificadores se comparan byte a byte sobre su co
Una `data` vacía o de tipo distinto de `bstr` y un array de más de 64 extensiones son violaciones del CDDL: MUST rechazarse con `ERR_NON_CANONICAL_CBOR` (§57).
Solo una implementación que conoce `(extension_id, extension_version)` interpreta su `data`, conforme a su schema registrado (§72):
Solo una implementación que conoce `(extension_id, extension_version)` en el objeto y el array en que aparece interpreta su `data`, conforme a su schema registrado (§72):
- una extensión crítica conocida cuya `data` no cumple su schema registrado → MUST reject (`ERR_EXTENSION_DATA_INVALID`);
- una extensión no crítica conocida cuya `data` no cumple su schema registrado no invalida el objeto: la implementación MUST tratarla como inutilizable, sin usar su `data`, y MUST notificarlo al llamador.
@ -1505,7 +1507,7 @@ La tabla resume, para cada sección de un `.dkc` y para una `.dkk`, quién puede
| `PRELUDE` y `PUBLIC_HEADER` | Cualquiera que tenga el `.dkc`: viajan en claro y ninguna clave los protege. | Paso 15: `header_binding`, dentro de `CONTROL_CBOR`, cubre sus bytes exactos (§26). Los pasos 1 a 8 solo comprueban su estructura. | Autoría ni fecha de creación: `header_binding` se calcula con bytes públicos (§36.1). Antes del paso 15, ni siquiera su coherencia con el control. |
| `CONTROL_CBOR` | Cualquiera puede sellar un control hacia la DateKey, porque basta la clave pública del perfil (§36.1). En `time_and_key`, uno que abra una credencial dada solo quien conozca la clave pública de su recipient. | Paso 11: el MAC de la cabecera `age` y STREAM de `OUTER_TIME_AGE`, y en `time_and_key` los de `INNER_ACCESS_AGE` en el paso 13, autentican sus bytes frente a quien no conoce la file key. Paso 15: `header_binding` lo vincula a `PUBLIC_HEADER`. | Que lo escribiera el creador de `PUBLIC_HEADER`: un tercero puede sellar otro control con un `header_binding` correcto. Tras la apertura, quien conoce `FK_TIME` o `FK_ACCESS` puede reescribirlo. |
| `PAYLOAD_AGE` | Quien conoce `R_PAYLOAD`: antes de la apertura, solo quien selló el control; después, cualquiera que haya abierto `CONTROL_CBOR` y obtenido `I_PAYLOAD`. | Paso 17: `I_PAYLOAD` desenvuelve `FK_PAYLOAD`, y el MAC de la cabecera y STREAM autentican cada byte (§30.1). | Autoría; tampoco que siga siendo el payload original después de que alguien haya abierto la cápsula, porque puede cifrar otro para `R_PAYLOAD`. |
| Cuerpo de la `.dkk` | Cualquiera que tenga la `.dkk`: no lleva MAC ni firma. | Paso 9: su `capsule_id` debe ser el de `PUBLIC_HEADER`, un valor público, y su `capsule_digest`, cuando existe y se comprueba, la ata a los bytes exactos de un `.dkc` (§43). Paso 13: `access_material` abre `INNER_ACCESS_AGE` solo si es la identity de uno de sus recipients. | Que la emitiera el creador de la cápsula. La `data` de sus extensiones no queda vinculada a nada. |
| Cuerpo de la `.dkk` | Cualquiera que tenga la `.dkk`: no lleva MAC ni firma. | Paso 9.a: su `capsule_id` debe ser el de `PUBLIC_HEADER`, un valor público, y su `capsule_digest`, cuando existe y se comprueba, la ata a los bytes exactos de un `.dkc` (§43). Paso 13: `access_material` abre `INNER_ACCESS_AGE` solo si es la identity de uno de sus recipients. | Que la emitiera el creador de la cápsula. La `data` de sus extensiones no queda vinculada a nada. |
En consecuencia, una afirmación de la que dependa una decisión de seguridad del lector —autorización, identidad, integridad o fecha— no puede apoyarse en `PUBLIC_HEADER` ni en una `.dkk` sin una extensión de firma, y la `data` de las extensiones de una `.dkk` solo informa a quien la posee (§72).
@ -1752,7 +1754,8 @@ y MUST ser independientes.
todas conocidas, alguna con data inválida
→ ERR_EXTENSION_DATA_INVALID.
5. SHOULD: inspeccionar la cabecera age de OUTER_TIME_AGE (§28.1)
5. Leer SEALED_CONTROL exacto (faltan bytes → ERR_INTEGRITY).
SHOULD: inspeccionar su cabecera age, la de OUTER_TIME_AGE (§28.1),
antes de usar red o secretos:
exactamente un stanza;
de tipo tlock.
@ -1800,6 +1803,10 @@ y MUST ser independientes.
validan ni se usan, y ningún error suyo se informa.
Solo entonces, obtener el release; si ninguna fuente lo entrega
→ ERR_RELEASE_UNAVAILABLE.
Una fuente que obtiene releases por red (relay, Release API o caché)
MUST verificar cada respuesta con las reglas del paso 10 y descartar
la que no las cumple; si ninguna entrega un release que las cumpla
→ ERR_RELEASE_UNAVAILABLE (paso 9).
10. Verificar el release localmente (§17, §51), en este orden:
ronda del release distinta de DateKey.round
@ -1810,6 +1817,10 @@ y MUST ser independientes.
es el punto en el infinito o que no verifica con la clave
pública pinneada como firma de la ronda según el scheme de drand
→ ERR_RELEASE_INVALID.
Estos códigos se informan para un release que el llamador
suministra directamente, como en los vectores oficiales: el que
llega por red sin cumplir estas reglas lo descarta su fuente en el
paso 9.
11. Abrir OUTER_TIME_AGE.
La identity tlock MUST recibir y validar el conjunto completo de stanzas
@ -1830,8 +1841,10 @@ y MUST ser independientes.
FK_TIME = W XOR H4(sigma)
r = H3(sigma, FK_TIME)
y MUST comprobar r·G == U, con G el generador del grupo de U. H2, H3
y H4 son las funciones de drand/tlock (§77), sobre SHA-256 con las
etiquetas IBE-H2, IBE-H3 e IBE-H4.
y H4 son las del paquete encrypt/ibe de drand/kyber, que tlock
importa (§77), sobre SHA-256 con las etiquetas IBE-H2, IBE-H3 e
IBE-H4. H2 resume el elemento de GT serializado como fija
«Serialización de GT en H2», tras este flujo.
Un cuerpo de otra longitud, un U que no cumple §12.2 o que es el
punto en el infinito, una comprobación r·G == U que falla o una
file key que no mide 16 bytes → ERR_INTEGRITY.
@ -1852,9 +1865,9 @@ y MUST ser independientes.
las reglas de stanzas del paso 12
→ ERR_POLICY_STRUCTURE_MISMATCH;
un stanza X25519 mal formado según la especificación age, que
exige un único argumento, el share efímero en Base64 sin
padding, de 32 bytes y que no es de orden bajo, y un cuerpo de
32 bytes
exige un único argumento tras el tipo, el share efímero en
Base64 sin padding, de 32 bytes y que no es de orden bajo, y un
cuerpo de 32 bytes
→ ERR_INTEGRITY;
alguna identity desenvuelve más de un stanza, aunque otra
desenvuelva exactamente uno
@ -1867,7 +1880,8 @@ y MUST ser independientes.
14. Parsear CONTROL_CBOR canónico, con las capas de §69.1.
Validar sus extensiones críticas (clave 4) como en el paso 4.
15. Verificar header_binding.
15. Verificar header_binding (§26)
→ ERR_HEADER_BINDING.
Desde aquí la data de las extensiones de PUBLIC_HEADER queda
vinculada al control abierto: coherencia interna (§36.1), no
autoría, salvo que la cubra una extensión de firma (§72).
@ -1885,9 +1899,9 @@ y MUST ser independientes.
18. Commit del plaintext solo si age completa sin error.
```
La inspección de los pasos 5, 6 y 8 es un SHOULD de fail-fast. La aplicación de las reglas de cardinalidad durante los pasos 11, 13 y 17 es un MUST. Una implementación no puede considerar válido un `.dkc` únicamente porque `age` haya podido desenvolver una file key: debe verificar también que el conjunto completo de stanzas cumple la política DateKeys V1.
La inspección de los pasos 5, 6 y 8 es un SHOULD de fail-fast; la lectura de `SEALED_CONTROL` en el paso 5 es obligatoria (§23). La aplicación de las reglas de cardinalidad durante los pasos 11, 13 y 17 es un MUST. Una implementación no puede considerar válido un `.dkc` únicamente porque `age` haya podido desenvolver una file key: debe verificar también que el conjunto completo de stanzas cumple la política DateKeys V1.
Precedencia: el código que se informa es el del primer paso que falla y, dentro de un objeto, el de la primera capa que falla (§69.1). Una implementación que omite los pasos 5, 6 u 8 detecta los mismos fallos, con el mismo código, en los pasos 11 y 17, salvo que antes falle otro paso; §69.1 delimita lo que eso cambia, y los vectores oficiales suponen que esos pasos se realizan.
Precedencia: el código que se informa es el del primer paso que falla y, dentro de un objeto, el de la primera capa que falla (§69.1). Una implementación que omite la inspección de los pasos 5, 6 u 8 detecta los mismos fallos, con el mismo código, en los pasos 11 y 17, salvo que antes falle otro paso; §69.1 delimita lo que eso cambia, y los vectores oficiales suponen que esa inspección se realiza.
La `.dkk` es una entrada distinta del `.dkc`. Una implementación MAY decodificarla al recibirla, antes del paso 1, pero MUST informar de cualquier error suyo —de trama, de schema o de sus campos— solo en el paso 9.a, en el orden de ese paso, y nunca si `access_policy` es `time_only`.
@ -1901,6 +1915,16 @@ Una implementación MAY saltar directamente a ese offset y leer solo la cabecera
Siempre que sea viable, una implementación SHOULD validar todo lo verificable localmente antes de realizar una petición de red o utilizar un secreto. Además de fallar antes, esta regla evita que cápsulas inválidas generen consultas observables en relays o en la Release API.
Serialización de GT en H2. H2, del paso 11, es SHA-256 de la etiqueta `IBE-H2` seguida de los 576 bytes del elemento de GT, truncado a los 16 bytes de V. Esos bytes MUST ser la serialización de kilic/bls12-381, la que usa drand/kyber (§77), sobre la torre Fp2 = Fp[u]/(u² + 1), Fp6 = Fp2[v]/(v³ − (u + 1)) y Fp12 = Fp6[w]/(w² − v): en cada nivel, los coeficientes de mayor a menor grado, y cada elemento de Fp, entre 0 y p − 1, en 48 bytes big-endian:
```text
Fp12 = c0 + c1·w → c1 || c0 (288 bytes cada uno)
Fp6 = c0 + c1·v + c2·v² → c2 || c1 || c0 (96 bytes cada uno)
Fp2 = c0 + c1·u → c1 || c0 (48 bytes cada uno)
```
Es el orden de las coordenadas de G2 en §12.2, c1 antes que c0, en cada nivel de la torre. Una serialización que empieza por c0 en cada nivel, como `Fp12.toBytes` de noble, da otro H2 y otra file key. Vector (`testdata/vectors/tlock_ibe.json`): con G1 y G2 los generadores de sus grupos, H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`; fija a la vez el pairing e y la serialización.
---
## 64. Mutation tests obligatorios
@ -2068,7 +2092,7 @@ Cuando unos bytes violan varias reglas, una implementación MUST informar del c
Cada objeto —Provider Profile, `PUBLIC_HEADER`, `CONTROL_CBOR`, `.dkk`— se valida en cuatro capas, en este orden:
1. **Trama.** La del `.dkc` para `PUBLIC_HEADER` (§22, §23) y la de la `.dkk` para su cuerpo (§40): magic, versión de framing, `FLAGS`, `RESERVED`, longitudes y límites de §57, y la presencia de los bytes que declara cada longitud, en el orden de esas secciones: `ERR_INVALID_MAGIC`, `ERR_UNSUPPORTED_VERSION`, `ERR_INVALID_FLAGS`, `ERR_INTEGRITY`. Un objeto que supera el límite de su trama es `ERR_INTEGRITY`. El Provider Profile y `CONTROL_CBOR` no tienen trama propia; `CONTROL_CBOR` queda acotado por `SEALED_CONTROL`.
1. **Trama.** La del `.dkc` para `PUBLIC_HEADER` (§22, §23) y la de la `.dkk` para su cuerpo (§40): magic, prelude completo, versión de framing, `FLAGS`, `RESERVED`, longitudes y límites de §57, y la presencia de los bytes que declara cada longitud, en el orden de esas secciones: `ERR_INVALID_MAGIC`; un prelude truncado, `ERR_INTEGRITY`, antes que la versión; después `ERR_UNSUPPORTED_VERSION`, `ERR_INVALID_FLAGS` y `ERR_INTEGRITY`. Un objeto que supera el límite de su trama es `ERR_INTEGRITY`. El Provider Profile y `CONTROL_CBOR` no tienen trama propia; `CONTROL_CBOR` queda acotado por `SEALED_CONTROL`.
2. **Tipo y versión de schema**, leídos antes que nada de las claves 0 y 1 (§70). El objeto MUST empezar, con cada cabecera en su forma más corta y dentro del perfil de §58, por: una cabecera de mapa de longitud definida que anuncia al menos dos entradas y no más de la mitad de los bytes que la siguen, porque cada entrada ocupa al menos dos; la clave 0; una cadena de texto, el type tag; la clave 1; y un entero sin signo de como mucho 2⁵³ − 1, la versión. Cualquier otra cosa ahí, incluida una versión de 2⁵³ o más, es `ERR_NON_CANONICAL_CBOR`. Después, un type tag distinto del propio del schema es `ERR_NON_CANONICAL_CBOR`, sea cual sea la versión. Solo entonces una versión distinta de 1 es `ERR_UNSUPPORTED_VERSION`, sea lo que sea lo que la sigue: claves desconocidas, elementos fuera del perfil, truncado o bytes sobrantes.
3. **Codificación y schema.** El perfil de §58, la igualdad con la recodificación y toda regla normativa de `datekeys.cddl` para el objeto, incluidos tipos, tamaños, rangos, el máximo de 64 extensiones, el orden y la unicidad de `extension_id` (§54) y los límites de implementación que se apliquen con ese código (§74): `ERR_NON_CANONICAL_CBOR`. Quedan fuera las reglas a las que §57 asigna código propio —la sintaxis de `compact_datekey`, `access_type`, la longitud de `access_material`, los nombres y la clave pública del Provider Profile—: de ellas, esta capa solo comprueba el tipo CBOR del campo, y el resto pasa a la capa 4.
4. **Campos semánticos** con código propio (§57), cada uno ya con el tipo CBOR de su regla, en orden ascendente de clave:
@ -2077,11 +2101,11 @@ Cada objeto —Provider Profile, `PUBLIC_HEADER`, `CONTROL_CBOR`, `.dkk`— se v
- `CONTROL_CBOR`, en el paso 14: las extensiones críticas de la clave 4;
- `.dkk`, en el paso 9.a: `access_type` y `access_material`, claves 4 y 5 → `ERR_ACCESS_INVALID`; después, las extensiones críticas de la clave 7.
En un array de extensiones críticas, primero cualquier extensión desconocida → `ERR_EXTENSION_CRITICAL_UNKNOWN`; solo si todas son conocidas, una con `data` inválida → `ERR_EXTENSION_DATA_INVALID` (§54). En esta capa, las extensiones no críticas nunca producen error.
En un array de extensiones críticas, primero cualquier extensión desconocida, también una conocida que no está registrada para ese objeto y ese array → `ERR_EXTENSION_CRITICAL_UNKNOWN`; solo si todas son conocidas, una con `data` inválida → `ERR_EXTENSION_DATA_INVALID` (§54). En esta capa, las extensiones no críticas nunca producen error.
Entre objetos y entre pasos decide el orden de §63: el primer paso que falla determina el código. Las comprobaciones que relacionan un objeto con otro, o con el perfil resuelto, pertenecen a su paso y no a la capa 4 del objeto: la cota de `round_time` (paso 7, §15), los argumentos del stanza tlock (paso 8), el vínculo de una `.dkk` con su `.dkc` (paso 9.a), la estructura frente a `access_policy` (paso 12) y `header_binding` (paso 15). La presencia de los bytes de `SEALED_CONTROL` se comprueba en el paso 5, después de validar `PUBLIC_HEADER` en el paso 4.
Alcance. La garantía vale para un mismo conjunto de comprobaciones. §63 deja opcionales la inspección previa de los pasos 5, 6 y 8 (SHOULD) y la comprobación de `capsule_digest` en el paso 9.a (§43). Una implementación que omite alguna detecta esos fallos más tarde, o no los detecta, y puede informar antes el código de otro paso; los vectores oficiales y los ejemplos de abajo suponen que se realizan todas. Ninguna otra elección de la implementación puede cambiar el código: ni el orden de sus comprobaciones dentro de una capa, ni decodificar la `.dkk` antes del paso 1 (§63), ni el orden en que prueba las identities del paso 13.
Alcance. La garantía vale para un mismo conjunto de comprobaciones. §63 deja opcionales la inspección previa de los pasos 5, 6 y 8 (SHOULD) y la comprobación de `capsule_digest` en el paso 9.a (§43). Una implementación que omite alguna detecta esos fallos más tarde, o no los detecta, y puede informar antes el código de otro paso; los vectores oficiales y los ejemplos de abajo suponen que se realizan todas. La lectura de `SEALED_CONTROL` en el paso 5 no es opcional: si faltan sus bytes, el código es `ERR_INTEGRITY` en ese paso (§23). Los códigos del paso 10 son los de un release que el llamador suministra directamente, como en los vectores oficiales: una fuente que obtiene releases por red descarta el que no cumple las reglas del paso 10, y si ninguna entrega uno que las cumpla el código es `ERR_RELEASE_UNAVAILABLE`, en el paso 9 (§63). Ninguna otra elección de la implementación puede cambiar el código: ni el orden de sus comprobaciones dentro de una capa, ni decodificar la `.dkk` antes del paso 1 (§63), ni el orden en que prueba las identities del paso 13.
Ejemplos, reproducibles con los vectores oficiales o con los tests de la implementación de referencia (§76):
@ -2092,12 +2116,14 @@ Ejemplos, reproducibles con los vectores oficiales o con los tests de la impleme
| `access_policy` 2 y la DateKey `dk1_x` | `ERR_NON_CANONICAL_CBOR` |
| DateKey `dk1_x` y una extensión crítica desconocida | `ERR_DATEKEY_INVALID` |
| Extensión crítica con `data` inválida seguida, en el array, de otra desconocida | `ERR_EXTENSION_CRITICAL_UNKNOWN` |
| Extensión crítica con `data` inválida seguida, en el array, de una conocida registrada solo como no crítica | `ERR_EXTENSION_CRITICAL_UNKNOWN` |
| Provider Profile con un `provider` inválido y un `chain_hash` que no corresponde | `ERR_UNKNOWN_PROFILE` |
| `.dkk` de otra cápsula con una extensión crítica desconocida | `ERR_EXTENSION_CRITICAL_UNKNOWN`, paso 9 |
| Cápsula `time_and_key` con `FLAGS` 1 y una `.dkk` sin magic `DKK1` | `ERR_INVALID_FLAGS`, paso 2 |
| Cápsula `time_only` válida y una `.dkk` sin magic `DKK1` | se abre: la `.dkk` no interviene |
| Cápsula `time_and_key` sin credenciales y con el reloj antes de `round_time` | `ERR_ACCESS_REQUIRED`, paso 9 |
| Release de otra ronda con una firma válida para esa ronda | `ERR_ROUND_MISMATCH`, paso 10 |
| Release de otra ronda con una firma válida para esa ronda, suministrado directamente | `ERR_ROUND_MISMATCH`, paso 10 |
| El mismo release, única respuesta de un relay | `ERR_RELEASE_UNAVAILABLE`, paso 9 |
| Firma del release negada y U del stanza tlock con c0 + p | `ERR_RELEASE_INVALID`, paso 10 |
| Dos identities: una desenvuelve un stanza de `INNER_ACCESS_AGE` y la otra dos | `ERR_POLICY_STRUCTURE_MISMATCH`, paso 13 |
| `CONTROL_CBOR` con una extensión crítica desconocida y el `header_binding` de otra cabecera | `ERR_EXTENSION_CRITICAL_UNKNOWN`, paso 14 |
@ -2142,6 +2168,7 @@ Cada extensión registrada, identificada por `(extension_id, extension_version)`
- la codificación de su `data`, o que no lleva `data`;
- su forma canónica;
- su longitud máxima, que no puede superar la trama del objeto que la contiene (§57);
- los objetos (`PUBLIC_HEADER`, `CONTROL_CBOR`, `.dkk`) y el array (crítico, no crítico o ambos) en que puede aparecer; en otro objeto o array se trata como desconocida (§54);
- sus vectores de prueba.
Si la codificación de `data` es CBOR:
@ -2157,7 +2184,7 @@ Ubicación según el modelo de confianza (§55.1):
- una extensión que lleva afirmaciones relevantes para la seguridad —aquellas de las que depende una decisión de seguridad de quien abre la cápsula: autorización, identidad, integridad o fecha— MUST registrarse en `CONTROL_CBOR` o estar firmada por una extensión de firma; ni siquiera en `CONTROL_CBOR` prueba autoría (§36.1);
- sin una extensión de firma que la cubra, la `data` de las extensiones de una `.dkk` es solo informativa para quien la posee: una implementación MUST NOT basar en ella una decisión de seguridad sobre la cápsula.
Nota: la `data` de `PUBLIC_HEADER` es pública. El protocolo base no la vincula al control hasta que se verifica `header_binding` (§63, paso 15), y esa verificación aporta coherencia interna, no autoría (§36.1): solo una extensión de firma puede aportar autenticidad del creador (§27). La de `.dkk` viaja en claro y `header_binding` no la cubre, así que el protocolo base no la autentica en ningún paso.
Nota: la `data` de `PUBLIC_HEADER` es pública. El protocolo base no la vincula al control hasta que se verifica `header_binding` (§63, paso 15), y esa verificación aporta coherencia interna, no autoría: solo una extensión de firma puede aportar autenticidad del creador (§36.1, §55.1). La de `.dkk` viaja en claro y `header_binding` no la cubre, así que el protocolo base no la autentica en ningún paso.
---
@ -2246,6 +2273,19 @@ BLS12-381 points
tlock stanza body
= U || V || W, 128 bytes en Quicknet (§63 paso 11)
GT en H2
= orden de kilic/kyber: Fp12 c1 || c0, Fp6 c2 || c1 || c0,
Fp2 c1 || c0, cada Fp en 48 bytes big-endian (§63 paso 11)
release sources
= una fuente de red verifica cada respuesta; sin release válido,
ERR_RELEASE_UNAVAILABLE en el paso 9; los códigos del paso 10,
para un release suministrado directamente
extension placement
= una extensión conocida fuera de los objetos y arrays de su
registro cuenta allí como desconocida (§54, §72)
recovery
= puede obtener release directamente del provider
@ -2318,9 +2358,9 @@ Nuevas ideas, preferencias editoriales o posibilidades futuras que no estén res
Esta política no impide correcciones editoriales que no alteren la semántica normativa.
### Cambio normativo v0.8.2: extensiones
### Cambios normativos de la v0.8.2
La v0.8.2 cierra el formato de las extensiones (§74) en un único cambio normativo:
La v0.8.2 cierra el formato de las extensiones (§74) en un único cambio normativo, que antes de publicarse completan los refinamientos, la enmienda y las correcciones de los bloques siguientes:
- `data` (clave 2) pasa de `any` a `bstr` no vacío y opaco, sin más límite de longitud que la trama de su contenedor; el protocolo base nunca decodifica ni valida su contenido (§31, §54, §57, §58.1);
- cada array admite como máximo 64 extensiones (§31, §54);
@ -2345,24 +2385,24 @@ Las versiones de framing y de schema (clave 1) no cambian. Un objeto v0.8.1 deja
#### Refinamientos de la v0.8.2
La v0.8.2 no se ha publicado todavía, así que estos refinamientos la modifican sin cambiar de versión. Ninguno cambia la codificación de un objeto válido ni el veredicto de un vector o fixture oficial existente: pasan a texto normativo reglas que solo estaban en la implementación de referencia o en `testdata/README.md`, y fijan el código cuando fallan varias reglas a la vez. `dk1.json` gana tres vectores, que fijan reglas de lectura de §19. Proceden de la implementación de referencia, de los vectores oficiales y de la revisión de una segunda implementación independiente, que encontró en `testdata/README.md` reglas que la especificación no fijaba.
La v0.8.2 no se ha publicado todavía, así que estos refinamientos la modifican sin cambiar de versión. Ninguno cambia la codificación de un objeto válido ni el veredicto de un vector o fixture oficial existente: pasan a texto normativo reglas que solo estaban en la implementación de referencia o en `testdata/README.md`, y fijan el código cuando fallan varias reglas a la vez. `dk1.json` gana cuatro vectores, que fijan reglas de lectura de §19. Proceden de la implementación de referencia, de los vectores oficiales y de la revisión de una segunda implementación independiente, que encontró en `testdata/README.md` reglas que la especificación no fijaba.
1. **Precedencia de errores por capas** (§57, §63, §69.1). Casos: los vectores de `cbor.json` «type tag of PUBLIC_HEADER and schema version 2: the type tag is checked first» (`ERR_NON_CANONICAL_CBOR`) y «schema version 2 and an unknown key 11: the version is read first» (`ERR_UNSUPPORTED_VERSION`); una `PUBLIC_HEADER` con `access_policy` 2 y la DateKey `dk1_x`, que la implementación de referencia informaba como `ERR_DATEKEY_INVALID` con su antigua librería CBOR y como `ERR_NON_CANONICAL_CBOR` con su codec propio, sin que la especificación decidiera entre ambos; y una `.dkk` de otra cápsula con una extensión crítica desconocida, que el paso 9 de la referencia informaba como `ERR_ACCESS_INVALID` porque comprobaba `capsule_id` antes que la `.dkk` como objeto, y que ahora es `ERR_EXTENSION_CRITICAL_UNKNOWN`. Las reglas con código propio de §57 no pertenecen a la capa 3: un `dk1_` con padding en `PUBLIC_HEADER` es `ERR_DATEKEY_NON_CANONICAL`, no `ERR_NON_CANONICAL_CBOR`, aunque rompa la expresión regular de `compact-datekey`. Solo los pasos opcionales —5, 6 y 8, y la comprobación de `capsule_digest`— pueden cambiar el código: la referencia comprueba el digest solo con un `.dkc` que puede releer, y la mutación «capsule_digest of the .dkk does not match» supone que se comprueba. Los errores de una `.dkk` se informan en el paso 9.a aunque se decodifique antes: la CLI de referencia la decodificaba antes del paso 1, así que `datekeys decrypt -in time_only.dkc -dkk` con una `.dkk` de `access_type` `mlkem768` fallaba con `ERR_ACCESS_INVALID`, mientras `capsule.Open` abría la misma cápsula con la misma credencial.
1. **Precedencia de errores por capas** (§57, §63, §69.1). Casos: los vectores de `cbor.json` «type tag of PUBLIC_HEADER and schema version 2: the type tag is checked first» (`ERR_NON_CANONICAL_CBOR`) y «schema version 2 and an unknown key 11: the version is read first» (`ERR_UNSUPPORTED_VERSION`); una `PUBLIC_HEADER` con `access_policy` 2 y la DateKey `dk1_x`, que la implementación de referencia informaba como `ERR_DATEKEY_INVALID` con su antigua librería CBOR y como `ERR_NON_CANONICAL_CBOR` con su codec propio, sin que la especificación decidiera entre ambos; y una `.dkk` de otra cápsula con una extensión crítica desconocida, que el paso 9 de la referencia informaba como `ERR_ACCESS_INVALID` porque comprobaba `capsule_id` antes que la `.dkk` como objeto, y que ahora es `ERR_EXTENSION_CRITICAL_UNKNOWN`. Las reglas con código propio de §57 no pertenecen a la capa 3: un `dk1_` con padding en `PUBLIC_HEADER` es `ERR_DATEKEY_NON_CANONICAL`, no `ERR_NON_CANONICAL_CBOR`, aunque rompa la expresión regular de `compact-datekey`. Las comprobaciones opcionales —la inspección de los pasos 5, 6 y 8, y la de `capsule_digest`— pueden cambiar el código: la referencia comprueba el digest solo con un `.dkc` que puede releer, y la mutación «capsule_digest of the .dkk does not match» supone que se comprueba. No eran lo único que podía cambiarlo: el código de un release inválido obtenido por red dependía también de si su fuente lo verificaba, hasta que lo fijaron las correcciones de la revisión formal (abajo). Los errores de una `.dkk` se informan en el paso 9.a aunque se decodifique antes: la CLI de referencia la decodificaba antes del paso 1, así que `datekeys decrypt -in time_only.dkc -dkk` con una `.dkk` de `access_type` `mlkem768` fallaba con `ERR_ACCESS_INVALID`, mientras `capsule.Open` abría la misma cápsula con la misma credencial.
2. **Modelo de confianza** (§55.1, §72). Caso: con los bytes públicos de `time_only.dkc` cualquiera construye otra cápsula con el mismo PRELUDE, la misma `PUBLIC_HEADER` y otro plaintext, que supera los 18 pasos; y otra `data` en la extensión de `time_and_key_portable_extension.dkk` abre la cápsula igual. La especificación solo lo decía de `time_only` (§36.1) y de la `data` de las extensiones (§72), sin una regla sobre dónde registrar afirmaciones de seguridad.
3. **Orden de `extension_id`** (§31, §54). Caso: el vector de `cbor.json` con U+FF61 y U+10000 en el orden de UTF-16 (`ERR_NON_CANONICAL_CBOR`); §31 solo pedía al encoder «ordenar por bytes UTF-8», sin definir la comparación ni lo que hace el decoder. Además, `extension_id` tiene al menos 1 byte: el vector «empty extension_id» de `cbor.json` (`ERR_NON_CANONICAL_CBOR`) dependía de ese mínimo, que §74 atribuía a §31 y que §31 no decía.
3. **Orden de `extension_id`** (§31, §54). Caso: el vector de `cbor.json` con U+FF61 y U+10000 en el orden de UTF-16 (`ERR_NON_CANONICAL_CBOR`); §31 solo pedía al encoder «ordenar por bytes UTF-8», sin definir la comparación ni lo que hace el decoder. Además, `extension_id` tiene al menos 1 byte: el vector «empty extension_id» de `cbor.json` (`ERR_NON_CANONICAL_CBOR`) dependía de ese mínimo, que §31 no decía: el CDDL lo incluía en su bloque de límites de la implementación de referencia (`extension-id = tstr .size (1..256)`), y la tabla de límites de `testdata/README.md` lo daba como rango normativo («1 byte or more») sin citar ninguna sección.
4. **Huecos que la especificación dejaba a la implementación**, con el comportamiento que ya tenía la referencia salvo donde se indica:
1. cabeceras `age` mal formadas, incluida una sin stanzas, que la gramática de C2SP excluye (`1*stanza`) (§28.1, §36): 10 casos de `inspect_differential.json` fallan por una cabecera sin stanzas con `ERR_INTEGRITY`, 5 en el paso 5 y 5 en el paso 6;
2. cota de `round_time` en 9999-12-31T23:59:59Z y rechazo de los instantes anteriores a `genesis_time` (§15): los vectores «after the last representable round» y «genesis - 1s: before the profile» de `quicknet_rounds.json`, «last Quicknet round» de `dk1.json` y 6 casos del paso 7 de `inspect_differential.json`;
3. reglas de lectura de `dk1_` (§19): de ellas dependen «padded Base64URL», «non-zero trailing bits», «fraction notation», «duplicate key» y «byte order mark» en `dk1.json`; esta última dependía de que el parser JSON de la referencia rechaza el BOM, que RFC 8259 permite ignorar. La referencia aceptaba CR y LF dentro del Base64, porque los decodificadores de Go los omiten, e informaba `ERR_DATEKEY_NON_CANONICAL`; ahora falla el paso 1 con `ERR_DATEKEY_INVALID`, como en una implementación que sigue el texto, en la que la revisión diferencial de la segunda implementación encontró 799 discrepancias de esta clase en 20 000 casos. Un número JSON se lee por su valor decimal exacto: con un double, `1.0000000000000001` sería 1. Vectores nuevos de `dk1.json`: «line feed inside the Base64», «carriage return and line feed after the Base64» y «version 1.0000000000000001: its exact value, not a double»;
3. reglas de lectura de `dk1_` (§19): de ellas dependen «padded Base64URL», «non-zero trailing bits», «fraction notation», «duplicate key» y «byte order mark» en `dk1.json`; esta última dependía de que el parser JSON de la referencia rechaza el BOM, que RFC 8259 permite ignorar. La referencia aceptaba CR y LF dentro del Base64, porque los decodificadores de Go los omiten, e informaba `ERR_DATEKEY_NON_CANONICAL`; ahora falla el paso 1 con `ERR_DATEKEY_INVALID`, como en una implementación que sigue el texto, en la que la revisión diferencial de la segunda implementación encontró 799 discrepancias de esta clase en 20 000 casos. Un número JSON se lee por su valor decimal exacto: con un double, `1.0000000000000001` sería 1. Vectores nuevos de `dk1.json`: «line feed inside the Base64», «carriage return and line feed after the Base64», «version 1.0000000000000001: its exact value, not a double» e «invalid UTF-8 in a member a repeated name overwrites». Este último lo encontró el diferencial de la segunda implementación: `encoding/json` sustituía el UTF-8 inválido por U+FFFD, así que un miembro que un nombre repetido sobrescribe superaba los pasos 2 y 3, y la referencia informaba `ERR_DATEKEY_NON_CANONICAL` en el paso 6; §19 lo hace fallar en el paso 2, con `ERR_DATEKEY_INVALID`;
4. cotas inferiores de las longitudes (§22, §23, §40, §57): un caso de `inspect_differential.json` con `PUBLIC_HEADER_LEN` 0 da `ERR_INTEGRITY` en el paso 2, y otros tres con `FLAGS` distinto de 0 dan `ERR_INVALID_FLAGS`; `BODY_LEN` 0 en una `.dkk`, que la referencia informaba como `ERR_NON_CANONICAL_CBOR` al decodificar un cuerpo vacío, pasa a `ERR_INTEGRITY`, como en §22;
5. comparación de los argumentos del stanza tlock (§35, §63 paso 8): la mutación «tlock round edited by a third party» y los 69 casos del paso 8 de `inspect_differential.json`; `01000`, `+1000` o un chain hash en mayúsculas no coinciden;
6. validación del Provider Profile y fórmula de `chain_hash` (§12.1): el bloque `provider_profile` de `cbor.json`, con «network default, left out of the chain hash» y «genesis_time 253402300799, 9999-12-31T23:59:59Z». Los alfabetos de los nombres son normativos, porque el JSON canónico de `dk1_` no define escapes (§18): de ellos dependen «invalid profile_id» en `cbor.json`, «uppercase network» en `dk1.json` y las mutaciones 46, 746, 1138 y 1754 de `inspect_differential.json` (`ERR_DATEKEY_INVALID` en el paso 4); sus longitudes siguen siendo límites de implementación. `period` es como mucho 2³² − 1 para pinnear el perfil: drand escribe `uint32(period)` en el hash de la información de cadena, y para un valor mayor el hash no está definido; ningún vector lo alcanza, porque el límite de 86400 segundos de la referencia falla antes. Al pinnear un perfil, la referencia aplicaba las reglas de campo antes que el schema: un `period` de 86401 segundos era `ERR_UNKNOWN_PROFILE` en `profile.NewRegistry` y `ERR_NON_CANONICAL_CBOR` en `profile.Decode`; ahora los dos dan el segundo. Un punto de G1 que está en la curva pero fuera del subgrupo de orden primo es `ERR_UNKNOWN_PROFILE`;
7. límites de implementación (§74): el vector «period of one day and one second, above the implementation limit» de `cbor.json`; en `INNER_ACCESS_AGE`, una cabecera que supera los del parser es `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12, como toda cabecera mal formada ahí (§28.1);
8. credenciales y reloj en el paso 9 (§63): las mutaciones «time_and_key without credentials» (`ERR_ACCESS_REQUIRED`, paso 9, sin red), «round not reached yet» (`ERR_RELEASE_UNAVAILABLE`, paso 9, sin red) y «access_policy=time_only with time_and_key structure», que ofrece una `.dkk` que no interviene y falla en el paso 12. Sin credenciales, `ERR_ACCESS_REQUIRED` va antes que el reloj; la referencia contaba una identity nula como credencial y, con el reloj antes de `round_time`, informaba `ERR_RELEASE_UNAVAILABLE`;
8. credenciales y reloj en el paso 9 (§63): las mutaciones «time_and_key without credentials» (`ERR_ACCESS_REQUIRED`, paso 9, sin red), «round not reached yet» (`ERR_RELEASE_UNAVAILABLE`, paso 9, sin red) y «access_policy=time_only with time_and_key structure», que ofrece una `.dkk` que no interviene y falla en el paso 12 por la regla de §36 para `time_only`, que tampoco estaba escrita: un resultado que empieza por la línea de versión de `age` es `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12, y cualquier otro se decodifica como `CONTROL_CBOR` en el paso 14. Sin credenciales, `ERR_ACCESS_REQUIRED` va antes que el reloj; la referencia contaba una identity nula como credencial y, con el reloj antes de `round_time`, informaba `ERR_RELEASE_UNAVAILABLE`;
9. verificación del release en el paso 10 (§17, §51, §63): las mutaciones de §64 «DateKey A + release of round B» (`ERR_ROUND_MISMATCH`, con una firma válida de la ronda 1001) y «release of another round» (`ERR_RELEASE_INVALID`, con la firma de la ronda 1001 presentada como de la ronda 1000) dependen de comparar la ronda antes que la firma, algo que solo decía `testdata/README.md`;
10. códigos de las identities en los pasos 11, 13 y 17 (§28.1, §36, §63): §28.1 remitía a unos «códigos propios de cada identity» que §63 no definía. Casos: las mutaciones «identity that is not a recipient» (`ERR_ACCESS_INVALID`, paso 13), «two INNER_ACCESS_AGE stanzas for one recipient» (`ERR_POLICY_STRUCTURE_MISMATCH`, paso 13) y «SEALED_CONTROL_A + PAYLOAD_AGE_B» (`ERR_INTEGRITY`, paso 17); y el share efímero repetido en `INNER_ACCESS_AGE`, que la referencia rechazaba en el paso 12 sin regla en §36. La referencia probaba las identities en orden y aceptaba el fichero con la primera que desenvolvía un stanza, aunque otra desenvolviera dos; ahora es `ERR_POLICY_STRUCTURE_MISMATCH` en cualquier orden.
10. códigos de las identities en los pasos 11, 13 y 17 (§28.1, §36, §63): §27 y esos pasos exigían que la identity rechazara el fichero sin asignar código, y los códigos solo estaban en los vectores de `mutations.json` y, para una identity que no es recipient (`ERR_ACCESS_INVALID`, paso 13), en `testdata/README.md`. Casos: las mutaciones «identity that is not a recipient» (`ERR_ACCESS_INVALID`, paso 13), «two INNER_ACCESS_AGE stanzas for one recipient» (`ERR_POLICY_STRUCTURE_MISMATCH`, paso 13) y «SEALED_CONTROL_A + PAYLOAD_AGE_B» (`ERR_INTEGRITY`, paso 17); y el share efímero repetido en `INNER_ACCESS_AGE`, que la referencia rechazaba en el paso 12 sin regla en §36. La referencia probaba las identities en orden y aceptaba el fichero con la primera que desenvolvía un stanza, aunque otra desenvolviera dos; ahora es `ERR_POLICY_STRUCTURE_MISMATCH` en cualquier orden.
Estos refinamientos cambian el código de la implementación de referencia en entradas que ningún vector oficial existente recoge: una `.dkk` con fallos a la vez en el objeto y en su vínculo con la cápsula, y una `.dkk` que la CLI no puede decodificar, ahora en el paso 9.a (punto 1); CR o LF en un `dk1_` (punto 4.3); una `.dkk` con `BODY_LEN` 0 (punto 4.4); los códigos de `profile.NewRegistry` (punto 4.6); una identity nula (punto 4.8); y dos identities de las que una desenvuelve dos stanzas (punto 4.10). Reproducen cada caso los tests `capsule.TestPrecedenceWithinPublicHeader`, `TestPrecedenceAcrossSteps`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestControlCriticalBeforeHeaderBinding`, `TestFrameLengthLowerBounds`, `TestMalformedAgeHeaders`, `TestTlockStanzaArgumentComparison` y `TestTrustModel`; `accesskey.TestDecodePrecedence`; `agewrap.TestAccessIdentityStrictness` y `TestMalformedX25519Stanzas`; `datekey.TestReadingRules`; `profile.TestDecodePrecedence`, `TestChainHashFormula` y `TestPinPathMatchesDecode`; `provider.TestVerifyRejects`; `extension.TestOrderIsUnsignedBytewise`; y `cmd/datekeys.TestDecryptAccessKeyOrder`.
Estos refinamientos cambian el código de la implementación de referencia en entradas que ningún vector oficial existente recoge: una `.dkk` con fallos a la vez en el objeto y en su vínculo con la cápsula, y una `.dkk` que la CLI no puede decodificar, ahora en el paso 9.a (punto 1); CR o LF en un `dk1_`, y UTF-8 inválido en un miembro que un nombre repetido sobrescribe (punto 4.3); una `.dkk` con `BODY_LEN` 0 (punto 4.4); los códigos de `profile.NewRegistry` (punto 4.6); una identity nula (punto 4.8); y dos identities de las que una desenvuelve dos stanzas (punto 4.10). Reproducen cada caso los tests `capsule.TestPrecedenceWithinPublicHeader`, `TestPrecedenceAcrossSteps`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestControlCriticalBeforeHeaderBinding`, `TestFrameLengthLowerBounds`, `TestMalformedAgeHeaders`, `TestTlockStanzaArgumentComparison` y `TestTrustModel`; `accesskey.TestDecodePrecedence`; `agewrap.TestAccessIdentityStrictness` y `TestMalformedX25519Stanzas`; `datekey.TestReadingRules`; `profile.TestDecodePrecedence`, `TestChainHashFormula` y `TestPinPathMatchesDecode`; `provider.TestVerifyRejects`; `extension.TestOrderIsUnsignedBytewise`; y `cmd/datekeys.TestDecryptAccessKeyOrder`.
#### Enmienda de la v0.8.2: canonicidad de puntos
@ -2378,6 +2418,18 @@ Caso reproducible que la justifica, de una segunda implementación independiente
La implementación de referencia ya rechazaba todas esas entradas con el código y el paso que fija el texto: ningún código cambia. Solo cambia dónde rechaza un U en el infinito, antes de descifrar en lugar de en la comprobación r·G == U, con el mismo `ERR_INTEGRITY`. Ningún fixture ni vector existente cambia de bytes ni de veredicto: `mutations.json` gana los diez casos de §64, en los que la cabecera `age` de los U y cuerpos editados conserva un MAC válido. La firma con x + p usa la ronda 1004 de Quicknet, la primera después de la 1000 cuya firma lo permite: ninguna de las rondas de los fixtures (1000, 1001 y 2000) tiene una x menor que 2³⁸¹ − p. Reproducen cada caso los tests `capsule.TestExportedMutationCorpus` y `TestPointMutationsChangeOnlyTheEncoding`; `profile.TestDrandPointDecodersAreCanonical` y `TestPublicKeyEncodingIsCanonical`; `provider.TestVerifyRejects`; y `agewrap.TestTimeIdentityStrictness` y `TestTimeIdentityRelease`.
#### Correcciones de la revisión formal
La v0.8.2 sigue sin publicarse, así que estas correcciones también la modifican sin cambiar de versión. Proceden de la revisión formal e independiente de esta especificación, una de las fuentes que admite esta política («revisión criptográfica o técnica externa»). Tres de sus observaciones cambian el texto normativo:
1. **Release inválido obtenido por red** (§63 pasos 9 y 10, §69.1). Una fuente que obtiene releases por red MUST verificar cada respuesta con las reglas del paso 10 y descartar la que no las cumple; si ninguna entrega un release que las cumpla, el código es `ERR_RELEASE_UNAVAILABLE`, en el paso 9. Los códigos del paso 10 son los de un release que el llamador suministra directamente, como en los vectores oficiales. Caso: el texto no decía dónde verifica una fuente de red, así que un release de otra ronda firmado para esa ronda, única respuesta de un relay, daba `ERR_RELEASE_UNAVAILABLE` en el paso 9 con la referencia, cuyo cliente drand descarta toda respuesta que no verifica, y `ERR_ROUND_MISMATCH` en el paso 10 con una fuente que lo devolviera sin verificar, aunque §69.1 afirmaba que, fuera de las comprobaciones opcionales, ninguna elección de la implementación podía cambiar el código.
2. **Objetos y arrays de cada extensión** (§31, §54, §69.1, §72). Cada extensión registrada MUST declarar los objetos y el array en que puede aparecer, y una extensión conocida que aparece en un objeto o array para el que no está registrada se trata allí como desconocida. Caso: §72 exige registrar en `CONTROL_CBOR` una extensión con afirmaciones relevantes para la seguridad, pero un lector que la conocía la aceptaba en cualquier objeto: copiada en las extensiones críticas de `PUBLIC_HEADER`, que cualquiera puede escribir (§55.1), o de una `.dkk`, superaba el paso 4 o el 9.
3. **Serialización de GT en H2** (§63 paso 11, §77). H2, H3 y H4 son las de `encrypt/ibe` de drand/kyber, y H2 resume el elemento de GT en el orden de kilic/bls12-381, c1 antes que c0 en cada nivel de la torre, con el vector H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`, nuevo en `testdata/vectors/tlock_ibe.json`. Caso: el paso 11 citaba H2 sin fijar la serialización, y en el orden de `Fp12.toBytes` de noble, c0 antes que c1, H2(e(G1, G2)) es `0118eea9d5971745f71e3c94926f1717`: una implementación sobre esa librería que siguiera el texto derivaría otra `FK_TIME` y no abriría ninguna cápsula.
Las demás observaciones no cambian ninguna regla ni ningún código: §27 remite la autenticidad de `PUBLIC_HEADER` al modelo de confianza (§55.1) y precisa que el MAC de `age` solo protege frente a quien no conoce la file key; el paso 5 separa la lectura de `SEALED_CONTROL`, obligatoria como ya fijaban §23 y §69.1, de la inspección de su cabecera; el paso 15 nombra su código, `ERR_HEADER_BINDING`; §21 hace MUST los 16 bytes de `capsule_id` que ya exigían §24, `datekeys.cddl` y §57; §77 gana RFC 8259, RFC 8610 y RFC 4648; y hay correcciones de redacción en §5, §36, §55.1, el paso 13, la capa 1 de §69.1 y §72. En este registro, los refinamientos cuentan cuatro vectores nuevos de `dk1.json`, no tres, con el de UTF-8 inválido (punto 4.3); el punto 4.8 recoge la regla de §36 para `time_only`; el punto 1 ya no dice que solo las comprobaciones opcionales pueden cambiar el código; y los casos 3 y 4.10 citan los textos que existían, en lugar de otros que ninguna versión anterior contenía.
La implementación de referencia ya seguía las correcciones 1 y 3: ningún código cambia. Para la 2 gana la interfaz opcional `extension.Placement`, que consultan las comprobaciones de extensiones de los pasos 4, 9.a y 14; un registro que no la implementa conoce sus extensiones en todos los objetos y arrays, como antes. Ningún fixture ni vector existente cambia. Reproducen cada caso los tests `capsule.TestReleaseFromANetworkSource` y `TestExtensionPlacement`; `extension.TestPlacement`; y `agewrap.TestTlockH2Vector`.
---
## 77. Referencias
@ -2388,6 +2440,9 @@ La implementación de referencia ya rechazaba todas esas entradas con el código
- drand/tlock
https://github.com/drand/tlock
- drand/kyber — paquete `encrypt/ibe`, que tlock importa: H2, H3 y H4 del IBE-CCA; el pairing y la serialización de GT son los de drand/kyber-bls12381, sobre kilic/bls12-381
https://github.com/drand/kyber
- age specification — C2SP
https://github.com/C2SP/C2SP/blob/main/age.md
@ -2396,6 +2451,12 @@ La implementación de referencia ya rechazaba todas esas entradas con el código
- RFC 8949 — CBOR
- RFC 8610 — CDDL
- RFC 8259 — JSON
- RFC 4648 — Base64 y Base64URL
- RFC 2119 / RFC 8174 — normative terminology
---

@ -2,12 +2,15 @@
- `DateKeys_Protocol_Specification_v0.8.2.md`: frozen copy of the normative
draft v0.8.2 (26 September 2026) implemented by this module. SHA-256:
`e6e59490284e0efe112704931b6d5997fca60e38b866afc17b7bacdcf395df03`.
`96edce44f12debeb456a1101d2c68dfc5946e1b180b01c5bbbb14b809cddb140`.
v0.8.2 replaces v0.8.1 with one normative change to extensions, refined
before release (error precedence, trust model, extension order, and rules
the reference had applied without normative text) and amended (the
canonical encoding of BLS12-381 points and the tlock stanza body), all
recorded with their reproducible cases in the specification's §76.
the reference had applied without normative text), amended (the canonical
encoding of BLS12-381 points and the tlock stanza body) and corrected after
a formal review (an invalid release from a network source, the objects and
arrays where each extension may appear, the serialization of GT in the
tlock H2), all recorded with their reproducible cases in the
specification's §76.
- `datekeys.cddl`: the CBOR schemas of the specification as implemented, with
the encoding rules CDDL cannot express.

42
testdata/README.md vendored

@ -32,6 +32,7 @@ Conventions for every file:
| `vectors/quicknet_rounds.json` | date → round resolution | §15, §16, §65 |
| `vectors/dk1.json` | canonical `dk1_` strings, and rejected encodings with their code | §18, §19, §66 |
| `vectors/cbor.json` | the CBOR profile, and one block of vectors per schema | §58, CDDL |
| `vectors/tlock_ibe.json` | H2 of the tlock IBE: the serialization of an element of GT | §63 step 11 |
| `vectors/mutations.json` | the mutation corpus: 33 mutations of §64 and further cases | §63, §64 |
| `vectors/inspect_differential.json` | 1825 mutations of the fixtures with the verdict of steps 1 to 8 | §63 |
| `fixtures/<name>.dkc`, `<name>.json` | official capsules and every intermediate value | §67 |
@ -151,6 +152,39 @@ implementation without the limit accepts them. The name alphabets, the
validation are not limits: they are rules of spec §12.1. Neither is the
minimum of one byte of `extension_id` (spec §31).
## `vectors/tlock_ibe.json`
H2, the hash of an element of GT in the IBE-CCA with which tlock wraps
FK_TIME: spec §63 step 11, and the paragraph "Serialización de GT en H2" after
the flow.
```json
{
"spec": "0.8.2",
"description": "…",
"vectors": [
{ "name": "H2(e(G1, G2)), the generators of G1 and G2", "g1": "97f1…", "g2": "93e0…", "gt": "0f41…", "h2": "cb87319f24560b5231579a09ad79f12e" }
]
}
```
- `g1`, `g2`: a point of G1 and one of G2 in the canonical compressed
encoding of spec §12.2, 48 and 96 bytes; the vector uses the generators.
- `gt`: e(g1, g2), the pairing of G1 × G2 of step 11, in 576 bytes. Over the
tower Fp2 = Fp[u]/(u² + 1), Fp6 = Fp2[v]/(v³ − (u + 1)) and
Fp12 = Fp6[w]/(w² − v), every element is written with its coefficients from
the highest degree to the lowest: c1 then c0 for Fp12, c2, c1 and c0 for
Fp6, c1 then c0 for Fp2, and each element of Fp in 48 bytes big-endian. It
is the order of kilic/bls12-381, which drand/kyber and tlock use.
- `h2`: SHA-256 of the ASCII bytes `IBE-H2` followed by `gt`, truncated to
its first 16 bytes, the length of V.
A reader checks that its pairing of `g1` and `g2` serializes to `gt` and that
its H2 of those bytes is `h2`; the vector fixes the pairing and the
serialization at once. The same 576 bytes with the twelve coordinates of Fp in
reverse order, c0 first at every level as `Fp12.toBytes` of `@noble/curves`
writes them, give `0118eea9d5971745f71e3c94926f1717` and another FK_TIME.
## `vectors/mutations.json`
The mutation corpus of spec §64, as frozen data. Each case is a `.dkc`, what
@ -186,7 +220,11 @@ reading flow (`capsule.Open`, §63) must fail.
is asked for. The reader must verify it (§51): a case may serve a release of
another round, a round with the signature of another, or a signature that is
not the canonical encoding of a point (§12.2). `null` means that no release
is available (`ERR_RELEASE_UNAVAILABLE`).
is available (`ERR_RELEASE_UNAVAILABLE`). The release is supplied directly,
as the caller's own, so one that breaks the rules of step 10 gets the code
of step 10; a source that fetched it over a network would have discarded
it, and the code would be `ERR_RELEASE_UNAVAILABLE` at step 9 (spec §63
steps 9 and 10).
- `now`: the reader's clock, RFC 3339. No release is requested before the round
time of the DateKey.
- `registry`: `default` pins exactly the Quicknet profile of
@ -222,6 +260,8 @@ file (steps 11, 13 and 17), are those of spec §63 as well. In this corpus:
- identities are not examined before the release (spec §63 step 13);
- a `release` of `null` is `ERR_RELEASE_UNAVAILABLE` at step 9;
- every `release` is supplied directly, so an invalid one fails at step 10:
the source is not a network source, which would discard it at step 9;
- every `.dkk` offered decodes: the corpus checks step 9.a, not the decoding
of a `.dkk`, whose errors spec §63 also places at step 9.a.

@ -0,0 +1,13 @@
{
"spec": "0.8.2",
"description": "H2 of the IBE-CCA of tlock (spec §63 step 11): SHA-256 of \"IBE-H2\" and the 576 bytes of an element of GT, c1 before c0 at every level of the tower and each coordinate of Fp in 48 bytes big-endian (the order of kilic/bls12-381), truncated to 16 bytes. Generated by the reference implementation with drand/kyber-bls12381, the pairing of tlock.",
"vectors": [
{
"name": "H2(e(G1, G2)), the generators of G1 and G2",
"g1": "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb",
"g2": "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8",
"gt": "0f41e58663bf08cf068672cbd01a7ec73baca4d72ca93544deff686bfd6df543d48eaa24afe47e1efde449383b67663104c581234d086a9902249b64728ffd21a189e87935a954051c7cdba7b3872629a4fafc05066245cb9108f0242d0fe3ef03350f55a7aefcd3c31b4fcb6ce5771cc6a0e9786ab5973320c806ad360829107ba810c5a09ffdd9be2291a0c25a99a211b8b424cd48bf38fcef68083b0b0ec5c81a93b330ee1a677d0d15ff7b984e8978ef48881e32fac91b93b47333e2ba5706fba23eb7c5af0d9f80940ca771b6ffd5857baaf222eb95a7d2809d61bfe02e1bfd1b68ff02f0b8102ae1c2d5d5ab1a19f26337d205fb469cd6bd15c3d5a04dc88784fbb3d0b2dbdea54d43b2b73f2cbb12d58386a8703e0f948226e47ee89d018107154f25a764bd3c79937a45b84546da634b8f6be14a8061e55cceba478b23f7dacaa35c8ca78beae9624045b4b601b2f522473d171391125ba84dc4007cfbf2f8da752f7c74185203fcca589ac719c34dffbbaad8431dad1c1fb597aaa5193502b86edb8857c273fa075a50512937e0794e1e65a7617c90d8bd66065b1fffe51d7a579973b1315021ec3c19934f1368bb445c7c2d209703f239689ce34c0378a68e72a6b3b216da0e22a5031b54ddff57309396b38c881c4c849ec23e87089a1c5b46e5110b86750ec6a532348868a84045483c92b7af5af689452eafabf1a8943e50439f1d59882a98eaa0170f1250ebd871fc0a92a7b2d83168d0d727272d441befa15c503dd8e90ce98db3e7b6d194f60839c508a84305aaca1789b6",
"h2": "cb87319f24560b5231579a09ad79f12e"
}
]
}
Loading…
Cancel
Save

Powered by TurnKey Linux.