Skip to content

feat(learning): invite-key classroom enrolment; identity and guard hardening - #8

Merged
SaulBuilds merged 8 commits into
mainfrom
fix/pba-r2-sdk-classroom
Sep 25, 2026
Merged

SaulBuilds merged 8 commits into
mainfrom
fix/pba-r2-sdk-classroom

Conversation

@SaulBuilds

@SaulBuilds SaulBuilds commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

This PR aligns the SDK with citrate-chain and adds some hardening. Details are in the private audit record.

Item Change Tests Mutation Status
ClassroomRegistry parity (citrate-chain #222, d89200c2) create() and rotate_invite_code() use an invite key pair and return the secret in last_invite_code. New enroll_with_invite(secret) signs the enrolment locally, binding it to this student, the registry and the chain id, and calls enrollWithInvite(address,bytes). enroll() is deprecated and refuses plain codes. tests/test_classroom_invite_key.py; the parity test is re-pinned to d89200c2 and asserts the removed selectors are gone revert: red (16 failed) FIXED
ID tokens verify_id_token refuses an iat beyond the clock tolerance (parity with JS). tests/test_hardening_round3.py hand mutant killed FIXED
Transport gate Regression tests for the host-agreement check (IPv6 zone-id forms). same hand mutant 'check dropped' killed (4 fail) FIXED
Share guard The structural match needs x in 1..255 and a y of at least 16 bytes, so coordinate-like metadata is accepted again. Share-shaped probes are still refused. same, plus updated probes 4 hand mutants killed FIXED

Existing tests changed: tests that encoded the old text-code classroom flow now use the key-pair flow.

Checks (Python 3.11.16):

  • 679 passed, 4 skipped. The economics suite is unchanged and runs in CI.
  • ruff, mypy and pip-audit are clean.
  • uv lock --check passes.
  • The wheel tripwire passes.

0.6.2 is still unpublished, so there's no bump; the change is noted in CHANGELOG.

Round 4 (hardening)

Item Change Revert check Mutants
Invite-secret validation _invite_account checks 1 <= secret < n itself and maps any key-library error to ValueError, so the result does not depend on the installed eth-keys version. red (6 failed) 5 killed
Share parser / guard parse_share_y is the single strict parser, and reconstruct uses it. The guard's y match is a whitespace-normalised superset, and a property test pins "the guard refuses everything the parser accepts". Integral float x counts as an integer. red (24 failed) 9 killed
Shared vectors tests/fixtures/share_guard_vectors.json is byte-identical to the citrate-sdk-js copy, with the sha256 pinned in both. n/a n/a
Locked environment New locked-env CI job: uv sync --frozen, then ruff, mypy, pytest and pip-audit on the locked set. uv.lock is refreshed for packages pip-audit flagged (eth-keys 0.8.0, eth-account 0.14.0, and others). n/a n/a

Frozen-lockfile local run (Python 3.11.16): 744 passed, 4 skipped, plus the economics suite (52 passed). ruff, mypy, locked pip-audit, uv lock --check and the wheel tripwire are all clean. Before the fixes, the same frozen environment gave 678 passed / 1 failed.

🤖 Generated with Claude Code

https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H

BerryManifold and others added 8 commits September 25, 2026 09:35
…BA-L6b-040 follow-up)

Aligns ClassroomManager with ClassroomRegistry at citrate-chain d89200c2
(#222). The invite is a key pair:
- create() and rotate_invite_code() generate or accept a 32-byte invite
  secret and register keccak256(abi.encodePacked(inviteKey)).
- enroll_with_invite(secret) looks up the teacher, signs
  enrollmentDigest(teacher, student, commitment) locally (EIP-191; bound to
  the registry and the pinned chain id) and calls
  enrollWithInvite(inviteKey, signature).
- enroll() is deprecated. It forwards an invite secret and refuses plain
  codes.

The selector-parity test is re-pinned to d89200c2 and now also asserts
that the removed selectors are not encoded. Existing tests that encoded the
old text-code flow are updated to the key-pair flow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H
…t, tighter share-shape match

- verify_id_token refuses an iat beyond the clock tolerance in the future
  (parity with the JS SDK).
- Transport gate: the host-agreement check gets its own IPv6 zone-id
  regression tests, so dropping it now fails 4 tests.
- The share guard's structural match needs x in 1..255 and a y of at least
  16 bytes (even-length hex or bytes), so coordinate-like caller metadata
  such as {x: 1, y: "10"} is no longer refused. The earlier probes were
  updated to share-length y and are all still refused.
- CHANGELOG notes round 3 under the unreleased 0.6.2.

Hand mutants (all killed): host-agreement dropped, future-iat dropped,
share min length 16->1, x range dropped, bytes min length dropped, x check
dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H
Adds x/y boundary cases for the tightened share-shape match: string and non-int x, 15/16-byte y, and list-valued y.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H
…y of eth-keys

_invite_account checks 1 <= secret < n itself and reports any key-library error as ValueError, so the result no longer depends on the installed eth-keys version (the locked 0.7.0 accepts a zero key; secrets >= n raised a bare Exception). Tests are in tests/test_hardening_round4.py.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H
…ed test vectors

- parse_share_y is the single share-value parser (optional 0x/0X, even-length hex, nothing else). reconstruct_key_from_shares uses it, so whitespace and trailing characters are rejected.
- The deploy guard's y match is a deliberate superset: after removing whitespace, any run of >= 16 bytes of hex digits. A property test asserts that the guard refuses every y the parser accepts.
- An integral float x (1.0) counts as an integer x, matching JS number semantics.
- tests/fixtures/share_guard_vectors.json is shared byte-for-byte with citrate-sdk-js (sha256 pinned in the test). Both guards run the same refuse and accept vectors.
- An odd-length share-sized hex y moves from the accepted to the refused round-3 cases.
Hand mutants (12) all killed; log kept in the lane record.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H
The shared python job installs unpinned dependencies. The new job runs uv sync --frozen and then ruff, mypy and pytest inside it, so CI exercises the same dependency versions as a local frozen install.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H
uv.lock pinned versions of aiohttp, click, idna and urllib3 that pip-audit flags, and eth-keys 0.7.0 / eth-account 0.13.7. Upgraded with uv lock --upgrade-package: aiohttp 3.14.3, click 8.5.0, idna 3.20, urllib3 2.8.0, eth-keys 0.8.0, eth-account 0.14.0, plus their transitive updates. A locked-environment pip-audit is now clean, and the full frozen suite passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H
Adds pip-audit (pinned) over the uv.lock environment, so a vulnerable pin in the lockfile fails CI, not only a vulnerable latest release.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H
@SaulBuilds
SaulBuilds merged commit 3bed9ac into main Sep 25, 2026
10 checks passed
@SaulBuilds
SaulBuilds deleted the fix/pba-r2-sdk-classroom branch September 25, 2026 22:42
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.

2 participants