Skip to content

feat(jwks): the JWKS prover and its mock live here, on libid-rs - #7

Closed
xgreenx wants to merge 16 commits into
feat/notary-service-rotationfrom
feat/jwks-prover-in-keeper
Closed

xgreenx wants to merge 16 commits into
feat/notary-service-rotationfrom
feat/jwks-prover-in-keeper

Conversation

@xgreenx

@xgreenx xgreenx commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

The keeper reached its notarized reading through the notary crate, and CI proved the rotation against a source-built notary and inline-deployed contracts.

Changes

  • src/jwks/ holds the JWKS layout, the prover over libid_tlsn::prover_generic and the mock; the notary dependency is gone.
  • libid-rs v0.4.0, libid-contracts 0.10.0, TLSNotary 550220e, the 24-entry libid-org/mpz patch 4db9454.
  • reqwest 0.13 (the copy alloy already pulls), toml 1, base64 0.23, num-bigint 0.5, Rust 1.98.1; alloy and k256 stay where libid-contracts and libid-crypto hold them.
  • The e2e job pulls ghcr.io/libid-org/notary:custom-1f54c19 and deploys with libid-deploy onto a throwaway anvil — image, tool and network file one env var each.
  • tests/e2e_real.rs takes KEEPER_E2E_GOOGLE_JWT_ROOTS, KEEPER_E2E_RPC and KEEPER_E2E_NOTARY_SERVICE; with none set it spawns its own anvil, so a local run stays one command.
  • The image drops root and the unused OpenSSL and defaults KEEPER_CONFIG to /etc/keeper/keeper.toml; releases publish RC tags, :latest only for final.

Verified

  • cargo test --all: 27 unit, 4 integration; clippy -D warnings and nightly fmt clean on 1.98.1.
  • Real rotation three ways against the pulled notary image: inline deploy, external stack, external plus explicit service.
  • Image builds at 150MB, runs as uid 10001, links libc/libm/libgcc_s only; keeper status reads the chain from a mounted config.
  • On GitHub: Rust and DCO green; the e2e job pulls and starts the notary image.

Not verified / known issues

  • libid-deploy v0.7.0 and networks/local-dev.toml do not exist yet: both URLs and the [identity] key name are guesses until that release lands.
  • The rotation itself has not run on GitHub, and Google rotating mid-run fails it.

The keeper obtained its notarized reading through the notary crate's
`jwks` helpers, which made it depend on the whole notary server and pin
the notary's exact libid-rs revision so cargo kept one copy of the types.
The notary is client-agnostic -- it signs the same record for every
session -- so those helpers were always keeper code.

Move them in: `jwks::layout` (reveal both directions whole, commit
nothing), `jwks::prover::{run_jwks_prover, notarize_jwks}` over
`libid_tlsn::prover_generic`, and `jwks::mock::MockProver`, which
synthesizes a byte-faithful record without MPC for the end-to-end test;
plus `examples/notarize_jwks`, which captures a real reading as a fixture.
`NotarizedSession` is `libid_transcript::AttestationWire`, so the keeper
and the notary agree on the frame by construction. The module carries its
own error type, the notary's minus the server-only variants.

Manifest: drop `notary`; libid-crypto and libid-signer move from rev
391078f to tag v0.3.0, joined by libid-tlsn, libid-transcript and
libid-ceremony at the same tag. Running the MPC prover means carrying the
TLSNotary fork pin (libid-org/tlsn 550220e) and, because cargo ignores
dependency-workspace patches, the same 24-entry libid-org/mpz patch block
the notary carries; the two move together.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Review of the move found leftovers from the module's notary origins, none
functional:

  * the module doc pinned [`NotarizedSession`] to `crate::NotarizedSession`,
    where the notary re-exported it; here the alias lives in this module,
    so the bare link resolves and the definition line goes (cargo doc was
    warning);
  * `jwks::Error` carried `Io`, `Json` and `Base64` variants nothing in the
    module converts into -- `Io`/`Json` served the notary's server side,
    `Base64` never existed there. The type now names exactly the sources
    the module converts: Jwks, Tlsn, Transcript, Crypto, Http;
  * three manifest comments still described a `notary` dependency: the
    rust-version floor, the publish = false rationale, and the libid-rs pin
    block (which also narrated a branch rev now replaced by the tag). The
    tlsn-patch comment likewise; and a stray blank line where the notary
    block was;
  * the e2e doc and the module doc still spoke from the notary's point of
    view;
  * the mock's hyper test uses `tokio::io::duplex`/`AsyncReadExt`, which
    compiled only through feature unification -- declare `io-util`.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx
xgreenx marked this pull request as ready for review September 14, 2026 11:23
ae8d1c5 replaced a single line of a multi-line comment and left its
neighbour dangling mid-sentence. Rewrite the block under the tlsn `[patch]`
header as one paragraph. Comment only.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
… example

`examples/notarize_jwks` was a one-off from 2026-09-03: it captured a real
notarized reading so libid-contracts#21 could test `GoogleJwtRoots` against
a record Google actually served. It came along with the prover helpers when
they moved here, but it is not keeper functionality -- the keeper's own
`proof.rs` -> `notarize_jwks` -> `rotate` path is that flow with the chain
step on the end.

