Skip to content

Signed model manifest (M1+M2) + design: attested abliterated models on vast - #59

Merged
wakesync merged 13 commits into
mainfrom
research/signed-model-manifest
Nov 5, 2026
Merged

wakesync merged 13 commits into
mainfrom
research/signed-model-manifest

Conversation

@wakesync

@wakesync wakesync commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

Closes #58. Part of #6, #36, #9.

Draft. Implements threat model mitigations M1 + M2 (PR #54) as a detached ed25519-signed manifest with a client-side verify step next to cp_verify (prover unchanged), and adds a design note answering "vast nodes running proven abliterated models, everything attested, like amiller/dsmack?". CPU only, no GPU spend.

Updated after the second independent review (CHANGES REQUESTED at 94966765, 1 must-fix). New head facd69cd. The must-fix and all four should-fixes are addressed below. The registered threshold did not change; code and wording changed after results, so PREREG has a dated amendment 3 and every number was re-measured at code commit c0d16125 (numbers in 0ac5fca1, ledger in facd69cd). Needs an exact-head re-review.

Second review (at 94966765): how each item was addressed

Must-fix 1. commitment.kind is the signer's word even with --public-weights-seed. Agreed: the flag checks a seed file against the signed digest, never the kind against the commitment. The split is now 11 always (the 10 one-field fields plus commitment.kind) + 3 unless a flag (conversion.public_weights_seed_sha256, upstream.*, conversion.committed_files); total 14 of 37 unchanged.

  • Tests: SIGNERS_WORD_UNLESS_FLAG drops commitment.kind; new SIGNERS_WORD_COORDINATED_ALWAYS = {commitment.kind} and SIGNERS_WORD_ALWAYS (11), asserted in test_coordinated_trusted_resign. The old made-up-digest row now covers only the seed digest.
  • New matching-seed coordinated row (tamper.coordinated, expected exit with the flag = 0): hiding->public-weights with the digest of a seed file the signer ships, --public-weights-seed with that file: ACCEPTED (cp_verify YES), documented as the signer's word. Plus test_public_weights_kind_with_matching_seed_accepted (a different seed file is still refused).
  • Text fixed: manifest.py docstring, tools/manifest/README.md (kind row now says the flag checks the digest, not the kind; Tests paragraph), RESULTS.md (coordinated table + split paragraph + claim), design note (rederivation paragraph), results.yaml (summary; trusted_resign_accepted_unless_flag_fields 4 -> 3; new trusted_resign_accepted_always_fields 11/37), this body and the proposed claim. README block and CLAIMS.md regenerated, not hand-edited.
  • PREREG ## Amendments entry 3, dated 2026-11-05 00:23 UTC, says what changed and why. Earlier lines untouched.

Should-fixes (all done, not just disclosed):

  • Article nit at design note line 81: the review was right, I had checked the wrong line last round. Reworded to "One comparison page from another GPU provider says the same".
  • {"none": 1} / {"none": 1.0}: new is_none() requires exactly {"none": true} (is True), used at every former == {"none": True}. Unit test (1, 1.0, false, "true", null all refused) and 4 runner rows, trusted re-sign: all refused.
  • Symlinked dirs: check_files now walks with os.walk(followlinks=False) and refuses any symlink inside a rehashed dir (file or directory, listed or not) and any non-regular file. Unit test + runner row: extra added_tokens.json in a symlinked subdir of --tokenizer-dir -> refused (sub is a symlink). Side effect, disclosed in RESULTS: a caller whose tokenizer/checkpoint is a symlink tree (HF cache) must copy or dereference first.
  • RecursionError: load catches it and refuses (nested too deeply to parse). Unit test (100k nested lists and objects) + runner row: refused, exit 1, no traceback.

Ledger, at facd69cd:

  • python3 scripts/update-results.py: update-results: wrote README block and CLAIMS.md (20 results, 102 metrics)
  • python3 scripts/update-results.py --check (exit 0): update-results: ok (20 results, 102 metrics, README and CLAIMS.md in sync)
  • python3 scripts/check-claims.py (exit 0): check-claims: ok (20 claims, 14 unreplicated, README block and CLAIMS.md agree, evidence paths exist, replicated_by resolves, 44 public .md files scanned)
  • scripts/check-no-host-paths.sh HEAD: check-no-host-paths: ok

First review (at 078d7429): how each was addressed

  1. results.yaml entry. Added signed-manifest (measured, unreplicated, pr: 59) with 5 metrics, all source: experiments/20261104-58-signed-manifest/numbers.json at commit 2699b0a0: registered_tampers_refused 137/137, untampered_accepted 10/10, trusted_resign_accepted_fields 10/37, trusted_resign_accepted_unless_flag_fields 4/37, verify_wall median 1.414 s (SD 0.026, min 1.383, max 1.452, n=10). README block and CLAIMS.md regenerated.
    • python3 scripts/update-results.py: update-results: wrote README block and CLAIMS.md (20 results, 101 metrics)
    • python3 scripts/update-results.py --check (exit 0): update-results: ok (20 results, 101 metrics, README and CLAIMS.md in sync)
    • python3 scripts/check-claims.py (exit 0): check-claims: ok (20 claims, 14 unreplicated, README block and CLAIMS.md agree, evidence paths exist, replicated_by resolves, 44 public .md files scanned)
  2. ed25519 small-order points. verify now refuses a public key A or commitment R with [8]A or [8]R the identity. New tests: all 8 small-order encodings recognized; identity key + R=[r]B, S=r refused for several messages (accepted by the old code, checked); each of the 8 small-order points as A with all 8 forgeries R=[r]B+T, S=r refused; each of the 8 as R under a real key refused (identity R satisfies the bare equation and was accepted before); manifest.load refuses the identity-key forgery. RFC 8032 3/3 and cryptography 64/64 still pass. RESULTS.md and the README state verification is cofactorless.
  3. Bound vs signer's word. The runner now reports which of the 27 one-field refusals come from the schema alone: 10 (format, commitment.kind, upstream.kind, conversion.committed_files, conversion.public_weights_seed_sha256, decode_policy.{kind,C,theta,cap,sT}); 17 are bound. New coordinated arm (tamper.coordinated, 12 schema-valid alternates re-signed by the trusted key): kind hiding->public-weights + made-up seed digest, full fake huggingface upstream + made-up committed_files, the same with --upstream-dir only, and made-up committed_files under synthetic are all ACCEPTED without the rehash flag and refused with --public-weights-seed / --upstream-dir / --committed-dir; decode-policy and serving alternates are refused without any extra flag. Tests pin SIGNERS_WORD_SINGLE (10), SCHEMA_ONLY (10) and SIGNERS_WORD_UNLESS_FLAG (commitment.kind, upstream.kind, conversion.committed_files, conversion.public_weights_seed_sha256). README, RESULTS, manifest.py docstring, the design note and the claim now say those are the signer's word unless the matching flag is passed: 14 of 37 fields on this fixture.
  4. Omission gap. Closed, not just disclosed: --end-tokens is required, and the caller must pass --recipe FILE or an explicit --no-recipe (exactly one). Measured: end tokens left out -> refused; recipe left out -> refused; both -> refused; --no-recipe -> accepted, including a changed script_sha256 re-signed by the trusted key (disclosed: under --no-recipe the digest is the signer's word). The "cannot skip a check by omission" sentence is rewritten: it covers the serving and recipe flags; the three rehash flags stay optional and their fields are listed as the signer's word.
  5. PREREG times. Dated note under ## Amendments: registration committed 22:16:43Z (not 22:25), first amendment committed 22:31:55Z (not 23:20). Registration line untouched. (My own amendment entries first said "2026-11-05" by mistake; fixed in aeb23646 and noted in the entry.)

Should-fix: design note "measured below" now points to RESULTS.md. The article nit does not apply to the committed bytes: in line 80 the word after "A" starts with a consonant (checked byte-wise with od), so that line is unchanged. Added the signer's-word sentence next to the rederivation chain.

Part A: tools/manifest/ (measured, unreplicated)

Manifest binds commitment ID, upstream checkpoint (HF repo + 40-hex revision + file sha256s, or synthetic generator), conversion recipe (script sha256, repo commit, version, params, committed files, public-weights seed digest), policy.txt/KL.bin/layout.txt digests, tokenizer / chat template / system prompt (digest or explicit none) + end tokens, decode policy, cp_verify build digest. verify takes the signer key from the caller, rehashes everything the caller holds, runs real cp_verify with the manifest's ID and matches result-v1 (id, build, policy limits).

PREREG experiments/20261104-58-signed-manifest/PREREG.md (first commit, amendments 1-3 dated). Results RESULTS.md, raw numbers.json (re-run at c0d16125, committed in 0ac5fca1; all 174 registered rows identical to the amendment 2 run):

  • registered tampers refused: 137/137 (37 fields x 3 signature arms, 15 file substitutions, 10 sig bit flips, non-canonical JSON). Threshold 100%: PASS
  • untampered: 10/10 accepted, cp_verify YES. Threshold 10/10: PASS. Verify call median 1.407 s (SD 0.027, min 1.380, max 1.471, n=10), stored small example, shared 16-vCPU EPYC VM (amendment 2 run: 1.414 / 0.026 / 1.383 / 1.452; first run: 1.44 / 0.10 / 1.40 / 1.75)
  • RFC 8032 vectors 3/3 (PASS); 64/64 identical signatures vs cryptography 41.0.7
  • small-order: identity-key forgery refused; 8/8 small-order keys refuse all forgeries; 8/8 small-order R refused
  • trusted re-sign (no threshold): one-field 27 refused (17 bound, 10 schema-only) / 10 accepted; coordinated 13 alternates: 5 accepted without the rehash flag; with it, 4 refused and the matching-seed commitment.kind row accepted (as expected); split 11 always + 3 unless a flag = 14/37
  • hardening (no threshold): symlinked-subdir extra tokenizer file, 100k-deep manifest, {"none":1} and {"none":1.0} x 2 fields: 6/6 refused
  • omission rows unchanged
  • python3 -m pytest -q tools/manifest: 61 passed

Measured, unreplicated.

Part B: research/security/DESIGN-VAST-ABLITERATED.md

Proposed claim (ledger entry signed-manifest added per TICK standard 8; nothing in loop/claims.json)

  • "tools/manifest refuses 137 of 137 registered tampers and accepts 10 of 10 untampered verifies of the stored small example; on that fixture 14 of 37 fields are the signer's word (11 always, including commitment.kind, and 3 unless the caller passes the matching rehash flag)." (measured, needs replication)

Open risks

Detached binding only (not in-statement). Pure-Python signing not constant time (tests only). Verification is cofactorless; cofactored verifiers may disagree on edge cases. No real model/tokenizer exercised; rehash flags tested on synthetic files only. Code changed after results (amendments 2 and 3); the threshold did not. commitment.kind is only checkable by rederivation (public-weights, H100). Rehashed dirs refuse symlinks, so HF-cache layouts must be dereferenced first. Abliteration determinism untested. Vast CC finding rests on vast's 2024 article + absence of evidence.

@wakesync

wakesync commented Nov 4, 2026

Copy link
Copy Markdown
Owner Author

Independent review (adversarial), PR #59 at 078d7429ede8d319d0c3c06ac525558340ace93a

Verdict: CHANGES REQUESTED. The registered numbers reproduce, the standards are mostly met, and the design note is labeled honestly. But adversarial probing turned up a real ed25519 gap, an overstated "bound vs signer's word" split, two checks a client skips just by leaving out a flag, and no results ledger entry. Five must-fixes below, all cheap, CPU only.

Fresh clone, head checked at start and again before posting (unchanged), verifier/build.sh on a shared 16-vCPU EPYC VM, nice -n 19, no GPU, no paid APIs.

1. Standards

  • PREREG first and alone: yes. 7930e5b touches only PREREG.md. Code is in 95b1260, results are in 078d742. The only later PREREG change is the dated entry under ## Amendments (diff checked).
  • The threshold as registered (100% of tampers, 0/10 false refusals, RFC vectors, cp_verify YES) matches the PASS verdict. The trusted re-sign arm is correctly kept outside the threshold.
  • ENV.json has commits, toolchain, hardware and seeds. n=10 timing with median/SD/min/max, and it is informational only. OK.
  • Timestamps are wrong (must-fix 5). The registration line says 22:25 UTC, but its commit is 22:16:43Z. The amendment says "23:20 UTC", but the commit containing it is 22:31:55Z.

2. Re-run (agree)

  • pytest -q tools/manifest: 31 passed.
  • run.py gave: registered arms 137/137 refused, untampered 10/10 accepted (cp_verify YES), trusted re-sign arm 27 refused / 10 accepted with the identical 10-field set, RFC 8032 3/3, cryptography 41.0.7 cross-check 64/64.
  • Refusal reasons per row match numbers.json category by category.
  • Verify wall time: median 1.42 s, SD 0.04, min 1.39, max 1.52 (n=10). The PR has 1.44 / 0.10 / 1.40 / 1.75. The ranges overlap and the numbers agree; timing has no threshold.
  • All numbers in RESULTS.md, numbers.json and the PR body agree with the re-run.

3. Crypto and encoding, adversarial

Holds:

  • S >= L is rejected (S+L variant refused).
  • Non-canonical point encodings are rejected (y >= p, and x = 0 with the sign bit set).
  • Wrong-length key and signature are rejected.
  • The signature is checked over the exact file bytes (domain-prefixed) before parsing.
  • Canonical JSON holds: a duplicate key, an uppercase \u00E9 escape, a missing trailing newline and a pretty-printed form all refuse ("not in canonical form").
  • An unknown top-level key refuses. A float in conversion.params refuses.
  • Manifest/proof binding holds. A commitment ID swap ends NO: model-statement inside cp_verify, and result-v1 id= is matched again. Pairing a valid manifest with a proof for other weights did not pass in any probe.

Breaks:

  • Small-order public keys are accepted. With the identity point as public key (0100..00), the signature R = [r]B, S = r verifies for any message (probe: verify(identity, b"any message at all", forged) == True). The cofactorless check [S]B = R + [k]A cannot catch an A of small order. In this tool the key comes from the caller, so the exploit needs a registry or pinned file to hold a small-order key. That is exactly the "keys from a registry" path the design note proposes, and RFC 8032 implementations commonly reject such keys.

  • Coordinated re-signs by the trusted key that the "27 bound" count hides. In the amendment arm, several of the 27 refusals are schema refusals of nonsense values, not bindings: format, commitment.kind (to "hidingx"), upstream.kind, conversion.committed_files[0] (to [0]), conversion.public_weights_seed_sha256 (null to 64 zeros under hiding). Valid alternate values pass:

    • commitment.kind: hiding -> public-weights plus a made-up public_weights_seed_sha256 was ACCEPTED with real cp_verify YES. This is the field that decides whether rederivation is even possible, and the client cannot tell.
    • A full fake huggingface upstream (Maverick repo name, zero revision, invented file digests) plus invented committed_files was ACCEPTED.

    These are signer's-word fields unless --upstream-dir/--committed-dir/--public-weights-seed are passed. The README hints at that, but RESULTS.md, the pinned SIGNERS_WORD set and the proposed claim ("10 of 37 fields are the signer's word only") understate it.

  • Opt-in checks. If the manifest says end_tokens: [2] and the client omits --end-tokens, the result is ACCEPTED. A wrong conversion.script_sha256 with --recipe omitted is also ACCEPTED. Two of the 15 registered file substitutions ("end tokens differ", "recipe") refuse only because the harness passes those flags. The README line "a client cannot skip a check by omission" is true for the {"none": true} fields only.

4. Claims vs evidence

  • The 137/137 denominator is described as registered tampers, not all attacks. OK.
  • The amendment arm's 10 accepted fields are disclosed as signer's word only, but the set is incomplete (see above).
  • Design note:
    • The measured/cited/inferred labels are correct.
    • The vast confidential-computing finding is framed as vast's own 2024 article plus absence of evidence, with the competitor source flagged as weak. Good.
    • It explicitly does not claim refusals are gone or that abliteration is safe.
    • Cost rows are inferred from cited upstream rates times our measured vast prices, with no new measured claim. OK.
    • Nits: "about 1.4 s for the small example, measured below" (the measurement is in RESULTS.md, not below), and "A external comparison page".

5. Results ledger

  • update-results.py --check and check-claims.py exit 0 at the head, and also after the trial merge.
  • The PR reports a measured PASS but adds no research/results.yaml entry. It deliberately parks it as a "proposed claim". Under TICK.md standard 8 the lane is not done without one (must-fix 1).

6. Merge and hygiene

  • Trial merge onto origin/main eb6e6ea (equal to the merge-base): clean, no conflicts.
  • check-no-host-paths.sh HEAD: ok. CI claims and guard: green.
  • All 3 commits are authored and committed by wakesync noreply. No 0xSolace, no trailers.

Must-fix

  1. Add a research/results.yaml entry for the manifest result:
    • measured, unreplicated
    • metrics: registered tampers refused (count, total 137), untampered accepted (10/10), trusted re-sign accepted fields (count), verify wall median with spread and n=10
    • source = numbers.json at its commit
    • Then run update-results.py, --check and check-claims.py, and paste both output lines in the PR body.
  2. ed25519: reject small-order public keys (and small-order R). For example, refuse if [8]A is the identity. Add a test with the identity key and the forged R=[r]B, S=r signature, plus at least one other small-order point. RFC vectors and the cryptography cross-check must still pass.
  3. Fix the bound vs signer's-word split.
    • Run the trusted re-sign arm with schema-valid alternates for fields with a closed value set or an internal constraint (commitment.kind with a matching seed digest, upstream kind swap, non-empty committed_files). Alternatively, report which of the 27 refusals were schema-only.
    • Update SIGNERS_WORD, RESULTS.md, the README and the proposed claim so that commitment.kind, upstream.*, conversion.committed_files and conversion.public_weights_seed_sha256 are listed as the signer's word unless the corresponding --*-dir/--public-weights-seed is passed.
    • Since this changes code and numbers after results, add a dated amendment and re-run.
  4. Close the omission gap or disclose it. Make --end-tokens required, and require --recipe or an explicit --no-recipe. Alternatively, state in README/RESULTS that these two checks are opt-in and that 2 of the 15 registered substitutions depend on the caller passing them. Correct the "cannot skip a check by omission" sentence.
  5. Correct the PREREG times with a dated note under ## Amendments, without rewriting the registration line. Registration was committed 22:16:43Z, not 22:25. The amendment entry was committed 22:31:55Z, not "23:20 UTC".

Should-fix (not blocking): the two design-note wording nits in section 4. Also consider stating in RESULTS.md that ed25519.verify uses the cofactorless equation, so cofactored verifiers (e.g. some libsodium modes) may disagree on edge-case signatures.

Re-review needed at the new head after the fixes. This PR is a draft, and un-drafting it is Shadow's call.

…tokens and --recipe/--no-recipe, coordinated trusted re-sign arm, PREREG amendment 2 and time correction (#58)
…137, small-order refused, 14 of 37 fields signer's word) (#58)
…licated), regenerate README block and CLAIMS.md (#58)
@wakesync

wakesync commented Nov 4, 2026

Copy link
Copy Markdown
Owner Author

Independent re-review at exact head 949667653bddd8bd92283b41b2755d1db6452372: CHANGES REQUESTED (1 must-fix)

Head was unchanged at the start and again before posting. Fresh clone, cp_verify built from verifier/build.sh (build ea191b01...bd66, the same as ENV.json). CPU only, nice -n 19. I did not write the PR or the fixes.

Prior must-fixes (from the review at 078d7429)

  1. results.yaml entry: fixed. signed-manifest is measured and unreplicated, with 5 metrics. Each metric matches numbers.json at 2699b0a0 (137/137, 10/10, 10/37, 4/37, median 1.414 s with SD 0.026, min 1.383, max 1.452). numbers.json is byte-identical between 2699b0a0 and the head. The caveat is must-fix 1 below: the 4/37 metric and its summary mean something narrower than the text says.
  2. ed25519 small-order: fixed. ed25519.py:114-136 refuses A and R when [8]P is the identity. My own check: I derived the torsion subgroup independently as [L]Q for random Q and got exactly the 8 encodings in the test list. All 192 forgeries were refused (8 small-order A x 8 torsion offsets x 3 values of r, S = r). All 8 small-order R with S = k*a under a real key were refused. All 40 non-canonical encodings (y in [p, 2^255) with both sign bits, plus x = 0 with sign 1) fail to decode, both as A and as R. S >= L is refused 4/4 (S+L, S+2L, 2^256-1, L). RFC 3/3 and cryptography 64/64 still pass.
  3. Signer's-word split: fixed, but one item is still wrong. The split is now SINGLE 10, UNLESS_FLAG 4, SCHEMA_ONLY 10, and the tests pin all three sets. The figure "14 of 37" is the same in RESULTS, the CLAIMS/README row (as "10 + 4"), the PR body and the docstrings. The remaining problem is must-fix 1.
  4. Omission: fixed. manifest.py:291-292 requires --end-tokens, and 297-301 requires exactly one of --recipe / --no-recipe. These checks are on the verify path, which is the only path that takes these flags (sign does not check files). Test: test_omitting_end_tokens_or_recipe_refused. The omission rows in numbers.json match.
  5. PREREG times: fixed. There is a dated correction under ## Amendments: 22:16:43Z and 22:31:55Z, matching git log. The registration line is untouched. The 11-05 date slip is disclosed.

Prior should-fixes: cofactorless verification is now stated (RESULTS:42, README:31). The "measured below" pointer is fixed. The article nit is still there. It was never on line 80. It is on line 81 of DESIGN-VAST-ABLITERATED.md: "A external comparison page" should be "An external comparison page".

Standards

  • PREREG 7930e5b0 is the first commit and touches only PREREG.md.
  • The post-result code change is disclosed as amendment 2, and the threshold is unchanged.
  • ENV.json has commits, toolchain, hardware and seeds.
  • Perf n = 10, reported as median, SD, min and max.
  • Identity: all 7 commits are authored and committed by wakesync noreply, with no trailers.

Re-run (measured, unreplicated, shared 16-vCPU EPYC VM)

  • pytest tools/manifest: 52 passed. Agree.
  • run.py: 137/137 registered refused, 10/10 untampered (cp_verify YES), trusted one-field 27 refused / 10 accepted (17 bound, 10 schema-only), coordinated 4 alternates accepted without the flag and refused with it, omission rows, RFC 3/3, 64/64, small-order 8/8 and 8/8. 0 row-level differences from the committed numbers.json, and the coordinated and omission arms are identical. Agree.
  • Verify wall time, n = 10: mine median 1.396 s, SD 0.014, min 1.369, max 1.417. PR: 1.414 / 0.026 / 1.383 / 1.452. Agree; this is informational, with no threshold.

Ledger and merge

  • update-results.py --check exits 0 and check-claims.py exits 0 at the head. Both also pass after a local trial merge onto origin/main eb6e6ea3, which is clean with no conflicts.
  • check-no-host-paths.sh HEAD: ok.
  • CI claims and guard: pass.

Must-fix

  1. commitment.kind is not bound by --public-weights-seed. It is the signer's word even with the flag. The flag rehashes a seed file against conversion.public_weights_seed_sha256 (manifest.py:310-313). Nothing compares that against what the commitment actually is.
    • What I ran: on the stored hiding example, the trusted signer re-signs kind: public-weights with public_weights_seed_sha256 set to the digest of a seed file that the signer also ships. The caller passes --public-weights-seed with that file. Result: ACCEPTED, cp_verify YES.
    • Why the test misses it: the coordinated arm only uses a made-up digest that does not match the caller's file, so it shows that a mismatched digest is refused. It does not show that the kind is checked.
    • Effect on the claim: only rederivation detects a false public-weights kind. So commitment.kind belongs with the always-signer's-word fields: 11 always and 3 unless a flag is passed. The total of 14 does not change.
    • Text to fix: SIGNERS_WORD_UNLESS_FLAG and its test comment; RESULTS:63 and 71-75; the manifest.py docstring (23-25); the design note at 133-135; the trusted_resign_accepted_unless_flag_fields metric (value and note) and the results.yaml summary; the PR body; and the proposed claim. Then regenerate the README and CLAIMS.md.
    • tools/manifest/README.md:11 already says this correctly ("nothing checks a claimed hiding"). It should also say that the flag checks the digest, not the kind.
    • Add a coordinated row with a matching seed (expected: accepted) so the test pins this.
    • This is a dated amendment-3 wording and test change. The registered threshold is unaffected.

Should-fix (not blocking)

  • {"none": 1} and {"none": 1.0} are canonical JSON, and the trusted signer can sign them. obj == {"none": True} (manifest.py:121, 180, 272, 282) accepts both as "none". This only matters for the signer, because an attacker cannot sign. Still, a value meaning "none" should have one encoding: compare with is True.
  • check_files uses rglob, which on Python 3.12 does not descend into symlinked directories. With a symlinked subdirectory holding added_tokens.json inside --tokenizer-dir, the run was ACCEPTED. A plain dotfile is refused. Either refuse symlinks in rehashed directories or disclose this.
  • A trusted-signed manifest nested about 100k deep raises an uncaught RecursionError (a traceback, not REFUSED, exit 1). An attacker-signed one is refused at the signature check first. Catch it in load.
  • Canonical-JSON smuggling checked and closed: duplicate keys, \u escaped keys, trailing whitespace and 24.0 for an int are all refused, and NaN in params is refused.
  • The design note's measured/cited/inferred labels look right to me.

Verdict: CHANGES REQUESTED at 949667653bddd8bd92283b41b2755d1db6452372. One must-fix, which is a claim/evidence wording change plus a test row. The numbers themselves reproduce. After the fix this needs another exact-head check, which can be a narrow one. This PR is a draft; un-drafting it is Shadow's call.

…ic-weights-seed (11 always + 3 unless a flag), matching-seed coordinated row, refuse symlinks in rehashed dirs, one encoding for none, refuse deep nesting, PREREG amendment 3 (#58)
…137, 14 of 37 fields signer's word: 11 always incl. commitment.kind, 3 unless a flag; hardening rows refused) (#58)
… 3 unless a flag), regenerate README block and CLAIMS.md (#58)
@wakesync

wakesync commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Third independent review (exact head): APPROVED at facd69cd0451583b323c4e88ea303e338a8fc9e1

Reviewed exact head facd69cd0451583b323c4e88ea303e338a8fc9e1 (checked at start and again before posting). The result is measured, unreplicated on CPU with the stored synthetic example, so it is not a headline. The PR is still a draft, so un-drafting is Shadow's call. Merge command once it is un-drafted:

gh pr merge 59 --merge --match-head-commit facd69cd0451583b323c4e88ea303e338a8fc9e1

Prior must-fix and should-fixes (review at 94966765): all fixed

  • Must-fix: commitment.kind is the signer's word even with --public-weights-seed. Fixed. I re-ran the attack myself: hiding->public-weights, re-signed by the trusted key, with the digest of a seed file the signer ships. Result: ACCEPTED both without the flag and with --public-weights-seed, and cp_verify YES. It is now classified as the signer's word everywhere:
    • code sets: SIGNERS_WORD_COORDINATED_ALWAYS = {commitment.kind} and SIGNERS_WORD_ALWAYS (11 fields); SIGNERS_WORD_UNLESS_FLAG (3 fields). test_coordinated_trusted_resign asserts 11 / 3 / 14.
    • test_public_weights_kind_with_matching_seed_accepted also checks that a different seed file is refused.
    • The same wording is in the manifest.py docstring, the tools/manifest/README.md kind row and Tests paragraph, RESULTS (coordinated table rows 69-70, split paragraph, claim), the design note section 2, the results.yaml summary with always=11/37 and unless_flag=3/37, the README block, CLAIMS.md and the PR body.
    • The old made-up-digest row now covers only the seed digest, which is correct.
  • Design note article: fixed ("One comparison page from another GPU provider").
  • {"none":1} / {"none":1.0}: fixed. is_none requires is True. I checked True, 1, 1.0, "true", "True", 0, False, null, [true], {"x":1} and an extra key: only {"none": true} counts as none. The trusted re-sign runner rows are refused (4/4).
  • Symlinked tokenizer subdir: fixed. I checked each case:
    • dir symlink to an outside dir: refused
    • self-loop dir symlink: refused
    • file symlink: refused
    • listed file as a symlink: refused
    • broken symlink: refused
    • FIFO: refused ("not a plain file")
    • unlisted hardlink: refused ("present but not listed")
    • listed file hardlinked to an outside inode with matching content: accepted, which is fine because the content is what is bound
  • RecursionError: fixed. Lists nested 500/900/990/995/1000/1010/1100/5000 deep and objects nested 990/1000/1100 deep parse and are refused by the schema. Lists nested 10^6 deep are refused with "nested too deeply to parse". Wide input (a 2M-element list, a 200k-key object) is refused cleanly. No traceback in any case.

Standards

  • PREREG.md is still the first commit (7930e5b0) and alone in it.
  • Amendment 3 is dated 2026-11-05 00:23 UTC, which matches the c0d16125 commit time 00:23:29Z. It sits after the existing lines, and the diff against 94966765 adds lines only. Threshold, the 137 registered tampers and n = 10 are unchanged.
  • The disclosure is accurate. It names the review head, says code changed after results, and correctly explains why the amendment 2 row proved less than claimed (it used a mismatched digest).
  • ENV.json now has measured_at = c0d16125, the amendment 2 measurement kept as history, verifier build ea191b01...bd66 and toolchain. It is complete for this re-run.
  • Perf: n = 10, with median, SD, min and max reported.

Re-run at the head (CPU, nice -n 19, shared 16-vCPU VM, my build ea191b01...bd66, same digest)

  • pytest tools/manifest: 61 passed. This includes the S>=L, non-canonical-JSON and small-order tests. ed25519.py is unchanged since 94966765.
  • run.py exit 0. A field-by-field diff of my numbers.json against the committed one shows 0 differences outside wall-time values:
    • 137/137 registered tampers refused
    • 10/10 untampered accepted
    • RFC 8032 3/3
    • cryptography cross-check 64/64
    • small-order 8/8 keys and 8/8 R, identity forgery refused
    • one-field re-sign: 27 refused (17 bound, 10 schema-only), 10 accepted
    • coordinated: 13 alternates, 5 accepted without the flag; with the flag, 4 refused and the matching-seed kind row accepted; coordinated_with_flag_unexpected = []
    • hardening 6/6 refused
    • omission rows unchanged
  • I diffed against the amendment 2 numbers.json (2699b0a0). All 174 rows are identical. The only changes are the coordinated arm (the new row and the narrowed covers), the new hardening keys and timings. Agree.
  • Verify wall time, mine: median 1.426 s, SD 0.024, min 1.399, max 1.471 (n = 10). The PR reports 1.407 / 0.027 / 1.380 / 1.471. The ranges overlap and there is no threshold. Agree with "about 1.41 s".
  • --end-tokens and --recipe / --no-recipe are still enforced on verify (omission rows refused). sign takes no serving flags, as before.

Ledger, claims, merge, identity

  • update-results.py --check: ok (20 results, 102 metrics). check-claims.py: ok (20 claims, 14 unreplicated). Both exit 0 at the head.
  • Every new or changed results.yaml metric matches numbers.json at 0ac5fca1: 137/137, 10/10, 10/37, always 11/37, unless_flag 3/37, verify median 1.407 (SD 0.027, min 1.380, max 1.471, n 10). The README diff is only the generated block.
  • The HF-cache symlink limitation is disclosed in RESULTS ("Hardening").
  • Trial merge onto origin/main eb6e6ea3 (local, not pushed): the head already contains it, so the merge is clean with no conflicts, and ledger and claims are still ok after it.
  • check-no-host-paths.sh HEAD: ok. CI claims and guard: pass.
  • All 10 commits are authored and committed by wakesync noreply. No 0xSolace, no trailers.

Should-fix (non-blocking, follow-up issue is fine)

  1. check_files fails open on an unreadable subdirectory. os.walk ignores errors by default. If a rehashed dir contains a subdir the verifying user cannot read, an extra file inside it is ACCEPTED: I reproduced this with a chmod-0 subdir holding an extra file. This is narrow, because it needs the caller's own input dir to have a subdir the caller can't read while the server can. But it breaks the "nothing else is there" wording. Fix: pass onerror so the walk raises a Refusal, and add a test.
  2. check_files with an empty entry list accepts a missing root or a root that is a regular file. For example, --committed-dir /nonexistent passes for a synthetic upstream with committed_files: []. Better to refuse when root is not an existing directory.
  3. Nit: the schema accepts a path like ./tokenizer.json, which can then never match. This fails closed, but _file_list could refuse . segments and empty segments too.

Open risks, unchanged and already disclosed: commitment.kind is checkable only by rederivation (public-weights, H100), and that has not been done. Also unchanged: the binding is detached only, verification is cofactorless, signing is not constant time, and the rehash flags are tested only on synthetic files.

… word even with --upstream-dir, verify is exact-match only, NEAR TEE source, #54 merged, #51 cite, 93x and reply-cost caveats (#58)
@wakesync

wakesync commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Independent review, PR #59: APPROVE at 26a3afd96012c29dc4e0cce35aacac64d345bbe5 (docs-only fix commit on top of facd69cd)

I did not write this PR. I reviewed facd69cd from a fresh clone, then pushed one small docs commit (26a3afd9: design note + tools/manifest/README.md only, no code, numbers or ledger changes). CPU only, nice -n 19, no GPU, no paid APIs. cp_verify was built with verifier/build.sh and gave build ea191b01...bd66, the same as ENV.json.

Verdict: APPROVE. I found no blocking security issue. The registered numbers reproduce exactly. The ed25519 code matches RFC 8032. verify takes the signer key only from the caller and runs the real cp_verify. I found two places where the text claimed more than the code does, plus one gap that fails closed. All three are fixed in text in 26a3afd9; the code follow-ups are listed below. Once it is un-drafted (Shadow's call):

gh pr merge 59 --merge --match-head-commit 26a3afd96012c29dc4e0cce35aacac64d345bbe5

1. Re-run (agree)

  • pytest tools/manifest: 61 passed. The 31 in the first review was the count at 078d7429; 61 is right for this head.
  • run.py exit 0. Diffing my numbers.json against the committed one field by field gives 0 differences outside wall-time fields:
    • registered tampers refused 137/137, untampered accepted 10/10 (cp_verify YES)
    • RFC 8032 3/3, cryptography cross-check 64/64, small-order 8/8 + 8/8 + identity forgery refused
    • one-field trusted re-sign: 27 refused (17 bound, 10 schema-only) and 10 accepted. Coordinated rows: 5 accepted without a rehash flag; with the flag only the matching-seed kind row is accepted. Hardening 6/6 refused, omission rows unchanged.
  • My verify wall time: median 1.416 s, SD 0.060, min 1.369, max 1.566, n = 10. The PR says 1.407 / 0.027 / 1.380 / 1.471. The ranges overlap and there is no threshold. Agree.
  • check-claims.py ok (20 claims), update-results.py --check ok (102 metrics), check-no-host-paths.sh HEAD ok, merge-tree onto main 672de049 is clean.

2. ed25519 vs RFC 8032 (agree)

  • I typed in RFC 8032 section 7.1 TEST 1, 2, 3 and TEST SHA(abc) myself, without copying the PR's table. All 4 pass: public key, signature bytes and verify all match. All 4 refuse a changed message and a flipped S bit.
  • I ran 300 random keys and messages against cryptography 41.0.7. All 300 give identical public keys and signatures. For each one I also flipped one signature bit, and both libraries gave the same accept/refuse answer.
  • S+L is refused. The code checks lengths and S < L, requires y < p and x = 0 with sign 1 to fail decoding, and refuses small-order A and R (as the second review checked). Verification is cofactorless and the docs say so.

3. Signer key and real cp_verify (agree)

  • --signer-key is required. load() checks the signature under the caller's key before parsing the JSON. Only after that does it require signer.public_key == the caller's key. The key inside the manifest is never used to verify.
  • run_cp_verify runs the caller's cp_verify binary as a subprocess with PATH=/usr/bin:/bin and -id set to the manifest's ID. It requires exit 0, a final YES, result-v1 status=0, and id=, build= and C/theta/cap/s/sh/sT/F plus policy= that all match the manifest. I checked by hand that the stored example's result-v1 line carries all of these fields.

4. The "signer's word" list (10 one-field, 14 total of 37)

On the fixture the list is complete. I enumerated all 37 leaves and agree with the 17 bound / 10 schema-only / 10 accepted split, and with 11 always + 3 unless a flag. It is fixture-specific, though, and the docs said more than the code does for a Hugging Face upstream:

  • upstream.repo and upstream.revision are the signer's word even with --upstream-dir. check_files rehashes file contents and has no idea where they came from. Repro: an HF manifest whose repo and revision were changed and re-signed by the trusted key was ACCEPTED with --upstream-dir (and --committed-dir). The README row said --upstream-dir covers "kind, repo, revision, files". Fixed in 26a3afd9 (README row and design note section 2.1).
    • Not fixed: the manifest.py docstring and RESULTS.md line 78 still say upstream.* is checked "unless a flag". Please narrow both to upstream.kind and upstream.files.
  • For an HF or public-weights manifest the number of leaves (and so the 14/37 ratio) changes. The text already says "on this fixture", which is right. Keep it that way in any README claim.
  • Missing, but disclosed rather than wrong: the tool checks the template, system prompt and tokenizer files. It does not check that example.req was rendered with them. The design note already says this holds only "if the client rebuilds the window itself".

5. Canonical JSON (agree, nits)

The canonical check is a byte-exact round trip, which is strict. These are all refused: no trailing newline, CRLF, leading space, a BOM, raw non-ASCII, a \u0074 escape where a plain ASCII character is canonical, NaN, a float or bool where an int is required, and duplicate keys (they re-serialize to different bytes).

  • Nit 1: conversion.params values and keys have no size limit (_str's 4096 cap is not applied). An int of 10^400 or a 100k-character string is accepted and signed. Python reproduces these exactly, but a JS or Go re-implementation of "canonical" would not (ints above 2^53). Bound params ints to [0, 2**53) or 2**63, and cap string and key length.
  • Nit 2 (from the third review, still open): ./tokenizer.json is accepted by _file_list. It fails closed, but empty and . segments should be refused.

6. Design note claims vs sources

Every source I checked is below with its result. Fixes are in 26a3afd9.

7. Functional gap: clipped-gap manifests can never verify (fails closed)

run_cp_verify never passes -policy and requires a final YES. A clipped-gap cp_verify acceptance ends in WITHIN LIMITS, and without -policy it ends in NO: policy.

  • Repro: an honest, trusted-signed clipped-gap manifest for examples/accepted-proof-clipped-gap (limits copied from its policy.txt) gives REFUSED: cp_verify: exit 1, last line ['NO: policy']. Run directly with -policy clipped-gap,9244130,323343,1,16,8, the same proof is WITHIN LIMITS.
  • So the schema accepts clipped-gap and RESULTS says decode-policy fields are "bound", but no clipped-gap manifest can pass. The design note's reply cost uses the clipped-gap rate. This is safe, because it refuses, but it is a real limitation and is now documented in both files.
  • Follow-up: pass -policy clipped-gap,C,theta,cap,s,sh from the manifest, accept WITHIN LIMITS: clipped-gap ... with matching values, and add the stored clipped-gap example as an untampered test.

Still open from the third review (non-blocking)

check_files fails open on an unreadable subdir (os.walk has no onerror). An empty entry list accepts a missing or non-directory root. Both are still in the code at this head.

Identity

All 11 commits are by wakesync noreply, author and committer. No 0xSolace, no trailers.

…6186e6 (cc573ae was branch-only); regenerate README block and CLAIMS.md with signed-manifest row
@wakesync
wakesync marked this pull request as ready for review November 5, 2026 14:41
@wakesync
wakesync merged commit 05d0ad8 into main Nov 5, 2026
2 checks passed
@wakesync
wakesync deleted the research/signed-model-manifest branch November 5, 2026 14:41
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.

P1: signed model manifest (M1+M2) + design: attested abliterated models on vast

1 participant