Skip to content

feat!: rotate IdentityJwksRoots through the Notary Service - #5

Open
xgreenx wants to merge 5 commits into
mainfrom
feat/notary-service-rotation
Open

feat!: rotate IdentityJwksRoots through the Notary Service#5
xgreenx wants to merge 5 commits into
mainfrom
feat/notary-service-rotation

Conversation

@xgreenx

@xgreenx xgreenx commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

The keeper obtains the section 9.1 record of a whole-session JWKS reading from the notary and submits GoogleJwtRoots.rotate(attestedData, proof) with quoteRotation() attached as value. The GoogleOidcVerifier target is gone with the archived login stack; a network names google_jwt_roots only (a config still naming identity_jwks_roots or google_oidc_verifier fails to load).

The end-to-end test deploys a real NotaryService (fee 1000 wei) and GoogleJwtRoots on Anvil, serves Google's real key set, and drives dry-run → rotation → steady state; the mock prover frames the body chunked, as Google does, so the on-chain de-chunker is exercised. It asserts the fee reached the service exactly once.

Builds only after the notary stack lands

Follow-up outside this PR: chain-configurations libid-deploy must deploy GoogleJwtRoots (key google_jwt_roots) wired to NotaryService on 0.7.0; its eden-testnet file records no [identity] section, so a keeper pointed at it fails config load by design (the test fixture keeps that file verbatim and asserts the error).

IdentityJwksRoots no longer verifies a Merkle transcript through the
legacy Notary contract: a rotation is the ordinary notarized session
every other consumer submits -- the ceremony section 9.1 record of the
JWKS reading plus the notary's EIP-191 signature over it -- verified by
the Notary Service, which charges the Notary Fee for it. The keeper
follows the contract.

* proof: `ProofSource::obtain` hands back the notary's `NotarizedSession`
  as it came off the wire (`notarize_jwks` for MPC-TLS, `build_session`
  for the mock). The record's bytes are what the signature covers, so
  nothing re-encodes them on the way to the chain.
* chain: `rotate_calldata` encodes `rotate(attestedData, proof)`;
  `submit_rotation` reads `quoteRotation()` right before sending and
  attaches it as value -- the contract forwards it whole and the service
  refuses anything but the exact fee, so a fee changed between decision
  and submission would otherwise waste an MPC-TLS session on a
  `WrongValue` revert. Trust is read from `trustedHashExpiresAt` only.
* config: one JWKS contract per network. `GoogleOidcVerifier` is
  archived with the wallet product, so `google_oidc_verifier`, the
  `ContractKind`/`Target` pair and the `[contracts]` mirror of the
  chain-configurations schema go; a network resolves to its
  `identity_jwks_roots` address or fails to load, which is what the
  legacy eden-testnet fixture now does (kept verbatim).
* e2e: NotaryService (fee 1000 wei) + IdentityJwksRoots from the embedded
  artifacts, Google's real pretty-printed body as the fixture (chunked by
  the mock, so the on-chain de-chunker runs), dry-run -> rotation ->
  steady state, and the fee lands in the service exactly once.

Pins: notary at the head of feat/jwks-ceremony-attestation and the
libid-rs crates at the head of fix/origin-form-request-target (the
commit that puts `GET /oauth2/v3/certs HTTP/1.1` on the wire, the line
the contract pins), both revs until the stack is tagged; the notary's
`[patch]` of tlsnotary/tlsn is repeated here because a `[patch]` is
read from the root manifest only. libid-contracts = "0.7.0" is NOT
PUBLISHED YET: the crate embeds forge output that is not in git, so it
cannot be a git dependency. Verified against a local checkout of that
release candidate through an uncommitted `[patch.crates-io]`; the
Cargo.lock entry for libid-contracts therefore has no source or
checksum, and `cargo build --locked` (the Dockerfile) fails until the
release is on crates.io and `cargo update -p libid-contracts` records
it.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
libid-contracts 0.7.0 renames IdentityJwksRoots to GoogleJwtRoots and moves
it beside the verifier it serves: the list is the google/v1 Platform
Verifier's trusted signing keys and nothing else, so "identity" said
nothing about it. The keeper follows the contract, on every surface that
spelled the old name:

* config: the inline key and the `[identity]` mirror of a
  chain-configurations network file are `google_jwt_roots`;
  `ResolvedNetwork.google_jwt_roots`; the load error and the address
  error name the new key.
* chain: the binding is `libid_contracts::bindings::ceremony::GoogleJwtRoots`
  (out of `identity`), calldata and the ABI round-trip test with it.
* run/proof/lib docs, log labels and the status table; the crate
  description says what the keeper keeps: Google's JWT signing keys,
  trusted on chain.
* e2e: deploys the `GoogleJwtRoots` artifact and configures it under the
  new key; the eden-testnet fixture's comment names the new key (the file
  is otherwise verbatim and still resolves to no contract).

Breaking for keeper.toml and for referenced network files: a
`identity_jwks_roots` key is now unknown and refused at load, which is the
right failure for a contract that no longer exists under that name.

Verified against a scratchpad copy of libid-contracts/rust/contracts at
feat/jwks-roots-notary-service (4d97ac3) with the version bumped to 0.7.0
through an uncommitted `[patch.crates-io]`, removed before this commit;
Cargo.lock is untouched.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The contract installs 2048-bit moduli only; a key of any other size
reverts InvalidModulusLength. The keeper hashed whatever decoded, so such
a key would read as untrusted and send a rotation that reverts, every
tick, for as long as Google published it. It is skipped with a warning
now, and the live keys keep rotating; a set with no acceptable key at all
is still an error.

Found by the key-insertion audit of GoogleJwtRoots (2026-09-03).

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The contract keeps the latest reading's key set and the one before it, and
nothing per kid: no modulusOfKid, no currentRoots. The end-to-end test
asserts the reading became the current generation whole, the kid hash the
keeper computed for a storage key nothing keys by any more is gone, and
the comments say lifetime where they said TTL.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
libid-contracts v0.7.0 is published, so the lock entry that was resolved
under a local path patch gains its registry source and checksum. Nothing
else moves: the manifest already asked for 0.7.0, and the notary and
libid-rs pins stay on their revs until those stacks release.

Verified against the published crate: nightly fmt, cargo build --locked,
clippy -D warnings, cargo test --all (18 unit, 4 integration incl. the
Anvil end-to-end rotation).

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx marked this pull request as ready for review September 8, 2026 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant