diff --git a/rust/contracts/src/bindings/ceremony.rs b/rust/contracts/src/bindings/ceremony.rs index 2c93b170..600dc138 100644 --- a/rust/contracts/src/bindings/ceremony.rs +++ b/rust/contracts/src/bindings/ceremony.rs @@ -141,8 +141,8 @@ mod google_jwt_roots_inner { /// What one rotation costs beyond gas: the Notary Fee, forwarded /// whole. `rotate` must be sent with exactly this value. function quoteRotation() external view returns (uint256); - /// Permissionless. `attestedData` is the ceremony-common section - /// 9.1 record of the JWKS session, `proof` the notary's + /// Permissionless. `attestedData` is the platform-ceremonies + /// section 4.1 record of the JWKS session, `proof` the notary's /// authentication of it (a 65-byte EIP-191 signature today). A /// reading dated no later than the current generation is refused /// with `NotNewer(createdAt, observedAt)`, and the revert hands diff --git a/rust/profiles/src/profiles.rs b/rust/profiles/src/profiles.rs index 262f1a97..87ccd63b 100644 --- a/rust/profiles/src/profiles.rs +++ b/rust/profiles/src/profiles.rs @@ -65,9 +65,8 @@ pub struct TokenSession { /// The form fields the body carries, in the order the prover /// serializes them. Every verifier holds the whole body to this list: /// exactly these names in this order, each once with a nonempty - /// value, nothing after the last. GitHub's list is REQ-PLAT-61's; - /// X's specification keeps its decoded form on ASM-PROV-07, so the - /// contract is stricter than the specification there. + /// value, nothing after the last. GitHub's list is REQ-PLAT-61's + /// and X's is REQ-PLAT-63's. pub token_fields: &'static [&'static str], } diff --git a/scripts/regen-ceremony-profiles.py b/scripts/regen-ceremony-profiles.py index c4976e35..59b12536 100755 --- a/scripts/regen-ceremony-profiles.py +++ b/scripts/regen-ceremony-profiles.py @@ -446,9 +446,8 @@ def gen_sol(spec: dict[str, Any]) -> str: " /// the prover serializes them, joined by `&`. Every verifier holds", " /// the whole body to its list (`requireExactForm`): exactly these", " /// names in this order, each once with a nonempty value and nothing", - " /// after the last. GitHub's list is REQ-PLAT-61's; X's specification", - " /// keeps its decoded form on ASM-PROV-07, so the contract is stricter", - " /// than the specification there.", + " /// after the last. GitHub's list is REQ-PLAT-61's and X's is", + " /// REQ-PLAT-63's.", "", ] for profile in profiles: @@ -652,9 +651,8 @@ def gen_rust(spec: dict[str, Any]) -> str: " /// The form fields the body carries, in the order the prover", " /// serializes them. Every verifier holds the whole body to this list:", " /// exactly these names in this order, each once with a nonempty", - " /// value, nothing after the last. GitHub's list is REQ-PLAT-61's;", - " /// X's specification keeps its decoded form on ASM-PROV-07, so the", - " /// contract is stricter than the specification there.", + " /// value, nothing after the last. GitHub's list is REQ-PLAT-61's", + " /// and X's is REQ-PLAT-63's.", " pub token_fields: &'static [&'static str],", "}", "", @@ -829,9 +827,7 @@ def gen_ts(spec: dict[str, Any]) -> str: " /** The form fields the body carries, in the order the prover serializes", " * them. Every verifier holds the whole body to this list: exactly these", " * names in this order, each once with a nonempty value, nothing after", - " * the last. GitHub's list is REQ-PLAT-61's; X's specification keeps its", - " * decoded form on ASM-PROV-07, so the contract is stricter than the", - " * specification there. */", + " * the last. GitHub's list is REQ-PLAT-61's and X's is REQ-PLAT-63's. */", " readonly tokenFields: readonly string[]", "}", "", diff --git a/solidity/contracts/ceremony/CeremonyAttestation.sol b/solidity/contracts/ceremony/CeremonyAttestation.sol index 7454d823..70f3d2b0 100644 --- a/solidity/contracts/ceremony/CeremonyAttestation.sol +++ b/solidity/contracts/ceremony/CeremonyAttestation.sol @@ -4,15 +4,14 @@ pragma solidity ^0.8.24; import {CeremonyFields} from "./CeremonyFields.sol"; /// @title CeremonyAttestation -/// @notice Decoder for the attested-data layout the launch profiles pin. -/// @dev THE LAYOUT IS THE PROFILE'S, NOT THE SPECIFICATION'S. REQ-COMMON-18 -/// has a Platform Profile fix the attestation format it accepts and -/// leaves the format itself to the profile author, so these bytes are an -/// agreement between four components -- this library, `libid-ceremony` in -/// libid-rs, the TypeScript mirror, and the notary that signs them -- -/// rather than a reading of a published layout. A divergence is silent: -/// the signature derives a key nobody trusts and every genuine -/// attestation is rejected with no error saying why. +/// @notice Decoder for the attested-data layout the X and GitHub profiles pin. +/// @dev THE LAYOUT IS THE PROFILE'S. REQ-COMMON-18 has a Platform Profile fix +/// the attestation format it accepts, and the X and GitHub profiles fix +/// this one in platform-ceremonies section 4.1. Four components read or +/// write these bytes -- this library, `libid-ceremony` in libid-rs, the +/// TypeScript mirror, and the notary that signs them -- and a divergence +/// is silent: the signature derives a key nobody trusts and every +/// genuine attestation is rejected with no error saying why. /// /// The verifying side holds no transcript. It rebuilds these exact bytes /// from what it was handed and derives the signing key from them, so a diff --git a/solidity/contracts/ceremony/CeremonyProfile.sol b/solidity/contracts/ceremony/CeremonyProfile.sol index 71cb06f7..6fe57523 100644 --- a/solidity/contracts/ceremony/CeremonyProfile.sol +++ b/solidity/contracts/ceremony/CeremonyProfile.sol @@ -74,9 +74,8 @@ library CeremonyProfile { /// the prover serializes them, joined by `&`. Every verifier holds /// the whole body to its list (`requireExactForm`): exactly these /// names in this order, each once with a nonempty value and nothing - /// after the last. GitHub's list is REQ-PLAT-61's; X's specification - /// keeps its decoded form on ASM-PROV-07, so the contract is stricter - /// than the specification there. + /// after the last. GitHub's list is REQ-PLAT-61's and X's is + /// REQ-PLAT-63's. bytes internal constant X_TOKEN_FIELDS = "grant_type&client_id&code&redirect_uri&code_verifier"; bytes internal constant GITHUB_TOKEN_FIELDS = "client_id&code&redirect_uri&code_verifier&client_secret"; diff --git a/solidity/contracts/ceremony/GoogleJwtRoots.sol b/solidity/contracts/ceremony/GoogleJwtRoots.sol index 0b272414..862b8c6e 100644 --- a/solidity/contracts/ceremony/GoogleJwtRoots.sol +++ b/solidity/contracts/ceremony/GoogleJwtRoots.sol @@ -48,14 +48,15 @@ import {INotaryService} from "./INotaryService.sol"; /// by modulus for a lifetime; revisit if Google's rotation ever shows /// that shape. /// -/// The reading is an ordinary notarized session -- the section 9.1 -/// record every Platform Verifier consumes, authenticated by the same -/// Notary Service and charged the same Notary Fee. What differs is the -/// layout the keeper's prover produces: everything in both directions is -/// revealed and nothing is committed. A public key set has nothing to -/// hide, and zero commitments is what lets this contract read the -/// transcript by concatenation safely -- with exact coverage and no -/// commitment, a cut between ranges cannot hide bytes. +/// The reading is an ordinary notarized session -- the +/// platform-ceremonies section 4.1 record the X and GitHub Platform +/// Verifiers consume, authenticated by the same Notary Service and +/// charged the same Notary Fee. What differs is the layout the keeper's +/// prover produces: everything in both directions is revealed and +/// nothing is committed. A public key set has nothing to hide, and zero +/// commitments is what lets this contract read the transcript by +/// concatenation safely -- with exact coverage and no commitment, a cut +/// between ranges cannot hide bytes. /// /// **Rotation is permissionless, and deliberately has no nullifier.** With /// an open caller set a one-shot nullifier would BE the attack: a @@ -304,7 +305,7 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab /// the Notary Fee, and nothing else -- no role, no allowlist, no /// owner anywhere in the path. /// - /// @param attestedData The exact bytes of ceremony-common section 9.1. + /// @param attestedData The exact bytes of platform-ceremonies section 4.1. /// @param proof The notary's authentication of them, opaque here. function rotate(bytes calldata attestedData, bytes calldata proof) external payable { GoogleJwtRootsStorage storage $ = _s(); diff --git a/solidity/contracts/ceremony/INotaryService.sol b/solidity/contracts/ceremony/INotaryService.sol index da818e71..98a4dda6 100644 --- a/solidity/contracts/ceremony/INotaryService.sol +++ b/solidity/contracts/ceremony/INotaryService.sol @@ -45,7 +45,7 @@ interface INotaryService { /// Handed back, the fields cannot be reached without paying for the /// check that vouches for them. /// - /// @param attestedData The exact bytes of ceremony-common section 9.1. + /// @param attestedData The exact bytes of platform-ceremonies section 4.1. /// @param proof Whatever this service accepts as the notary's /// authentication of `attestedData`. Opaque to the /// caller. diff --git a/solidity/contracts/ceremony/TlsNotaryVerifierBase.sol b/solidity/contracts/ceremony/TlsNotaryVerifierBase.sol index 47237f78..c0c01051 100644 --- a/solidity/contracts/ceremony/TlsNotaryVerifierBase.sol +++ b/solidity/contracts/ceremony/TlsNotaryVerifierBase.sol @@ -140,9 +140,8 @@ abstract contract TlsNotaryVerifierBase is IPlatformVerifier, PlatformVerifierBa /// one spelling, and nothing after the last. Every profile is held /// this way, so the reads below see a body already known to carry /// each name once, and acceptance never rests on the platform - /// refusing a form it did not count. GitHub's list is REQ-PLAT-61's; - /// X's specification keeps its decoded form on ASM-PROV-07, so the - /// contract is stricter than the specification there. + /// refusing a form it did not count. GitHub's list is REQ-PLAT-61's + /// and X's is REQ-PLAT-63's. function _tokenFields() internal pure virtual returns (bytes memory); /// @dev Any VALUE constraint the profile places on a token-body field diff --git a/solidity/contracts/ceremony/XPlatformVerifier.sol b/solidity/contracts/ceremony/XPlatformVerifier.sol index cfdc21a3..2b16f110 100644 --- a/solidity/contracts/ceremony/XPlatformVerifier.sol +++ b/solidity/contracts/ceremony/XPlatformVerifier.sol @@ -18,9 +18,7 @@ import {TlsNotaryVerifierBase} from "./TlsNotaryVerifierBase.sol"; /// body field, so the request is revealed entire and /// `_tokenBody` refuses any other count. The body is /// held to exactly `X_TOKEN_FIELDS` in order, as every -/// profile's is; the specification keeps X's decoded -/// form on ASM-PROV-07, so this contract is stricter -/// than the specification there. +/// profile's is (REQ-PLAT-63). /// token response — the `"access_token":"` delimiter and its closing /// quote revealed; the bearer and every other byte /// committed. diff --git a/solidity/contracts/ceremony/profiles.json b/solidity/contracts/ceremony/profiles.json index 335f2c3b..2edf3fe7 100644 --- a/solidity/contracts/ceremony/profiles.json +++ b/solidity/contracts/ceremony/profiles.json @@ -125,14 +125,12 @@ "tokenFields is the order the specification's X table gives the", "body, and what the verifier holds the WHOLE body to: exactly", "these five, in this order, each once with a nonempty value in the", - "serializer's one spelling, and not a byte more. A form X might", - "read as a sixth field -- a raw `;` inside a value, an encoded", - "spelling of a name, a second `grant_type` -- is refused here, so", - "acceptance does not depend on X rejecting a malformed or", - "duplicate form. The specification keeps X's decoded form on", - "ASM-PROV-07 -- the platform rejects a body carrying a profile", - "field twice -- backed by the recurring probes REQ-COMMON-32", - "requires; the contract is stricter than the specification here.", + "serializer's one spelling, and not a byte more (REQ-PLAT-63). A", + "form X might read as a sixth field -- a raw `;` inside a value,", + "an encoded spelling of a name, a second `grant_type` -- is", + "refused here, so nothing about this profile rests on", + "ASM-PROV-07: acceptance does not depend on X rejecting a", + "malformed or duplicate form.", "Of the values, the verifier reads three: `code_verifier`, the", "one value the digest recomputes; `client_id`, held to", "REQ-COMMON-16B's charset; `grant_type`, compared to", diff --git a/solidity/contracts/ceremony/test/AttestationBuilder.sol b/solidity/contracts/ceremony/test/AttestationBuilder.sol index 170def34..b0bb0c41 100644 --- a/solidity/contracts/ceremony/test/AttestationBuilder.sol +++ b/solidity/contracts/ceremony/test/AttestationBuilder.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -/// @notice Builds section 9.1 attested data, for tests only. +/// @notice Builds platform-ceremonies section 4.1 attested data, for tests only. /// /// @dev The inverse of `CeremonyAttestation.decode`. Rust-versus-Solidity /// agreement on these bytes is proven separately, by the pinned fixture in diff --git a/solidity/contracts/ceremony/test/CeremonyAttestation.t.sol b/solidity/contracts/ceremony/test/CeremonyAttestation.t.sol index 66809989..dcb11ab6 100644 --- a/solidity/contracts/ceremony/test/CeremonyAttestation.t.sol +++ b/solidity/contracts/ceremony/test/CeremonyAttestation.t.sol @@ -5,8 +5,8 @@ import {Test} from "forge-std/Test.sol"; import {CeremonyAttestation} from "../CeremonyAttestation.sol"; import {AttestationBuilder} from "./AttestationBuilder.sol"; -/// @notice Pins the section 9.1 decoder against bytes produced by the Rust -/// encoder in `libid-rs/crates/libid-ceremony`. +/// @notice Pins the platform-ceremonies section 4.1 decoder against bytes +/// produced by the Rust encoder in `libid-rs/crates/libid-ceremony`. /// @dev The notary writes these bytes and the chain reads them, in two /// languages. If the two ever disagree the notary's signature derives a /// key nobody trusts and every genuine attestation is rejected, so the diff --git a/solidity/contracts/ceremony/test/CeremonyAuthorization.t.sol b/solidity/contracts/ceremony/test/CeremonyAuthorization.t.sol index 995407a7..248141f0 100644 --- a/solidity/contracts/ceremony/test/CeremonyAuthorization.t.sol +++ b/solidity/contracts/ceremony/test/CeremonyAuthorization.t.sol @@ -76,7 +76,7 @@ contract CeremonyAuthorizationTest is Test { ); assertTrue( CeremonyAuthorization.digest(OPERATION_DOMAIN, 2, CHAIN_ID, NONCE, TRANSACTION_DATA) != base, - "verifier version does not bind" + "platform ceremony version does not bind" ); assertTrue( CeremonyAuthorization.digest(OPERATION_DOMAIN, 1, bytes32(uint256(CHAIN_ID) ^ 1), NONCE, TRANSACTION_DATA) diff --git a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol index 5f571389..dbb38dfd 100644 --- a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol +++ b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol @@ -27,12 +27,13 @@ contract LimbHasher is GoogleJwtRoots { /// The `google/v1` profile's trust list, fed by a notarized reading of /// Google's JWKS endpoint. /// -/// The reading is an ordinary section 9.1 session: built with the same -/// builder every Platform Verifier test uses, signed at test time by the anvil -/// key, and authenticated by a real NotaryService behind a proxy charging a -/// real fee. Nothing here mocks the thing under test. Google's real body rides -/// along as a fixture, so the parser is tested against the document it will -/// read rather than a shape a test author remembered. +/// The reading is an ordinary platform-ceremonies section 4.1 session: built +/// with the same builder the X and GitHub Platform Verifier tests use, signed +/// at test time by the anvil key, and authenticated by a real NotaryService +/// behind a proxy charging a real fee. Nothing here mocks the thing under +/// test. Google's real body rides along as a fixture, so the parser is tested +/// against the document it will read rather than a shape a test author +/// remembered. contract GoogleJwtRootsTest is Test { GoogleJwtRoots roots; NotaryService notary; diff --git a/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol index 4d60e569..ed1178a0 100644 --- a/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol +++ b/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol @@ -105,9 +105,10 @@ contract GoogleJwtRootsRealSessionTest is Test { // ─── What the fixture is ──────────────────────────────────────── /// The record is the JWKS session layout, signed by the key the fixture - /// names, and its bytes sit where the section 9.1 layout fixes them: the - /// sent direction's first revealed range begins at 48 + 8 + 4 + 8 = 68, - /// the received one at 68 + 136 + 8 + 8 + 4 + 8 = 232. + /// names, and its bytes sit where the platform-ceremonies section 4.1 + /// layout fixes them: the sent direction's first revealed range begins + /// at 48 + 8 + 4 + 8 = 68, the received one at + /// 68 + 136 + 8 + 8 + 4 + 8 = 232. function test_theRecordIsTheSessionTheFixtureDescribes() public view { assertEq(signer, vm.addr(ANVIL_KEY), "the fixture was signed by anvil #0"); assertEq(record.length, 2811); diff --git a/solidity/contracts/ceremony/test/NotaryService.t.sol b/solidity/contracts/ceremony/test/NotaryService.t.sol index cdacd9dc..777f29bf 100644 --- a/solidity/contracts/ceremony/test/NotaryService.t.sol +++ b/solidity/contracts/ceremony/test/NotaryService.t.sol @@ -9,7 +9,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {CeremonyAttestation} from "../CeremonyAttestation.sol"; import {NotaryService} from "../NotaryService.sol"; -/// @notice The Notary Service of ceremony-common section 9.2. +/// @notice The Notary Service of ceremony-common section 9.1. /// @dev The signature below is real: produced with `cast wallet sign` over the /// digest of the cross-language attestation fixture, by the public anvil /// key. Nothing here mocks the thing under test. diff --git a/solidity/contracts/ceremony/test/XPlatformVerifier.t.sol b/solidity/contracts/ceremony/test/XPlatformVerifier.t.sol index 7747a0f9..8ea8be2c 100644 --- a/solidity/contracts/ceremony/test/XPlatformVerifier.t.sol +++ b/solidity/contracts/ceremony/test/XPlatformVerifier.t.sol @@ -429,10 +429,10 @@ contract XPlatformVerifierTest is Test { this.run{value: quote}(s); } - /// @dev A duplicate in an ENCODED spelling. `code%5Fverifier` is - /// `code_verifier` to a form parser but no literal match for it, - /// which is the case ASM-PROV-07 covers for the specification. It is - /// a sixth pair here, refused like any other. + /// @dev REQ-PLAT-63, TEST-PLAT-09C: a duplicate in an ENCODED spelling. + /// `code%5Fverifier` is `code_verifier` to a form parser but no + /// literal match for it. It is a sixth pair here, refused like any + /// other. function test_rejectsATokenBodyWithAnEncodedDuplicateName() public { bytes memory honest = _honestTokenBody(); TlsNotaryVerifierBase.TlsNotaryProof memory s = diff --git a/solidity/contracts/identity/IdentityRegistry.sol b/solidity/contracts/identity/IdentityRegistry.sol index 376af3ab..cf1b25c2 100644 --- a/solidity/contracts/identity/IdentityRegistry.sol +++ b/solidity/contracts/identity/IdentityRegistry.sol @@ -78,10 +78,12 @@ import {IdentityNodes} from "./IdentityNodes.sol"; /// learn whether its copy is stale. Merging them into one map would remove /// the only freshness signal the chain can give. /// -/// **There is no nullifier.** A binding is idempotent, so replaying a -/// proof would rewrite the same value; the danger is an older proof -/// undoing a newer one, and the `observedAt` watermark refuses that. A -/// replay carries the same timestamp, so the same rule refuses it too. +/// **The Authorization Digest is the nullifier.** `bind` records every +/// digest it accepts in `spentDigests` and refuses one already recorded +/// with `DigestAlreadySpent` (REQ-COMMON-03, REQ-COMMON-03A). An older +/// proof carries a digest of its own, so the `observedAt` watermark is +/// what stops it undoing a newer binding. Equal is not newer, so the +/// watermark also refuses a second proof of the same observation. /// /// **There is no pause.** A pause is a lever over other people's bindings, /// and nothing here needs one: no funds are held, and no address is diff --git a/solidity/contracts/identity/test/IdentityRegistry.t.sol b/solidity/contracts/identity/test/IdentityRegistry.t.sol index 559f4906..d5873854 100644 --- a/solidity/contracts/identity/test/IdentityRegistry.t.sol +++ b/solidity/contracts/identity/test/IdentityRegistry.t.sol @@ -205,8 +205,10 @@ contract IdentityRegistryTest is Test { assertEq(registry.resolveHandle(X, "shared"), bob, "the handle moved back"); } - /// Replaying the exact proof is refused by the same rule, because equal is - /// not newer. That is why the contract needs no nullifier. + /// A second proof of the same observation, under a fresh digest, is + /// refused by the same rule, because equal is not newer. The exact proof + /// stops earlier, at its spent digest (`test_aDigestIsSpendableOnce` in + /// `CeremonyBind.t.sol`). function test_replayingAProofIsRefused() public { _bind(alice, "123", "alice", 100); diff --git a/solidity/script/Deploy.s.sol b/solidity/script/Deploy.s.sol index f024dd93..24cab349 100644 --- a/solidity/script/Deploy.s.sol +++ b/solidity/script/Deploy.s.sol @@ -48,7 +48,7 @@ contract Deploy is Script { // 1. The Notary Service (UUPS proxy) -- the ONE place a notary // attestation is authenticated, deployed first so its proxy address // can be wired into every consumer's initialize. It derives the - // digest from the attested bytes itself (REQ-COMMON-49) and charges + // digest from the attested bytes itself (REQ-COMMON-33) and charges // the Notary Fee. Key rotation is `setNotary`; a fee change is // `setFee`; a proof-system change is a UUPS upgrade of this proxy // alone. diff --git a/ts/packages/contracts/src/ceremony/profiles.ts b/ts/packages/contracts/src/ceremony/profiles.ts index ee0fe413..da99b1bd 100644 --- a/ts/packages/contracts/src/ceremony/profiles.ts +++ b/ts/packages/contracts/src/ceremony/profiles.ts @@ -46,9 +46,7 @@ export interface TokenSession { /** The form fields the body carries, in the order the prover serializes * them. Every verifier holds the whole body to this list: exactly these * names in this order, each once with a nonempty value, nothing after - * the last. GitHub's list is REQ-PLAT-61's; X's specification keeps its - * decoded form on ASM-PROV-07, so the contract is stricter than the - * specification there. */ + * the last. GitHub's list is REQ-PLAT-61's and X's is REQ-PLAT-63's. */ readonly tokenFields: readonly string[] }