Skip to content
4 changes: 2 additions & 2 deletions rust/contracts/src/bindings/ceremony.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions rust/profiles/src/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
}

Expand Down
14 changes: 5 additions & 9 deletions scripts/regen-ceremony-profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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],",
"}",
"",
Expand Down Expand Up @@ -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[]",
"}",
"",
Expand Down
17 changes: 8 additions & 9 deletions solidity/contracts/ceremony/CeremonyAttestation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions solidity/contracts/ceremony/CeremonyProfile.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
19 changes: 10 additions & 9 deletions solidity/contracts/ceremony/GoogleJwtRoots.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion solidity/contracts/ceremony/INotaryService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 2 additions & 3 deletions solidity/contracts/ceremony/TlsNotaryVerifierBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions solidity/contracts/ceremony/XPlatformVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 6 additions & 8 deletions solidity/contracts/ceremony/profiles.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion solidity/contracts/ceremony/test/AttestationBuilder.sol
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions solidity/contracts/ceremony/test/CeremonyAttestation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 7 additions & 6 deletions solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion solidity/contracts/ceremony/test/NotaryService.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions solidity/contracts/ceremony/test/XPlatformVerifier.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
10 changes: 6 additions & 4 deletions solidity/contracts/identity/IdentityRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions solidity/contracts/identity/test/IdentityRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion solidity/script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 1 addition & 3 deletions ts/packages/contracts/src/ceremony/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
}

Expand Down
Loading