Replace it with the test that flow deserves. `e2e.rs` proves everything but
MPC-TLS, with the mock as the proof source; `tests/e2e_real.rs` proves the
rest: a running notary the keeper shares no crate with, the real contracts
on Anvil, Google's live key set, and one genuine MPC-TLS rotation -- dry run,
the rotation with the fee landing in the service, steady state. It is
`#[ignore]`d because it needs a notary (`KEEPER_E2E_NOTARY`, local binary or
the ghcr.io image) and network to Google; the module docs say how to run it.
`KEEPER_E2E_CAPTURE` writes the record it obtained, which is what the
example used to do, with the signature checked against the trusted address
first.

`chrono` leaves the dev-dependencies with the example.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
`cargo test --all` covers everything but MPC-TLS, with the mock prover
standing in. The ignored e2e covers the rest, and nothing ran it.

The notary is built from source at a pinned rev rather than pulled. The
published image predates the ceremony-attestation TCP path this test
drives. Bump NOTARY_REV once an image carries it.

The job reaches www.googleapis.com on purpose. GoogleJwtRoots pins that
authority, so a local TLS fixture cannot stand in.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
Latest of both. libid-rs v0.4.0 brings libid-profiles and libid-identity
0.10 and two transcript fixes the JWKS layout depends on: a JSON member
is found with the whitespace JSON allows, and one scan finds the key for
both member shapes.

libid-contracts 0.10.0 is the version the ceremony contracts ship in.
The anvil rotation e2e deploys NotaryService and GoogleJwtRoots from its
artifacts and still reaches steady state.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
reqwest 0.13 is the version alloy's HTTP transport already pulls, so the
build now carries one reqwest and one TLS stack instead of two; `rustls`
is that release's name for the `rustls-tls` backend. toml 1, base64 0.23
and num-bigint 0.5 are the current majors, and nothing here exchanges
their types with another crate.

alloy and k256 stay: libid-contracts hands back alloy 1 types and
libid-crypto takes a k256 0.13 key, so a newer major would be a second,
incompatible type rather than an upgrade.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
1.98.1 is the current stable. The pin sits above the crate's inherited
1.95 floor, which no job exercises -- so say that, rather than repeating
a claim about building on the floor that stopped being true at 1.97.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
CI no longer deploys the contracts from inside this test: libid-deploy
puts them on the chain, so the addresses have to come in from outside.
KEEPER_E2E_GOOGLE_JWT_ROOTS selects that stack, KEEPER_E2E_RPC names its
chain, KEEPER_E2E_NOTARY_SERVICE overrides the service the fee is paid
to -- and with none of them set the test still spawns its own anvil and
deploys, so a local run stays one command.

The fee now comes from quoteRotation() rather than the constant, and two
wirings an external stack can get wrong are asserted before the session.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The job built the notary from source and deployed the contracts from
inside the test, so it proved the rotation against two things no
deployment ever runs. It now pulls the published notary image and lays
the stack down with libid-deploy on a throwaway anvil, which is exactly
what a deployment does; the image, the tool and its network file are one
env var each at the top of the file.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The graph has no openssl-sys -- ldd on the binary lists libc, libm and
libgcc_s -- so libssl-dev and libssl3 were claiming a dependency that
does not exist. The keeper writes nothing and listens on nothing, so it
runs as uid 10001. KEEPER_CONFIG now points at /etc/keeper/keeper.toml,
because --config defaults to a relative path that resolves against / in
a container, which makes a mounted config the whole run recipe. The
OCI labels link the ghcr package to this repo, as the notary's do.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
A release candidate could not be published at all, and every release
moved :latest, so pulling it could land on an RC. Both now follow the
notary's rules, which downstream consumers already read. The header also
described a wasm bundle this repo does not ship.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The head of the notary's release branch, and the tag its own image
workflow published last. A `custom-` tag is a build artefact: this
becomes a version tag once notary#10 merges and a release is cut.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The notary pull is the slowest step and depends on nothing else in the
job, so running it first both overlaps the download with the deploy and
proves the image starts even on a run that fails further down.

Assisted-by: Claude Opus 5
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
The step looked only under [identity]. chain-configurations declares the
ceremony contracts under [contracts], so the lookup found nothing and the
job failed after a successful deploy.

Read both sections. The pre-0.10 schema carried the roots under
[identity] as identity_jwks_roots, and the new one is still settling
upstream, so accepting either keeps this from breaking on a rename.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
custom-1f54c19 was amd64 only, so the job worked on GitHub runners but
forced emulation for anyone running it on an arm64 machine.

custom-c957999 is a manifest list covering linux/amd64 and linux/arm64.
Verified: the arm64 variant runs natively here and the rotation passes
against it.

Assisted-by: Claude Fable 5.1
Signed-off-by: xgreenx <xgreenx9999@gmail.com>
@xgreenx

xgreenx commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #9, which is now a single PR against main containing this branch's commits unchanged. Branch kept.

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