From 8220a5bcbcc90a3d04cd33c38cb9ba47324c27b2 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 20 Aug 2026 01:44:45 +0100 Subject: [PATCH] docs(specs): define the attestation format and handle normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two places in the suite named a thing without saying what it is. This gives each a concrete definition an implementer can build from, and changes no decision already made. Common §9.1 fixes the attested data as a byte concatenation and the signature over it. The Notary Service signs off chain, where it holds the transcript, and verifies on chain, where it holds none: it derives the verification key from the attested-data-and-signature pair alone and decides whether that key is one it trusts. A caller-supplied digest, preimage hash, or verification key is refused, because a caller-computed digest authenticates whatever the caller hashed. The layout carries formatTag, platformId, operationTag, authorityId, createdAt, the two transcript lengths REQ-COMMON-36 requires, and per direction a list of revealed ranges with their offsets and a list of range commitments with theirs. It carries no Chain ID and no verifier identity: the attested data describes a session, not a destination, and binding it to one verifier would keep a newly registered version from checking attestations made before it existed. It carries no handle, account identifier, client identifier, or chain address either — each is already in the revealed ranges, and a second signed copy can disagree with the bytes it came from. Platform §2.1a replaces the criteria prose with the algorithm: five per-platform parameters, six ordered steps, and four rejection kinds. The published vector table becomes a cross-check on an implementation of that algorithm rather than its source, so a disagreement between the two is a defect in the table. REQ-COMMON-47..61, REQ-PLAT-61..74 and TEST-COMMON-24..26 are new here. Signed-off-by: xgreenx --- specs/ceremony-common.md | 251 +++++++++++++++++++++++++++++++++-- specs/platform-ceremonies.md | 194 +++++++++++++++++++++++---- 2 files changed, 413 insertions(+), 32 deletions(-) diff --git a/specs/ceremony-common.md b/specs/ceremony-common.md index edd8a207..8a302dab 100644 --- a/specs/ceremony-common.md +++ b/specs/ceremony-common.md @@ -117,7 +117,7 @@ Canonical Runtime: The immutable browser release that constructs Notary Service: The role that observes a TLS session and signs the resulting attestation, and that answers whether an attestation is authentic. Its answer is a single accept-or-reject decision covering its own signature - over the data it attested, and it charges the Notary Fee for giving + over the attested data of §9.1, and it charges the Notary Fee for giving one. It holds no transcript when it answers: the attested data carries the transcript lengths, the revealed ranges, and the range commitments inside the bytes it signed, so that signature is what binds them to the @@ -451,7 +451,7 @@ an identity session — so one submission on either path pays two fees. The Consumer MUST call the Proof Verifier with the identity platform, the Platform Verifier Version, the submission, and the native value the quotation of REQ-COMMON-06E returns. That value covers one Notary Fee of - §9.1 for each attestation the selected profile requires, and is zero where + §9.2 for each attestation the selected profile requires, and is zero where its Attestation Count is zero. Necessity: cross-component interoperability of one verification entry point serving every Consumer. - REQ-COMMON-05A: @@ -538,7 +538,7 @@ the Consumer decides whether that domain is its own. disagree with the submitted one because dispatch reads it from the submission in the first place. -The Notary Fees of §9.1 are charged at the bottom of this path, so native +The Notary Fees of §9.2 are charged at the bottom of this path, so native value passes down it and stops where the work is done. A path with no attestation to verify carries no value at all. @@ -841,9 +841,10 @@ and no `authorization` needle to count. derivable from the ranges around it. - REQ-COMMON-36 (upholds SP-EXCHANGE-01): The Notary Service MUST carry the total transcript length of each direction - of the session it observed in the data it signs. The Platform Verifier MUST - take the transcript length used for the coverage check of REQ-COMMON-35 - from those signed lengths and from nothing else. + of the session it observed in the `sentTranscriptLength` and + `recvTranscriptLength` fields of the attested data of §9.1. The Platform + Verifier MUST take the transcript length used for the coverage check of + REQ-COMMON-35 from those two signed fields and from nothing else. Necessity: without a signed length, bytes past the last revealed range are invisible, which is what makes a planted-header request pass every substring-anchored check. @@ -997,7 +998,205 @@ Disclosure makes such bytes auditable but does not constrain their decoded form semantics. Launch therefore retains ASM-PROV-07 as a soundness dependency for every form-encoded token request. -### 9.1 Attestation verification and its fee +### 9.1 Attestation format + +An attestation is a byte string and a signature over it. The Notary Service +signs off chain, where it holds the transcript. It verifies on chain, where +it holds no transcript at all and nothing derived from one beyond what the +attestation itself carries. The verifying side therefore answers one +question, and that question is not whether the revealed bytes match a +transcript: it derives the key that signed the attested data and decides +whether that key is one it currently trusts. Everything tying the attested +data to a session that really happened is carried by the notary key under +ASM-NOTARY-01, not by any comparison performed on the Consumer Chain. The +Notary Fee of §9.2 attaches to that same verification. + +The distinction is what makes the field list below load-bearing. A verifying +side handed a digest its caller computed authenticates a number rather than +a session: whatever the caller hashed is what the signature is checked +against, and the values the Platform Verifier goes on to read are whatever +the caller supplied next to it. Deriving the key from the attested data +itself is what makes these fields the signed fields. + +`U16BE`, `U32BE`, and `U64BE` are the fixed-width unsigned big-endian +encodings of §5. `UTF8` emits the exact UTF-8 bytes of a string. Offsets are +zero-based byte offsets into that direction's complete transcript, `start` +inclusive and `end` exclusive. + +```text +attestedData = + formatTag // 32 bytes + || platformId // 32 bytes + || operationTag // 32 bytes + || authorityId // 32 bytes + || U64BE(createdAt) // 8 bytes + || U32BE(sentTranscriptLength) // 4 bytes + || U32BE(recvTranscriptLength) // 4 bytes + || directionBlock(sent) // variable + || directionBlock(received) // variable + +directionBlock(d) = + U16BE(COUNT(revealedRanges(d))) + || revealedRange(d, 0) || ... || revealedRange(d, n - 1) + || U16BE(COUNT(rangeCommitments(d))) + || rangeCommitment(d, 0) || ... || rangeCommitment(d, m - 1) + +revealedRange(d, i) = + U32BE(start) || U32BE(end) || bytes // exactly end - start bytes + +rangeCommitment(d, j) = + U32BE(start) || U32BE(end) || commitment // 32-byte value + +attestationDigest = keccak256(attestedData) +``` + +The attested data describes the observed session and says nothing about where +the evidence will be spent. It names no Consumer Chain and no Platform +Verifier, and that omission is deliberate. The Authorization Digest of §5 +already commits the chain, and it is bound to the token attestation through +the revealed `code_verifier` of §7, so presenting the same attestation on +another chain would take a preimage of that verifier. Binding an attestation +to one verifier identity would be worse than redundant: a newly registered +Platform Verifier version could not check attestations made before it +existed, which contradicts the concurrent version support REQ-COMMON-05B +requires, and the notary would have to know at notarization time which +verifier the user will later submit to, which it cannot know and which +REQ-COMMON-33 forbids it from deciding. + +- REQ-COMMON-47 (upholds SP-EXCHANGE-01): + The Notary Service MUST build the attested data of every attestation as + exactly the byte concatenation above. The Notary Service MUST sign + `attestationDigest` and no other preimage. Necessity: the verifying side + rebuilds the same bytes from the attestation it is handed, so a field + reordered, omitted, or encoded differently on either side derives a key + nobody trusts and rejects a genuine attestation. +- REQ-COMMON-48 (upholds SP-EXCHANGE-01): + The Notary Service MUST encode each fixed-width field at exactly the width + the layout states, rejecting a value that does not fit its field. The + Notary Service MUST emit every variable-length part behind the count and + offset fields the layout places before it. Necessity: every boundary in + the byte string is then derivable from bytes that precede it, so two + different attestations cannot share one preimage by shifting a boundary. +- REQ-COMMON-49 (upholds SP-EXCHANGE-01): + The Notary Service MUST take one attested-data-and-signature pair and + derive the verification key from that pair alone. The Notary Service + MUST NOT accept a digest, a preimage hash, or a verification key supplied + by its caller. Necessity: a caller-computed digest authenticates whatever + the caller hashed, which need not be the attested data the Platform + Verifier goes on to read. +- REQ-COMMON-50 (upholds SP-EXCHANGE-01): + The Notary Service MUST return exactly one accept-or-reject decision for + that pair. The Notary Service MUST accept only when the derived key is one + of the notary keys it currently holds as trusted. +- REQ-COMMON-51 (upholds SP-EXCHANGE-01): + The Notary Service MUST NOT compare the attested data against a + transcript. Its answer covers the signature and the trust status of the + derived key, and nothing else; whether the ranges are the ones a profile + expects, and what their bytes must contain, belong to the Platform + Verifier. Necessity: the verifying side holds no transcript, so a + requirement to compare against one is not implementable and would be read + as a guarantee nothing provides. +- REQ-COMMON-52 (upholds SP-BIND-01, SP-EXCHANGE-01): + Where the attested data the Notary Service accepted carries a value, the + Platform Verifier MUST read that value out of the attested data. The + Platform Verifier MUST reject a submission carrying a caller-supplied + duplicate of a value the attested data already carries. The Platform + Verifier MAY act on a caller-supplied value the attested data does not + carry, and only where it authenticates that value itself against evidence + the caller does not control: `pkceNonce` by the verifier recomputation of + REQ-COMMON-15A, and Google `aud` bytes by the audience comparison its + Platform Profile fixes in REQ-PLAT-19A. Necessity: a duplicate read beside + the signed copy is caller-controlled, and a caller who can choose it can + retarget an otherwise genuine attestation; a value the verifier + authenticates itself is not that case, and a profile whose Attestation + Count is zero carries no attested data at all for such a rule to reach. +- REQ-COMMON-53 (upholds SP-EXCHANGE-01): + The Notary Service MUST derive `formatTag` as the keccak256 of a + libID-namespaced ASCII string naming this attestation format and its + version. The Platform Verifier MUST reject an attestation whose + `formatTag` differs from the one its Platform Profile pins. A change to + the field list, to a field's width, or to a field's meaning takes a new + version string rather than another field. +- REQ-COMMON-54: Withdrawn. The attested data binds no Consumer Chain and no + Platform Verifier, for the reason stated above these requirements. +- REQ-COMMON-55 (upholds SP-EXCHANGE-01): + The Notary Service MUST set `platformId` to the keccak256 of the `UTF8` + bytes of the identity-platform name, and `operationTag` to the keccak256 + of a libID-namespaced ASCII string naming which session of the ceremony + this attestation covers. The Platform Verifier MUST reject an attestation + whose `platformId` or `operationTag` differs from the constant its profile + pins for the session it is checking. Necessity: one ceremony notarizes more + than one session, and two attestations that differ only in which session + they came from are otherwise interchangeable. +- REQ-COMMON-56 (upholds SP-BIND-01): + The Notary Service MUST set `authorityId` to the keccak256 of the + authority it authenticated under REQ-COMMON-21, in the canonical byte form + of §9. The Platform Verifier MUST compare that field with its pinned + authority constant under REQ-COMMON-21A. Necessity: `platformId` names the + ceremony the attestation belongs to, while the authority names the host + that actually answered, and one identity platform serves a ceremony from + more than one host. +- REQ-COMMON-57 (upholds SP-FRESH-01): + The Notary Service MUST set `createdAt` to its own clock reading when the + session it observed completed. The Notary Service MUST NOT take that value + from the prover, from a response header, or from any other party. +- REQ-COMMON-58 (upholds SP-EXCHANGE-01): + The Notary Service MUST set `sentTranscriptLength` and + `recvTranscriptLength` to the total byte count of the sent and received + directions of the session it observed. The Platform Verifier MUST take the + signed transcript length REQ-COMMON-35 and REQ-COMMON-36 require from + these two fields and from nothing else. Necessity: these fields are where + the signed length REQ-COMMON-36 demands lives, and a length carried + anywhere outside the signed bytes is a length the prover chooses. +- REQ-COMMON-59 (upholds SP-EXCHANGE-01): + The Notary Service MUST emit one `revealedRange` per range the prover + disclosed in that direction, in ascending `start` order, each carrying its + offsets and exactly `end - start` bytes. The Platform Verifier MUST reject + an attestation whose revealed ranges for a direction are out of order, + overlapping, empty, or ending past that direction's transcript length. + Necessity: revealed bytes signed without their offsets state that some + bytes were disclosed but not where they sat, which is not enough to tile a + transcript under REQ-COMMON-18A or to cover it under REQ-COMMON-35. +- REQ-COMMON-60 (upholds SP-CLIENT-01, SP-EXCHANGE-01): + The Notary Service MUST emit one `rangeCommitment` per hidden range in + that direction, in ascending `start` order, each carrying its offsets and + its commitment value. The Notary Service MUST NOT emit the plaintext of a + committed range anywhere in the attested data. The Platform Verifier MUST + reject an attestation whose range commitments for a direction are out of + order, overlapping, empty, ending past that direction's transcript length, + or overlapping a revealed range. +- REQ-COMMON-61 (upholds SP-EXCHANGE-01): + The Notary Service MUST NOT place in the attested data any value it + obtained by applying a profile rule to the transcript, including a handle, + an account identifier, a client identifier, or a chain address. Necessity: + every such value is already derivable from the revealed ranges, a second + signed representation can disagree with the bytes it was taken from, and + producing one makes the Notary Service decide something profile-specific, + which REQ-COMMON-33 forbids. + +The layout generalizes the one shipped today, extends it in four places, and +drops two consumer-side bindings. Today's signed fields are a chain +identifier, a verifying-contract address, +a hashed platform name, an operation tag, one commitment value with its two +offsets, hashes of the revealed bytes of each direction, two revealed-range +end offsets for the sent direction only, a handle, an account identifier, a +session address, and a timestamp. The generalizations are: one commitment +and one pair of offsets become a list per direction; the sent direction's +two end offsets become explicit `start` and `end` pairs; and the received +direction's revealed bytes, which today carry no offsets at all, gain them. +The extensions are: a leading `formatTag`, so the preimage names its own +layout instead of relying on the operation tag to do it; an `authorityId` +separate from `platformId`, so the authenticated host is a field rather than +an aspect of the platform name; and `sentTranscriptLength` and +`recvTranscriptLength`, which appear nowhere in any signed field today and +which REQ-COMMON-36 requires. The handle, the account identifier, and the +session address are dropped by REQ-COMMON-61; the first two are read +from the revealed ranges under REQ-COMMON-19A, and the address a ceremony +authorizes is bound by the Authorization Digest of §5. The chain identifier +and the verifying-contract address go with them, for the reason given above +the requirements: the attested data describes a session, not a destination. + +### 9.2 Attestation verification and its fee An attestation is authenticated on the Consumer Chain, not inside the Proving Circuit. The Notary Service takes attested data and its notary @@ -1012,7 +1211,7 @@ below governs one attestation a profile does require. Service calls and the fee quotation of REQ-COMMON-06E both count attestations, and a profile leaving that list open fixes neither. - REQ-COMMON-33 (upholds SP-EXCHANGE-01): - The Notary Service MUST take the attested data and its notary + The Notary Service MUST take the attested data of §9.1 and its notary signature and return exactly one accept-or-reject decision covering that signature over exactly those bytes. The Notary Service MUST NOT decide anything profile-specific. Necessity: the attested data carries the @@ -1250,6 +1449,42 @@ the constructions that role implements. Platform Verifier taking the digest from any other source rejects the submission. +- TEST-COMMON-24 (exercises REQ-COMMON-47, REQ-COMMON-48, REQ-COMMON-53, REQ-COMMON-58, REQ-COMMON-61): + Attested data built as the §9.1 concatenation rebuilds byte for byte on the + verifying side and hashes to the signed `attestationDigest`; attested data + whose fields are reordered, whose fixed-width field is emitted at another + width, or whose variable-length part is not preceded by the count and + offset fields the layout places before it derives a key no trusted notary + holds; an attestation whose `formatTag` differs from the one the Platform + Profile pins is rejected; the coverage length of REQ-COMMON-35 comes from + the two transcript-length fields and from no other source; attested data + carrying a handle, an account identifier, a client identifier, or a chain + address is rejected; and attested data carrying a Chain ID or a verifier + identity derives a key no trusted notary holds, because the §9.1 layout + lists neither field. +- TEST-COMMON-25 (exercises REQ-COMMON-49, REQ-COMMON-50, REQ-COMMON-51, REQ-COMMON-52): + A verification call supplying a digest, a preimage hash, or a verification + key beside the attested-data-and-signature pair is rejected; one such pair + yields exactly one accept-or-reject decision, which accepts only under a + currently trusted notary key; the Notary Service exposes no interface + taking a transcript and no decision depending on one; a submission + carrying a caller-supplied duplicate of a value its attested data already + carries is rejected; and a submitted `pkceNonce`, together with the + Google `aud` bytes of a profile carrying no attested data at all, is + accepted because the Platform Verifier authenticates each of them itself. + Verification: inspection of the Notary Service interface for the + transcript rule. +- TEST-COMMON-26 (exercises REQ-COMMON-55, REQ-COMMON-56, REQ-COMMON-57, REQ-COMMON-59, REQ-COMMON-60): + An attestation naming a foreign `platformId`, a foreign `operationTag`, or + a foreign `authorityId` is rejected; the same attestation verifies under + every Platform Verifier version registered for its platform, on the chain + whose Chain ID the Authorization Digest committed; revealed ranges and + range commitments that are out of order, + overlapping, empty, ending past that direction's transcript length, or + overlapping one another are each rejected; and no attested data carries the + plaintext of a committed range or a `createdAt` drawn from the prover, from + a response header, or from any party but the notary. Verification: + inspection of the emitted attestations for the `createdAt` source. ## 12. Security Considerations This document enforces SP-BIND-01, SP-CLIENT-01, SP-EXCHANGE-01, diff --git a/specs/platform-ceremonies.md b/specs/platform-ceremonies.md index 281def7b..8cdeae76 100644 --- a/specs/platform-ceremonies.md +++ b/specs/platform-ceremonies.md @@ -123,22 +123,168 @@ derivation, layered strictly: proof statement or Consumer behavior may rely on it. Necessity: a check running in software the prover chooses whether to run is not a defense. -Normalization applies these per-platform criteria: ASCII-only input with -disallowed bytes rejected; lowercasing; Google validates the value as an -email address and keeps its `@`; X and GitHub strip one leading `@`; -underscore is allowed on X and not on GitHub; hyphen is allowed on GitHub and -not on X, and never leading, trailing, or doubled; a per-platform maximum -length; an empty result is rejected. The exact byte-level algorithm is fixed -by the shared cross-language handle vector table this profile publishes -alongside the specification, which every implementation reproduces; that -table, not this prose, is the precision anchor. A profile that publishes no -such table is ineligible. - -- TEST-PLAT-20 (exercises REQ-PLAT-08A, REQ-PLAT-08B, REQ-PLAT-08C): - Every implementation reproduces the shared handle vector table byte for - byte; a caller-supplied normalized handle or pre-hashed key is rejected; - and identity bytes transformed anywhere before Consumer-side derivation - fail conformance. +The input is the raw authenticated handle of §2.1: Google's signed `email` +claim, X's `data.username`, GitHub's `login`. Normalization is a closed byte +transform over those bytes. It is not Unicode case folding, not UTS-46, and +not IDNA. + +**Parameters.** A profile fixes five values, and nothing else varies between +platforms. The permitted-byte and shape-rule rows are derived from those five +and are stated here so an implementer needs no other source. + +| Parameter | Google | X | GitHub | +|---|---|---|---| +| `maxLength` (bytes) | 62 | 15 | 39 | +| `stripLeadingAt` | false | true | true | +| `isEmail` | true | false | false | +| `allowUnderscore` | false | true | false | +| `allowHyphen` | false | false | true | +| Permitted bytes (step 5) | `a`-`z` `0`-`9` `.` `+` `-` `_` `@` | `a`-`z` `0`-`9` `_` | `a`-`z` `0`-`9` `-` | +| Shape rule (step 6) | email | none | hyphen | + +`isEmail` supersedes the two boolean rows: Google's permitted set is the +email set of the table, not a set assembled from `allowUnderscore` and +`allowHyphen`, which are never read on that path. + +**Algorithm.** Given the raw handle bytes and the parameters above, the +normalized handle is the result of these six steps, applied in this order: + +1. Trim. Remove leading `0x20` bytes and trailing `0x20` bytes. No other byte + is trimmable. Trim runs once, here, and never again. +2. Strip. When `stripLeadingAt` is set, and bytes remain, and the first + remaining byte is `0x40`, remove that one byte. At most one byte is removed. +3. Empty. When no bytes remain, reject as `EmptyHandle`. +4. Length. When more than `maxLength` bytes remain, reject as `HandleTooLong`. + The count is bytes, not characters. +5. Fold and filter, one byte at a time, left to right. A byte in `0x41`-`0x5A` + becomes that byte plus `0x20`; every other byte is unchanged. The folded + byte is then tested against the platform's permitted set, and a byte + outside that set rejects as `BadCharacter`. +6. Shape, over the step-5 output. On the email rule, exactly one `0x40` is + present and it is neither the first nor the last byte; anything else + rejects as `BadShape`. On the hyphen rule, the first byte is not `0x2D`, + the last byte is not `0x2D`, and no two adjacent bytes are both `0x2D`; + anything else rejects as `BadShape`. On no rule, nothing is checked. + +The step-5 output is the normalized handle. + +| Rejection | Raised when | Step | +|---|---|---| +| `EmptyHandle` | no bytes remain after trimming and the `@` strip | 3 | +| `HandleTooLong` | more than `maxLength` bytes remain after those two steps | 4 | +| `BadCharacter` | a folded byte lies outside the permitted set | 5 | +| `BadShape` | every byte is permitted, the arrangement is not | 6 | + +- REQ-PLAT-61: + The Implementation MUST apply steps 1 through 6 in the order given. + Necessity: the steps do not commute, so a re-ordered implementation derives + a different key from the same authenticated bytes. +- REQ-PLAT-62: + In step 1 the Implementation MUST treat `0x20` as the only trimmable byte. + The Implementation MUST carry a leading or trailing `0x09`, `0x0a`, or + `0x0d` into step 5, which rejects it. Necessity: a byte trimmed by one + implementation and refused by another maps two inputs onto one identity in + half the system. +- REQ-PLAT-63: + In step 2 the Implementation MUST remove at most one `0x40` byte. The + Implementation MUST NOT trim again after that removal. Necessity: after + `" @ alice "` is trimmed and stripped a space leads the value and step 5 + refuses it, where a second trim would accept it as `alice`. +- REQ-PLAT-64: + In step 4 the Implementation MUST compare `maxLength` against the byte count + remaining after steps 1 and 2. Necessity: the length gate precedes the + character gate, so an over-long input carrying a disallowed byte is + `HandleTooLong`, and a count of code points would disagree. +- REQ-PLAT-65: + In step 5 the Implementation MUST map bytes `0x41` through `0x5A`, and no + other byte, to that byte plus `0x20`. The Implementation MUST NOT apply + Unicode case folding, UTS-46, IDNA, or any normalization form. Necessity: + two values differing by more than ASCII case are two identities, and a + folding table would merge them. +- REQ-PLAT-66: + In step 5 the Implementation MUST reject every byte outside the platform's + permitted row, including every byte from `0x80` through `0xff`. Necessity: + refusing the whole range above ASCII keeps a multi-byte character out of the + key, which is what lets independent implementations agree without a shared + Unicode table. +- REQ-PLAT-67: + When `isEmail` is set, the Implementation MUST use the email permitted set + whatever `allowUnderscore` and `allowHyphen` hold. Necessity: identity + compatibility, because those two parameters are not read on the email path. +- REQ-PLAT-68: + In step 6 on the email rule, the Implementation MUST require exactly one + `0x40` byte at neither edge. Necessity: a value with no `@`, with two, or + with an empty side is not an address, and the keyspace would otherwise hold + names no account can prove. +- REQ-PLAT-69: + In step 6 the Implementation MUST apply the hyphen rule only when + `allowHyphen` is set and `isEmail` is clear. Necessity: `-` is permitted + inside a Google address, where a leading, trailing, or doubled `-` occurs in + real local parts and domain labels. +- REQ-PLAT-70: + The Implementation MUST report the first rejection the step order reaches. + Necessity: an input that is both over-long and ill-charactered is + `HandleTooLong`, and an implementation checking characters first would + disagree with the published vectors. +- REQ-PLAT-71: + On the email rule the Implementation MUST preserve every dot, every `+` tag, + and the domain exactly as step 5 folded them. The Implementation MUST NOT + remove a dot, strip a tag, or rewrite a domain. Necessity: two addresses a + provider happens to route to one mailbox are two identities here, and + merging them would let one holder reach another's key. +- REQ-PLAT-72: + The Platform Profile MUST fix all five parameters above. Necessity: a + profile leaving any of them open does not define one handle keyspace, and + §7 requires the same of every new platform. +- REQ-PLAT-73: + The Consumer MUST derive the stored handle key from the exact step-5 output + bytes. Necessity: a key derived from the raw bytes, or from a differently + folded value, is a key no reader looks up. +- REQ-PLAT-74: + When a caller-supplied lookup argument does not normalize, the Consumer MUST + answer that no holder exists. The Consumer MUST NOT fail the surrounding + transaction. Necessity: a resolver asked who holds a given text answers + "nobody" for text nobody could hold, and a stray space in a recipient field + would otherwise abort the transaction around it. + +**Conformance vectors.** The algorithm above is the definition. The profile +publishes a cross-language vector table as a cross-check on an implementation +of that algorithm, never as its source: 44 cases, 19 for X, 14 for GitHub and +11 for Google; 15 accepted and 29 rejected, the rejections being 5 +`EmptyHandle`, 3 `HandleTooLong`, 13 `BadCharacter` and 8 `BadShape`. A +disagreement between a published vector and the algorithm above is a defect +in the vector table. A profile that publishes no such table is ineligible. + +Representative cases, quoted from that table: + +| Platform | Input | Result | +|---|---|---| +| X | `" @Alice_1 "` | `alice_1` | +| X | `"@a"` | `a` | +| X | `"a123456789012345"` | `HandleTooLong` | +| X | `"@"` | `EmptyHandle` | +| X | `"@@alice"` | `BadCharacter` | +| X | `" @ alice "` | `BadCharacter` | +| X | `"ali\tce"` | `BadCharacter` | +| X | `"alicé"` | `BadCharacter` | +| GitHub | `"@Octo-Cat"` | `octo-cat` | +| GitHub | `"-octocat"` | `BadShape` | +| GitHub | `"octo--cat"` | `BadShape` | +| GitHub | `"octo_cat"` | `BadCharacter` | +| Google | `"A.B+tag@Example.COM"` | `a.b+tag@example.com` | +| Google | `"@Alice@example.com"` | `BadShape` | +| Google | `"alice"` | `BadShape` | +| Google | `"ali ce@example.com"` | `BadCharacter` | + +In that table `\t` is the single byte `0x09` and `é` is the two bytes +`0xc3 0xa9`. + +- TEST-PLAT-20 (exercises REQ-PLAT-08A, REQ-PLAT-08B, REQ-PLAT-08C, REQ-PLAT-61, REQ-PLAT-62, REQ-PLAT-63, REQ-PLAT-64, REQ-PLAT-65, REQ-PLAT-66, REQ-PLAT-67, REQ-PLAT-68, REQ-PLAT-69, REQ-PLAT-70, REQ-PLAT-71, REQ-PLAT-72, REQ-PLAT-73, REQ-PLAT-74): + Every implementation reproduces the published handle vector table byte for + byte, including the rejection kind of each rejected case; a caller-supplied + normalized handle or pre-hashed key is rejected; a lookup argument that does + not normalize resolves to no holder rather than failing; and identity bytes + transformed anywhere before Consumer-side derivation fail conformance. ### 2.2 Metadata ordering and validity ceilings @@ -419,13 +565,13 @@ attestation format: | Range | Revealed | Why | |---|---|---| | request method and path | yes | the Platform Verifier compares them with its profile constants | -| endpoint authority | not a range | the Notary Service authenticated the TLS server identity, and the Platform Verifier compares the attested authority against its pinned constant per common REQ-COMMON-21A | +| endpoint authority | not a range | the Notary Service authenticated the TLS server identity, and the Platform Verifier compares the attestation's `authorityId` per common REQ-COMMON-56 | | `grant_type` | yes | constant `authorization_code`; the Platform Verifier compares it byte for byte per REQ-PLAT-56 | | `client_id` | yes | the Platform Verifier reads and returns it | | `code` | yes | compared to the code consumed at redirect ingress | | `redirect_uri` | yes | the Canonical Runtime compares its immutable profile; no chain or circuit value | | `code_verifier` | yes | the Platform Verifier recomputes it from the digest and `pkceNonce` per common REQ-COMMON-15A | -| attestation timestamp | not a range | the attestation's own signed creation time, which derives the authenticated validity ceiling per §2.2 | +| attestation timestamp | not a range | the signed `createdAt` of the attested data, which derives the authenticated validity ceiling per §2.2 and common REQ-COMMON-57 | | `"access_token":"` and the closing quote immediately around the bearer value | yes | anchor the committed bearer range as that field's value, per common REQ-COMMON-18A | | bearer range | committed | a blinded commitment, opened only in circuit | | everything else | no | headers, `scope`, `token_type`, other response fields | @@ -433,7 +579,7 @@ attestation format: Neither the authority nor the attestation timestamp is a transcript range. The authority reaches the Platform Verifier as the TLS server identity the Notary Service authenticated under common -REQ-COMMON-21, carried in the attested data: the transcript +REQ-COMMON-21, carried in the attested data as `authorityId`: the transcript holds the authority only in a `Host` header this table hides, and a revealed `Host` header is prover-composed text that says nothing about which server answered. The timestamp is the signed creation time of the attested data @@ -776,8 +922,8 @@ submission and every published artifact. | `code_verifier` | yes | the Platform Verifier recomputes it from the digest and `pkceNonce` per common REQ-COMMON-15A | | `"access_token":"` and the closing quote immediately around the bearer value | yes | anchor the committed bearer range as that field's value, per common REQ-COMMON-18A | | bearer range | committed | a blinded commitment, opened only in circuit to link this attestation to `/user` | -| attestation timestamp | not a range | the attestation's own signed creation time, which derives the authenticated validity ceiling per §2.2 | -| token endpoint authority | not a range | the Notary Service authenticated the TLS server identity, and the Platform Verifier compares the attested authority against its pinned constant per common REQ-COMMON-21A | +| attestation timestamp | not a range | the signed `createdAt` of the attested data, which derives the authenticated validity ceiling per §2.2 and common REQ-COMMON-57 | +| token endpoint authority | not a range | the Notary Service authenticated the TLS server identity, and the Platform Verifier compares the attestation's `authorityId` per common REQ-COMMON-56 | | token request method | yes | the Platform Verifier checks its profile method | | token request path | yes | the Platform Verifier checks its profile path | | `client_secret` | no | never revealed, per REQ-PLAT-35A | @@ -790,7 +936,7 @@ leave that range indistinguishable from a `refresh_token` value. Neither the authority nor the attestation timestamp is a transcript range at all. The authority reaches the Platform Verifier as the TLS server identity the Notary Service authenticated under common -REQ-COMMON-21, carried in the attested data, because the +REQ-COMMON-21, carried in the attested data as `authorityId`, because the transcript holds the authority only in a `Host` header this table hides and a revealed `Host` header is prover-composed text that says nothing about which server answered. The timestamp is the signed creation time of the attested @@ -827,8 +973,8 @@ response header. Revealing more would widen exposure without adding a check. configured notary's signature and revealing the token request's method and path. The Platform Verifier MUST compare those two revealed values with the `github/v1` profile. The Platform Verifier MUST compare the authority that - attestation authenticates with the same profile, per common REQ-COMMON-21A. - Necessity: the authority is never a revealed range, + attestation authenticates with the same profile, per common REQ-COMMON-21A + and REQ-COMMON-56. Necessity: the authority is never a revealed range, because the transcript carries it only in a prover-composed `Host` header. - REQ-PLAT-46 (upholds SP-EXCHANGE-01): The Canonical Runtime MUST require the disclosed `code` to equal the code it