fix(security): SDK hardening from the pre-bounty audit; 0.6.2 - #6
Merged
Merged
Conversation
…L6b-003)
deploy_model with threshold_shares > 0 put every share of the model AES key
into tx_data["encryption_metadata"]["key_shares"], i.e. public calldata.
Anyone reading the chain rebuilt the key and decrypted the uploaded model.
- encrypt_model refuses threshold_shares > 0; the new
encrypt_model_with_key_shares requires share_holder_public_keys (one
distinct secp256k1 key per share), ECDH-wraps each share to its holder
(existing V2 envelope) and returns the envelopes APART from the public
metadata. deploy_model hands them back on ModelDeployment
.key_share_envelopes for off-chain delivery; the metadata carries only
{threshold, total_shares}.
- KeyManager.unwrap_key_share (holder side, owner-pinned) and
reconstruct_key_from_shares(shares, threshold) with the threshold taken
from the caller, never the share.
- deploy_model runs assert_no_key_share_material over the whole tx payload
(caller metadata included) before signing.
- Variant (PBA-L4-005 twin): finite_field validates every share (integer x
in 1..255, distinct, equal non-empty y), verify_shares now checks extra
shares lie on the polynomial, Lagrange is general in x.
- examples/encrypted_inference.py uses holder keys and off-chain shares.
- Tripwires: scripts/keyshare_leak_tripwire.py (independent GF(2^8));
tests decode the signed raw tx and assert no calldata subset rebuilds the
key; scripts/check_keyshare_wheel.py runs the same against the built
wheel in CI and before publish (fails on the published 0.6.0 wheel).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
…es in the transport gate (PBA-L6b-026)
"\u00a0http://remote" parsed with an empty scheme and passed the gate; requests then stripped the whitespace and sent plaintext to the remote host (all 6 gated clients). The gate now refuses whitespace/control/format characters anywhere, allowlists {https, http}, refuses an empty scheme or host and non-string input, and the gated clients use the URL it returns (identity client hunks land with the PBA-L6b-029 commit).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
…d + factory code (PBA-L6b-027) The verifier posted to any rpc_url (remote plaintext included) and trusted whatever came back, so a MITM or hostile RPC could echo the publicly computable prediction and "do not fund" passed. It now goes through the transport gate (allow_insecure_http opt-in), requires eth_chainId == the pinned chain (chain_id override), and requires non-empty factory code before comparing predictAddress. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
… ABI parity (PBA-L6b-028, PBA-L6b-040) L6b-028: the default invite code was classroom-<ms timestamp>; its keccak is public at createClassroom and the block timestamp bounds the window, so a +/-60 s search recovered it in under a second. Default is now secrets.token_urlsafe(16), exposed to the caller as ClassroomManager.last_invite_code. L6b-040: the SDK still encoded enrollWithCode(bytes32 hash); ClassroomRegistry (CHAIN-B-C009) takes enrollWithCode(bytes rawCode). New parity test pins every ClassroomRegistry selector the SDK encodes to the contract source @ citrate-chain 21726055; it also caught getClassroom, which returns a Classroom struct (one tuple behind an offset) that the SDK decoded as six flat values. Two existing tests that pinned the removed bytes32 ABI are corrected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
…h; match SIWE routes (PBA-L6b-029, PBA-L3a-011/012 twins)
L6b-029: verify_id_token accepted a missing or non-numeric exp (never expired) and ignored typ, so an at+jwt/logout+jwt token from the same key and audience passed as an ID token. exp and iat must be finite numbers (bools rejected) and typ absent or JWT. Existing fixtures gain the iat claim real tokens carry.
Variant sweep, Python twins of the JS findings:
- PBA-L3a-011: refresh(refresh_token, expected_sub) refuses a refreshed ID token naming a different sub.
- PBA-L3a-012: siwe_challenge() is GET /siwe/challenge -> {nonce}; build_siwe_message() builds the EIP-4361 message the authority enforces (40204, Expiration Time <= 24 h, uri host, EIP-55; byte-identical to the JS builder); siwe_verify returns kind=redirect|token (ID token verified) and surfaces the server reason. Contract test transcribes citrate-identity siwe-routes.ts @ 08959bf.
Also routes the identity client through the URL the transport gate returns (PBA-L6b-026).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
…L6b-030) download_bytes buffered the whole body and returned it unchecked. It now streams with max_bytes (default 1 GiB), verifies expected_sha256 when given, and verifies self-describing addresses with no hint (sha256:<hex> pointers and CIDv1 raw/sha2-256). dag-pb CIDs hash a chunked DAG and cannot be recomputed from the bytes; the docstring directs callers to pass expected_sha256 (e.g. the on-chain model_hash) for those. IPFSManager.download and download_from_ipfs thread both parameters. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
…ulting to index 0 (PBA-L6b-031) create_pool(access=...) turned any unrecognised value into an Open pool, post_job(tier='zk') posted a Commitment-tier job while escrowing max_price, and an unknown pool mode became InferencePool. New abi.enum_index accepts the canonical name or a case-insensitive exact match and raises ValueError otherwise, before any transaction. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
…042) The SPY-B-005 pin covered only CitrateClient. The Learning/Staking/Classroom/Compute/Treasury/Farming managers send writes with node-side eth_sendTransaction and never asked which chain the node is on. New _chain_guard.pinned_send asserts eth_chainId against the pinned chain (artifact 40204 by default; chain_id= keyword to override) once per manager, refuses garbage, and puts chainId in the tx so an honest node also rejects a mismatch. Existing manager tests now answer eth_chainId like a real node (tests/_chain_rpc.py) and read the last rpc call instead of the first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
0.6.1 exists only in the repo (PyPI has 0.6.0 alone) and already names another change set, so the fix ships as the next free version, 0.6.2. CHANGELOG carries the advisory (rotate models deployed with threshold_shares > 0; yank 0.6.0) and the breaking-change notes. Not published: release is the owner's call via the publish workflow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
uv.lock locked cryptography 46.0.7 although pyproject requires >=48.0.1, and was missing packages the dev extra pulls in; uv lock --check failed on main. Regenerated with uv 0.12.19 (cryptography now 50.0.1; existing pins kept where still compatible) and added a uv-lock-check CI job (uv lock --check) so the lock cannot drift silently again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
…BA-L6-003 policy) Rebased onto the merge-gate PR (#4), which pins every action to a full commit SHA; the two jobs this branch adds now use the same pins. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
…(PBA-L6b-030) mutmut over the functions this branch changed left survivors on boundaries (exp/nbf tolerance, share-count and threshold edges, guard depth), parameter hand-offs (IPFS manager, wallet RPC, deploy plumbing) and request shapes. tests/test_pba_r2_mutation_hardening.py pins them. One behaviour change: a sha256:<hex> pointer that is not 32 bytes of hex is now refused instead of being treated as unverifiable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P
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.
Pre-bounty remediation R2: SDK lane (citrate-sdk-python)
This PR makes security hardening changes from the 2026-09-24 pre-bounty audit. Finding detail, PoCs and mutation evidence are in the private audit record. The version is bumped to 0.6.2, and there is a security note in
CHANGELOG.md. Publishing is the owner's call.uv.lockregenerated and checked in CIuv lock --checkand the wheel tripwire are green on Python 3.11.CHANGELOG.md.🤖 Generated with Claude Code