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>
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{
namestring
dkc[]byte
ocapsule.OpenOptions
stepint
}{
{"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:=rangeboth{
tc.o.Extensions=r.reg
step,calls,err:=openStep(t,tc.dkc,tc.o)
if!r.placed{
iferr!=nil{
t.Errorf("%s, %s: %v at step %d",tc.name,r.name,err,step)
@ -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`: 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* |
| 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) |
| 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) |
| 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`) |
| 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) |
| 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
| 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 |
| 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.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` |
| 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 |
| 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 |
@ -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)
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:
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
"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",