feat(learning): invite-key classroom enrolment; identity and guard hardening - #8
Merged
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR aligns the SDK with citrate-chain and adds some hardening. Details are in the private audit record.
create()androtate_invite_code()use an invite key pair and return the secret inlast_invite_code. Newenroll_with_invite(secret)signs the enrolment locally, binding it to this student, the registry and the chain id, and callsenrollWithInvite(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 goneverify_id_tokenrefuses aniatbeyond the clock tolerance (parity with JS).tests/test_hardening_round3.pyExisting tests changed: tests that encoded the old text-code classroom flow now use the key-pair flow.
Checks (Python 3.11.16):
uv lock --checkpasses.0.6.2 is still unpublished, so there's no bump; the change is noted in CHANGELOG.
Round 4 (hardening)
_invite_accountchecks 1 <= secret < n itself and maps any key-library error toValueError, so the result does not depend on the installed eth-keys version.parse_share_yis 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 floatxcounts as an integer.tests/fixtures/share_guard_vectors.jsonis byte-identical to the citrate-sdk-js copy, with the sha256 pinned in both.locked-envCI job:uv sync --frozen, then ruff, mypy, pytest and pip-audit on the locked set.uv.lockis refreshed for packages pip-audit flagged (eth-keys 0.8.0, eth-account 0.14.0, and others).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 --checkand 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