diff --git a/.github/workflows/weave.yml b/.github/workflows/weave.yml index b5dfe1c0..fc9c9d3f 100644 --- a/.github/workflows/weave.yml +++ b/.github/workflows/weave.yml @@ -43,6 +43,17 @@ jobs: PYTHONDONTWRITEBYTECODE=1 python3 research/weave/test.py --receipt "$RUNNER_TEMP/weave-check.json" cat "$RUNNER_TEMP/weave-check.json" PYTHONDONTWRITEBYTECODE=1 python3 research/weave/cycle.py demo | tee "$RUNNER_TEMP/weave-demo.json" + - name: Reproduce native candidate public-recovery attack (expected exit 1) + run: | + set -euo pipefail + if python3 research/weave/private_public.py --sources "$GITHUB_WORKSPACE/.weave-inputs" --require-distinction > "$RUNNER_TEMP/weave-private-public.json"; then + echo "The frozen experiment unexpectedly accepted asymmetry; reassess its scope." + exit 1 + else + status=$? + test "$status" -eq 1 + fi + cat "$RUNNER_TEMP/weave-private-public.json" - name: Package tracked research source and observed evidence if: always() run: | @@ -52,7 +63,7 @@ jobs: git -C .weave-inputs/ucns archive --format=zip --output="$RUNNER_TEMP/weave-inspection/ucns.zip" HEAD git rev-parse HEAD > "$RUNNER_TEMP/weave-inspection/STACK_HEAD.txt" git -C .weave-inputs/ucns rev-parse HEAD > "$RUNNER_TEMP/weave-inspection/UCNS_HEAD.txt" - for evidence in weave-check.json weave-demo.json; do + for evidence in weave-check.json weave-demo.json weave-private-public.json; do if test -f "$RUNNER_TEMP/$evidence"; then cp "$RUNNER_TEMP/$evidence" "$RUNNER_TEMP/weave-inspection/"; fi done - name: Retain exact-source research bundle, not a security release diff --git a/research/weave/PRIVATE_PUBLIC.md b/research/weave/PRIVATE_PUBLIC.md new file mode 100644 index 00000000..554bff61 --- /dev/null +++ b/research/weave/PRIVATE_PUBLIC.md @@ -0,0 +1,130 @@ +# Native-shift polynomial candidate v1 + +This replaces the earlier input-partition-only experiment, which did not fulfill +the construction task. Scope: a new, explicitly proposed public/private binding +after the merged native Weave cycle. The candidate may fail; no result transfers +to intended Weave as a whole. + +## Defined relation (frozen before execution) + +Generate 256 private material bytes. Construct their actual Stack `ByteOrigin` +using the unchanged, source-locked UCNS geometry. For circle i=0..7, select native +byte axis `material[i]` and complete frame `material[8+i] & 1`. Its complete turn +`t_i` includes the 360/720 distinction. Define integer displacement +`a_i = 1 + 256*t_i`, in 1..512. These are explicit candidate choices, not UCNS laws. + +The private eight-circle candidate consists of that source-bearing origin and +its eight native axes/states. It is Stack research, not a claim to have completed +UCNS's general higher-dimensional gonol construction. All eight states affect the +binding; six are withheld as individual geometric records. +This key-origin is separate from the cycle's retained message-origin; it does not +replace message identity or its sequential byte-occurrence axes. + +For the six hidden displacements: + + F_0(x) = x + F_j(x) = (F_(j-1)(x) + a_(j+1))^2, j=1..6 + +Publish the 65 exact integer coefficients of degree-64 `F_6`, the whole-circle +state 0 and occurrence-circle state 1, origin identity, corpus, existing cycle +profile and native-source identity. The coefficient payload is explicitly counted +as additional public evaluation information attached to the public view. It must +be attacked; calling a record “whole-plus-one” does not hide its disclosures. + +Sender: run the real merged sequence cycle, interpret its exact length-bearing +record as integer x, and emit `y = F_6(x+a_0)+a_1`, plus key identity and record +length. The sender has only public artifacts, message and code. + +Recipient: rebuild the private native states from private material; check the +public/private binding; subtract a_1; repeatedly take an **exact** nonnegative +integer square root and subtract a_7 through a_2; subtract a_0; restore exactly +the recorded byte length; run the unchanged cycle inverse. No plaintext copy, +encoder trace or sender callback is available to recovery. + +This is scalar algebra on an exact integer representation of an already-native +cycle record. It does not redefine UCNS geometry, replace sequence/occurrence +construction, reintroduce per-bit circles, alter prime routes, or import a +conventional cryptosystem. It is an explicit candidate for the previously missing +binding relation, not a ratified answer to Q2/Q7/Q12. + +## Falsification criterion and attack + +Required: public-only sending, exact private recovery, and failure of attempted +public-only recovery. Wrong/missing private input must be rejected by the private +API, but that refusal never establishes private necessity. + +Preregistered attack: coefficient decomposition. For degree d, coefficient +`[x^(d-1)]F = d*a_2` reveals the first hidden displacement. Substitute +`x = z-a_2`; all odd powers vanish. Replace `z^2` with a new variable and repeat +on the degree-d/2 polynomial. This peels all six displacements using **only the +public key**. Then perform exact recovery without the private material. + +Prediction: this candidate is FALSIFIED by that attack. The implementation must +nevertheless demonstrate the actual public sender/private receiver composition +and produce the counterexample. A successful public attack is not recast as an +interface success or a failure of intended Weave. If decomposition instead fails, +report the failure and preserve the exact case; do not infer security. + +The full private material need not be recovered: an equivalent inverse is enough +to falsify recipient-private necessity. The effective hidden inverse space is at +most 512^6 choices; the larger material origin does not confer additional inverse +entropy. The algebraic attack avoids enumerating that space. + +## Artifacts, boundaries and usage + +`keygen(profile, corpus, sources)` returns public and private serialized bytes. +The public artifact has two native complete turns plus all public evaluation +coefficients; the private artifact has the original private material. `send` takes +no private argument. `recover` requires private material. `public_recover` performs +the coefficient attack directly, without calling `recover` or `keygen`. + +```sh +cd research/weave +python private_public.py --sources /checkouts +python private_public.py --sources /checkouts --require-distinction +WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_private_public.py -v +WEAVE_SOURCES=/checkouts python test.py --receipt /tmp/weave-check.json +``` + +The first command emits observed evidence. The second exits 1 on a successful +public recovery attack; malformed inputs/infrastructure errors exit 2. Regression +success does not mean the candidate survived. The demo uses explicitly disclosed +fixture material for reproducibility; ordinary keygen uses fresh system randomness. + +Sender, private receiver and public attacker run as separate fresh processes in +an allowlisted source bundle, with only their declared serialized inputs. This is +an information-flow test, not an OS sandbox. All packet/key bytes are counted. +Limits: 256 plaintext bytes, at most three existing cycle rounds, a 16 KiB cycle +round budget, and six algebraic layers. Source integrity and exact-square checks +are not authentication. No network or production integration is introduced. + +Resource preflight: degree-64 polynomial arithmetic, six integer square roots and +a finite existing regression suite fit the available CPU/memory/disk. Stop at the +exact construction/recovery/attack results, not an invented elapsed-time cutoff. +Rollback: remove this candidate, tests and CI links; merged-cycle code and source +pins remain unchanged. A surviving candidate would proceed to additional attacks; +a failed one remains scoped evidence and is not enabled as encryption. + +Observed result: [evidence/private-public-v1.json](evidence/private-public-v1.json) +records public-only sending, exact private recovery and exact public recovery by +coefficient decomposition: **FALSIFIED for this candidate**. The demo counts +5,904 inner-cycle bytes, 377,885 packet bytes, 8,091 public-artifact bytes and 572 +private-artifact bytes. No compression or fixed doubling is claimed. All 196 +Weave tests passed with zero failures/errors/skips; suite source SHA-256 +`b43b7329099175e54d0ac2eb7a6850bbe870e7ddd898027243fb6e6fa609a0f3`. +The three-process test also runs with fresh system-random private material. + +Domain claim: `weave.research.native-shift-polynomial/v1` is a provisional Stack +scalar-binding experiment. “Private gonol” here denotes the source-bearing eight- +state candidate described above, not canonical UCNS general gonol completion or +an established cryptographic primitive. No term collision licenses either claim. +Consulted skill-lib `38c64332b840b2bbe1c07e53aeee8996644548e9` and METAPAT +`86415a5368c1a1417c2b6731f19967a6b1fce6bb`; their authority does not establish +cryptographic validity. + +## hmmm + +The candidate is expected to fail. A surviving native asymmetric relation, +confidentiality, authentication, replay handling and the broader private-corpus +binding remain open. Failure of this explicit algebraic candidate says nothing +about every possible private/public gonol construction. diff --git a/research/weave/QUESTIONS.md b/research/weave/QUESTIONS.md index f076a1e1..bd338a66 100644 --- a/research/weave/QUESTIONS.md +++ b/research/weave/QUESTIONS.md @@ -49,6 +49,15 @@ and inverse, failures, budgets and size accounting are in `CYCLE.md`. ## Usage +The proposed native-shift polynomial relation in [PRIVATE_PUBLIC.md](PRIVATE_PUBLIC.md) +now constructs a source-bearing private eight-state candidate, publishes its two +public native states and expanded evaluation law, and executes public-only sending +and exact private recovery over the real cycle. Public coefficient decomposition +also recovers exactly: this candidate is FALSIFIED for private-state necessity. +This is a result about the specified candidate, not intended Weave as a whole. Run +`python private_public.py --sources /checkouts --require-distinction`; exit 1 +records the actual successful public-recovery attack. + Run `python cycle.py demo --sources /checkouts`. Change supported operating choices through an explicit profile; do not silently change a named profile's meaning. The current next mathematical question is the private/public relation, not another diff --git a/research/weave/README.md b/research/weave/README.md index aa5e6eb5..f59908aa 100644 --- a/research/weave/README.md +++ b/research/weave/README.md @@ -22,6 +22,15 @@ accounting, budgets and exact source lock. These choices are not universal laws. [The numeral layer](NUMERAL_CONSTRUCTION.md) separately records length-bearing integers, Unicode references and exact prime recipes. +[The private/public candidate](PRIVATE_PUBLIC.md) derives an expanded public +polynomial from private native circle displacements. A public-only sender evaluates +it over the real cycle record; the recipient reverses exact square roots using +private native state. A public coefficient-decomposition attack also recovers +exactly, falsifying this candidate's private necessity. Run +`python private_public.py --sources /checkouts --require-distinction` to reproduce +the attack and failing acceptance gate. The earlier input-partition-only probe +has been replaced; no candidate failure transfers to all intended Weave relations. + `native_binary.py` is the Stack-owned binary-origin/sequence candidate, consuming actual source-verified UCNS `AxisCirclePosition` and `NativeMobiusState` objects. UCHC supplies the origin/axis architecture reference; the premature UCHC #7 diff --git a/research/weave/evidence/private-public-v1.json b/research/weave/evidence/private-public-v1.json new file mode 100644 index 00000000..01acc899 --- /dev/null +++ b/research/weave/evidence/private-public-v1.json @@ -0,0 +1,57 @@ +{ + "accounting": { + "inner_cycle_bytes": 5904, + "message_bytes": 14, + "packet_bytes": 377885, + "private_bytes": 572, + "public_bytes": 8091 + }, + "attack": "exact public coefficient decomposition", + "distinction_established": false, + "equivalent_inverse_recovered": true, + "hmmm": "A surviving asymmetric relation and cryptographic security remain unestablished.", + "inputs": { + "attacker": [ + "packet", + "public" + ], + "recipient": [ + "packet", + "public", + "private" + ], + "sender": [ + "message", + "public" + ], + "shared": [ + "exact code", + "locked native sources" + ] + }, + "law": "weave.native-shift-polynomial/v1", + "native_lock_sha256": "cdf11526ca93a8627eea036d7e56ac630d4be04ccb1fe14ba5973966b19d6f43", + "packet_sha256": "9b2c6593ea118d36ca722a4d3c827fbace309c677fe90485061209635299430c", + "private_material_given_to_attacker": false, + "private_recovery_exact": true, + "public_recovery_exact": true, + "public_sender_completed": true, + "public_sha256": "5ea45180a952ea9d52ce7d6eabd5d523e82045c8bae58d7c5ed388a88af5aceb", + "schema": "weave.private-public-experiment/v2", + "scope": "This candidate only; no verdict on all native private/public relations.", + "source_files": { + "CYCLE_NATIVE.json": "cdf11526ca93a8627eea036d7e56ac630d4be04ccb1fe14ba5973966b19d6f43", + "affixiation.py": "15518004e43873515badefd951d6f32a0de79ce8002d3aebd41921bf8d7eb985", + "cycle.py": "25cbe0e5567562c1f2de3de0ca69d4e5a4e57e78a283aa7dcee882b0b16de562", + "cycle_native.py": "8a3cf679078b6ffc36a26ab99d0f4cbd593cfa2c404e05fd927d8fae09947219", + "inputs/ucns/src/ucns/axis_circle.py": "ab69c3cb84065d8f6332d31d0c9f128b2685954ecf4de5d3c5d29e0afe371f9c", + "inputs/ucns/src/ucns/direct_mobius.py": "d8d1360c753dac7431071e007c5105a21b5396dd9e2f7e5ba4089d99e056a5bf", + "native_binary.py": "3843b4fb7c735c2d3d45f28a13905397a4dd8ff988fba91b3fe1f92e75103e4d", + "numeral.py": "a3c2b67611b095f65ba4b355fac2275735a062fb76ba2170790e64d8b434f9e3", + "prime_schedule.py": "c5b59833d236c1f7dfe66102052e1796e8532a25f299820c68e2b417c6678242", + "private_public.py": "6619e1982d166748a61cc9448033f96dc0787b8bf32d41f0c37ebe3169ca665b", + "safe_output.py": "5bdfc6ead9f7eb9341d3de669ece63cdf653e1e8ba7882b334360cbafad8b052" + }, + "source_sha256": "496a529002bc66f82189cf64d5f866c0241f22304325722b6a34f67e936f08c2", + "status": "FALSIFIED" +} diff --git a/research/weave/private_public.py b/research/weave/private_public.py new file mode 100644 index 00000000..e0cb0b1b --- /dev/null +++ b/research/weave/private_public.py @@ -0,0 +1,367 @@ +# ratios: loc_comments=271:50 imports_exports=16:7 calls_definitions=157:19 +# === MODULE_BUILD === +# id: weave_native_shift_polynomial +# module_name: private_public +# module_kind: experiment +# summary: proposed public polynomial evaluation and native-private exact-root recovery after the unchanged Weave cycle +# owner: Erin Spencer +# public_surface: keygen, send, recover, public_recover, decompose, experiment +# internal_surface: exact scalar binding, serialized artifacts and fresh-process witness +# auth_boundary: none +# storage_boundary: write +# network_boundary: none +# user_data_boundary: read +# admin_only: false +# tests: tests/test_private_public.py +# rollout: research CLI only; public recovery falsifies this candidate +# rollback: remove candidate and its tests, evidence and links +# unresolved: surviving asymmetry, confidentiality, authentication and replay +# === END MODULE_BUILD === +# === CONTRACTS === +# id: native_private_key_relation +# given: private source material and the exact native geometry +# then: eight complete native states determine two public displacements and six privately factored operations +# id: public_sender_evaluation +# given: public artifact and message only +# then: evaluate the published polynomial on the complete real Weave cycle record without recipient-private input +# id: native_private_exact_recovery +# given: matching private material, public artifact and packet +# then: native-derived inverse operations recover the exact cycle record and original bytes +# id: public_inverse_falsification +# given: published coefficients and packet only +# then: test coefficient decomposition and report exact public recovery as candidate falsification +# id: candidate_artifact_admission +# given: malformed, oversized or mismatched data +# then: reject without inventing keys, accepting approximate roots or returning partial plaintext +# id: candidate_process_boundary +# given: a research experiment +# then: sender, private receiver and public attack run in fresh processes with only their declared inputs +# === END CONTRACTS === +"""Usage: python private_public.py --sources /checkouts [--require-distinction]. + +PRIVATE_PUBLIC.md defines the proposed law, geometry boundary and algebraic attack. +This is a candidate construction, not a security implementation. The old input- +partition-only experiment is replaced, not advertised as completing this work. +""" +from __future__ import annotations + +import argparse +from fractions import Fraction +from hashlib import sha256 +from math import comb, isqrt +from pathlib import Path +import os +import re +import secrets +import subprocess +import sys +import tempfile + +import cycle +from cycle import Profile, CycleLimits, canonical, strict_json +from cycle_native import load_native +from numeral import Refused, _Reader, _uint, _blob + +ROOT = Path(__file__).resolve().parent +LAW = 'weave.native-shift-polynomial/v1' +MAGIC = b'WPP\x01' +LIMITS = CycleLimits(input_bytes=256, round_bytes=16384, rounds=3) +MAX_RECORD = LIMITS.round_bytes + 128 +MAX_BOUND_BITS = 64 * (8 * MAX_RECORD + 16) + 16 +MAX_WIRE = MAX_BOUND_BITS // 8 + 256 +RUNTIME = ('private_public.py', 'cycle.py', 'cycle_native.py', 'native_binary.py', + 'numeral.py', 'affixiation.py', 'prime_schedule.py', 'safe_output.py', 'CYCLE_NATIVE.json') + + +def _object(data, fields): + if type(data) is not bytes or len(data) > 65536: + raise Refused('bounded serialized artifact required') + obj = strict_json(data) + if type(obj) is not dict or set(obj) != set(fields) or canonical(obj) != data: + raise Refused('exact canonical artifact schema required') + return obj + + +def _hex(value, maximum): + if (type(value) is not str or len(value) > maximum * 2 + or re.fullmatch(r'(?:[0-9a-f]{2})*', value) is None): + raise Refused('bounded canonical hexadecimal required') + return bytes.fromhex(value) + + +def _native(material, sources): + if type(material) is not bytes or len(material) != 256: + raise Refused('exactly 256 private material bytes required') + api, geometry = load_native(sources) + origin = api.ByteOrigin(material, LAW + '/key-origin', 0, geometry) + turns = [] + for i in range(8): + axis = origin.byte_axis(material[i]) + state = api.Geometry.placed(geometry, axis, Fraction(-(material[8+i] & 1))) + turns.append(api.Geometry.lift(geometry, state)) + return origin.identity, tuple(turns) + + +def _offsets(turns): + return tuple(1 + int(t * 256) for t in turns) + + +def _expand(shifts): + coefficients = [0, 1] + for shift in shifts: + coefficients[0] += shift + squared = [0] * (2 * len(coefficients) - 1) + for i, a in enumerate(coefficients): + for j, b in enumerate(coefficients): + squared[i+j] += a*b + coefficients = squared + return coefficients + + +def _evaluate(coefficients, x): + value = 0 + for coefficient in reversed(coefficients): + value = value*x + coefficient + return value + + +def _public(data): + obj = _object(data, ('law', 'key_origin', 'public_turns', 'coefficients', 'profile', + 'corpus_hex', 'native_lock_sha256')) + if obj['law'] != LAW or obj['native_lock_sha256'] != sha256((ROOT/'CYCLE_NATIVE.json').read_bytes()).hexdigest(): + raise Refused('law or native source identity mismatch') + if len(_hex(obj['key_origin'], 32)) != 32: + raise Refused('key origin identity required') + rows = obj['public_turns'] + if type(rows) is not list or len(rows) != 2: + raise Refused('exactly whole-plus-one native complete turns required') + turns = [] + for row in rows: + if (type(row) is not list or len(row) != 2 or any(type(v) is not int for v in row) + or not 1 <= row[1] <= 256 or not 0 <= row[0] < 2*row[1]): + raise Refused('invalid complete turn') + t = Fraction(*row) + if [t.numerator, t.denominator] != row or (t*256).denominator != 1: + raise Refused('turn must be a canonical native key-axis position') + turns.append(t) + c = obj['coefficients'] + if (type(c) is not list or len(c) != 65 or c[-1] != 1 + or any(type(v) is not int or v < 0 or v.bit_length() > 4096 for v in c)): + raise Refused('bounded exact monic degree-64 public polynomial required') + profile = Profile.read(canonical(obj['profile']), LIMITS) + corpus = _hex(obj['corpus_hex'], 4096) + if not corpus: + raise Refused('actual nonempty public corpus required') + return obj, profile, corpus, _offsets(turns) + + +def keygen(profile: Profile, corpus: bytes, sources: str | Path, *, material: bytes | None = None): + """Construct this candidate's native private configuration and public law.""" + if material is None: + material = secrets.token_bytes(256) + origin, turns = _native(material, sources) + if type(corpus) is not bytes: + raise Refused('actual corpus bytes required') + public = canonical({'law': LAW, 'key_origin': origin, + 'public_turns': [[t.numerator, t.denominator] for t in turns[:2]], + 'coefficients': _expand(_offsets(turns)[2:]), + 'profile': Profile.as_dict(profile), 'corpus_hex': corpus.hex(), + 'native_lock_sha256': sha256((ROOT/'CYCLE_NATIVE.json').read_bytes()).hexdigest()}) + _public(public) + private = canonical({'law': LAW, 'material_hex': material.hex()}) + return public, private + + +def send(message: bytes, public: bytes, sources: str | Path) -> bytes: + """Evaluate the expanded public law; no private material or keygen call.""" + obj, profile, corpus, (whole, one) = _public(public) + record = cycle.forward(message, corpus, profile, sources, LIMITS)[0] + x = int.from_bytes(record, 'big') + y = _evaluate(obj['coefficients'], x + whole) + one + if y.bit_length() > MAX_BOUND_BITS: + raise Refused('public evaluation exceeds bound integer budget') + encoded = y.to_bytes((y.bit_length()+7)//8, 'big') + return MAGIC + sha256(public).digest() + _uint(len(record)) + _blob(encoded) + + +def _packet(packet, public): + if type(packet) is not bytes or len(packet) > MAX_WIRE: + raise Refused('bounded packet bytes required') + reader = _Reader(packet) + if reader.take(4) != MAGIC or reader.take(32) != sha256(public).digest(): + raise Refused('packet law/key identity mismatch') + length = reader.uint() + if not 1 <= length <= MAX_RECORD: + raise Refused('cycle record length outside budget') + encoded = reader.blob(MAX_WIRE) + if reader.pos != len(packet) or not encoded or encoded[0] == 0: + raise Refused('noncanonical or trailing bound integer') + y = int.from_bytes(encoded, 'big') + if y.bit_length() > MAX_BOUND_BITS: + raise Refused('bound integer exceeds bit budget') + return length, y + + +def _invert(y, shifts): + for shift in reversed(shifts): + if y < 0: + raise Refused('negative inverse state') + root = isqrt(y) + if root*root != y: + raise Refused('non-exact square in private relation') + y = root - shift + return y + + +def _restore(packet, public, shifts, sources): + _, profile, corpus, (whole, one) = _public(public) + length, y = _packet(packet, public) + x = _invert(y-one, shifts) - whole + if x < 0 or x.bit_length() > length*8: + raise Refused('inverse does not fit declared record length') + return cycle.reverse(x.to_bytes(length, 'big'), corpus, profile, sources, LIMITS) + + +def recover(packet: bytes, public: bytes, private: bytes, sources: str | Path) -> bytes: + """Use actual private native states for exact square-root recovery.""" + _, profile, corpus, _ = _public(public) + secret = _object(private, ('law', 'material_hex')) + if secret['law'] != LAW: + raise Refused('private law mismatch') + material = _hex(secret['material_hex'], 256) + rebuilt, _ = keygen(profile, corpus, sources, material=material) + if rebuilt != public: + raise Refused('private material belongs to a different key') + _, turns = _native(material, sources) + return _restore(packet, public, _offsets(turns)[2:], sources) + + +def decompose(coefficients): + """Public algebraic attack: peel inner native displacements from coefficients.""" + if (type(coefficients) not in (list, tuple) or len(coefficients) != 65 + or any(type(v) is not int or v < 0 or v.bit_length() > 4096 for v in coefficients)): + raise Refused('bounded exact degree-64 coefficient list required') + p = list(coefficients) + shifts = [] + for _ in range(6): + degree = len(p)-1 + if degree < 2 or p[-1] != 1 or p[-2] % degree: + raise Refused('polynomial does not admit the candidate decomposition') + shift = p[-2] // degree + if not 1 <= shift <= 512: + raise Refused('recovered displacement outside native candidate domain') + # F(z-shift) = H(z^2). Exact cancellation, no numeric root estimates. + translated = [sum(p[j]*comb(j,i)*(-shift)**(j-i) for j in range(i,degree+1)) + for i in range(degree+1)] + if any(translated[1::2]): + raise Refused('public polynomial has nonzero odd residuals') + p = translated[::2] + shifts.append(shift) + if p != [0, 1]: + raise Refused('public decomposition did not end at identity') + return tuple(shifts) + + +def public_recover(packet: bytes, public: bytes, sources: str | Path) -> bytes: + """Recover via public coefficients alone; never consume the private artifact.""" + obj, _, _, _ = _public(public) + return _restore(packet, public, decompose(obj['coefficients']), sources) + + +def _worker(): + request = strict_json(sys.stdin.buffer.read(2*MAX_WIRE+262145)) + if type(request) is not dict or request.get('operation') not in ('send', 'recover', 'public-recover'): + raise Refused('unknown worker operation') + operation = request['operation'] + fields = {'operation', 'public', 'data'} | ({'private'} if operation == 'recover' else set()) + if set(request) != fields: + raise Refused('worker input manifest mismatch') + public = _hex(request['public'], 65536) + data = _hex(request['data'], MAX_WIRE) + if operation == 'send': + result = send(data, public, ROOT/'inputs') + elif operation == 'public-recover': + result = public_recover(data, public, ROOT/'inputs') + else: + result = recover(data, public, _hex(request['private'], 65536), ROOT/'inputs') + return {'data': result.hex()} + + +def _run(root, operation, public, data, private=None): + request = {'operation': operation, 'public': public.hex(), 'data': data.hex()} + if private is not None: + request['private'] = private.hex() + result = subprocess.run([sys.executable, '-S', '-B', '-E', str(root/'private_public.py'), '--worker'], + input=canonical(request), stdout=subprocess.PIPE, stderr=subprocess.PIPE, + cwd=root, env={}) + if result.returncode: + raise RuntimeError(f'{operation} worker failed: {result.stderr.decode(errors="replace")}') + response = strict_json(result.stdout) + if type(response) is not dict or set(response) != {'data'}: + raise RuntimeError('invalid worker response') + return _hex(response['data'], MAX_WIRE) + + +def experiment(message, profile, corpus, sources, *, material=None): + """Execute the actual candidate and its public inverse attack, with scoped evidence.""" + public, private = keygen(profile, corpus, sources, material=material) + with tempfile.TemporaryDirectory(prefix='weave-native-key-') as directory: + root = Path(directory) + for name in RUNTIME: + (root/name).write_bytes((ROOT/name).read_bytes()) + lock = strict_json((ROOT/'CYCLE_NATIVE.json').read_bytes()) + for name in lock['ucns_sources']: + target = root/'inputs'/'ucns'/name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((Path(sources)/'ucns'/name).read_bytes()) + files = lambda: {str(p.relative_to(root)): sha256(p.read_bytes()).hexdigest() + for p in sorted(root.rglob('*')) if p.is_file()} + source_files = files() + packet = _run(root, 'send', public, message) + restored = _run(root, 'recover', public, packet, private) + attacked = _run(root, 'public-recover', public, packet) + if files() != source_files: + raise RuntimeError('worker bundle changed during execution') + if restored != message: + raise RuntimeError('candidate private recovery failed') + exact = attacked == message + return {'schema': 'weave.private-public-experiment/v2', 'law': LAW, + 'status': 'FALSIFIED' if exact else 'UNRESOLVED', 'distinction_established': False, + 'public_sender_completed': True, 'private_recovery_exact': True, + 'public_recovery_exact': exact, 'attack': 'exact public coefficient decomposition', + 'private_material_given_to_attacker': False, 'equivalent_inverse_recovered': exact, + 'inputs': {'sender': ['message', 'public'], 'recipient': ['packet', 'public', 'private'], + 'attacker': ['packet', 'public'], 'shared': ['exact code', 'locked native sources']}, + 'accounting': {'message_bytes': len(message), 'packet_bytes': len(packet), + 'public_bytes': len(public), 'private_bytes': len(private), + 'inner_cycle_bytes': _packet(packet, public)[0]}, + 'source_files': source_files, 'source_sha256': sha256(canonical(source_files)).hexdigest(), + 'public_sha256': sha256(public).hexdigest(), 'packet_sha256': sha256(packet).hexdigest(), + 'native_lock_sha256': sha256((ROOT/'CYCLE_NATIVE.json').read_bytes()).hexdigest(), + 'scope': 'This candidate only; no verdict on all native private/public relations.', + 'hmmm': 'A surviving asymmetric relation and cryptographic security remain unestablished.'} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--sources', type=Path, default=Path(os.environ.get('WEAVE_SOURCES', ROOT/'sources'))) + parser.add_argument('--require-distinction', action='store_true') + parser.add_argument('--worker', action='store_true', help=argparse.SUPPRESS) + args = parser.parse_args() + try: + if args.worker: + result = _worker() + else: + profile = Profile.read((ROOT/'profiles'/'cycle-v1.json').read_bytes(), LIMITS) + result = experiment(b'ABxABy\x00ABxABy\xff', profile, bytes(range(256)), args.sources, + material=bytes((i*73+41)%256 for i in range(256))) + print(canonical(result).decode()) + return 1 if args.require_distinction and not result.get('distinction_established', False) else 0 + except (Refused, OSError, RuntimeError) as exc: + print(f'private/public experiment error: {exc}', file=sys.stderr) + return 2 + + +if __name__ == '__main__': + raise SystemExit(main()) +# ratios: loc_comments=271:50 imports_exports=16:7 calls_definitions=157:19 diff --git a/research/weave/test.py b/research/weave/test.py index 7d2941ab..6c399510 100644 --- a/research/weave/test.py +++ b/research/weave/test.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# ratios: loc_comments=82:7 imports_exports=9:2 calls_definitions=18:2 """Run the full repository Weave suite and emit a source-bound receipt. Usage: WEAVE_SOURCES=/checkouts python test.py --receipt /tmp/weave-check.json @@ -17,7 +18,7 @@ from assembly import Pipeline ROOT = Path(__file__).resolve().parent -EXPECTED_TESTS = 176 # 168 existing + 8 cheap-admission/public-helper regressions. +EXPECTED_TESTS = 196 # 176 merged-cycle regressions + 20 native public/private candidate tests. def snapshot(): @@ -65,11 +66,13 @@ def main(): 'native_origin_methods': 13, 'cycle_repair_methods': 12, 'review_closure_methods': 18, 'final_findings_methods': 9, 'terminal_record_methods': 9, 'header_admission_methods': 3, - 'numeral_single_replay_methods': 4, 'cheap_admission_methods': 8 + 'numeral_single_replay_methods': 4, 'cheap_admission_methods': 8, + 'private_public_candidate_methods': 20 }, 'cycle': 'EXPLICIT_CANDIDATE: corpus normalization once; native sequence affixiation and prime-derived bit interleave; exact reverse', 'full_weave': Pipeline().plan(), 'asymmetric_cipher': 'NOT_IMPLEMENTED: cycle/profile recovery is not a public/private trapdoor', + 'private_public_experiment': 'FALSIFIED_CANDIDATE: native-shift polynomial supports public sending and private recovery, but public coefficients reveal an equivalent inverse', 'native_language_corpus_replay': 'NOT_EXECUTED: binary construction does not use or replace a language corpus', 'security_observations': [ 'The older fixed transport map remains recoverable in its declared attack experiment.', @@ -93,3 +96,4 @@ def main(): if __name__ == '__main__': raise SystemExit(main()) +# ratios: loc_comments=82:7 imports_exports=9:2 calls_definitions=18:2 diff --git a/research/weave/tests/test_private_public.py b/research/weave/tests/test_private_public.py new file mode 100644 index 00000000..f71afb17 --- /dev/null +++ b/research/weave/tests/test_private_public.py @@ -0,0 +1,322 @@ +# ratios: loc_comments=187:106 imports_exports=14:1 calls_definitions=131:22 +# === CHECKS === +# id: check_native_candidate_private_native_states_preserve_full_frame +# proves: native_private_key_relation +# call: self::test_private_native_states_preserve_full_frame +# mutates: none +# cleanup: none +# id: check_native_candidate_public_artifact_exposes_two_states_and_evaluation_coefficients +# proves: native_private_key_relation +# call: self::test_public_artifact_exposes_two_states_and_evaluation_coefficients +# mutates: none +# cleanup: none +# id: check_native_candidate_keygen_default_uses_fresh_material +# proves: native_private_key_relation +# call: self::test_keygen_default_uses_fresh_material +# mutates: none +# cleanup: none +# id: check_native_candidate_expanded_public_law_equals_private_composition +# proves: native_private_exact_recovery +# call: self::test_expanded_public_law_equals_private_composition +# mutates: none +# cleanup: none +# id: check_native_candidate_sender_never_calls_private_construction_or_inverse +# proves: public_sender_evaluation +# call: self::test_sender_never_calls_private_construction_or_inverse +# mutates: none +# cleanup: none +# id: check_native_candidate_private_exact_recovery_binary_edge_cases +# proves: native_private_exact_recovery +# call: self::test_private_exact_recovery_binary_edge_cases +# mutates: none +# cleanup: none +# id: check_native_candidate_every_native_state_materially_changes_binding +# proves: native_private_key_relation +# call: self::test_every_native_state_materially_changes_binding +# mutates: none +# cleanup: none +# id: check_native_candidate_public_only_attack_recovers_without_private_path +# proves: public_inverse_falsification +# call: self::test_public_only_attack_recovers_without_private_path +# mutates: none +# cleanup: none +# id: check_native_candidate_public_decomposition_recovers_equivalent_inverse_not_material +# proves: public_inverse_falsification +# call: self::test_public_decomposition_recovers_equivalent_inverse_not_material +# mutates: none +# cleanup: none +# id: check_native_candidate_private_api_rejects_missing_and_wrong_material +# proves: candidate_artifact_admission +# call: self::test_private_api_rejects_missing_and_wrong_material +# mutates: none +# cleanup: none +# id: check_native_candidate_non_exact_root_is_refused_without_rounding +# proves: candidate_artifact_admission +# call: self::test_non_exact_root_is_refused_without_rounding +# mutates: none +# cleanup: none +# id: check_native_candidate_malformed_public_artifacts_are_refused +# proves: candidate_artifact_admission +# call: self::test_malformed_public_artifacts_are_refused +# mutates: none +# cleanup: none +# id: check_native_candidate_packet_identity_length_and_framing_are_checked +# proves: candidate_artifact_admission +# call: self::test_packet_identity_length_and_framing_are_checked +# mutates: none +# cleanup: none +# id: check_native_candidate_budgets_and_private_material_admission +# proves: candidate_artifact_admission +# call: self::test_budgets_and_private_material_admission +# mutates: none +# cleanup: none +# id: check_native_candidate_decomposition_refuses_non_candidate_polynomial +# proves: candidate_artifact_admission +# call: self::test_decomposition_refuses_non_candidate_polynomial +# mutates: none +# cleanup: none +# id: check_native_candidate_three_round_fresh_process_construction_and_attack +# proves: candidate_process_boundary +# call: self::test_three_round_fresh_process_construction_and_attack +# mutates: filesystem +# cleanup: tempdir_teardown +# id: check_native_candidate_source_failure_is_error_not_attack_resistance +# proves: candidate_process_boundary +# call: self::test_source_failure_is_error_not_attack_resistance +# mutates: none +# cleanup: none +# id: check_native_candidate_worker_failure_is_error_not_attack_resistance +# proves: candidate_process_boundary +# call: self::test_worker_failure_is_error_not_attack_resistance +# mutates: filesystem +# cleanup: tempdir_teardown +# id: check_native_candidate_acceptance_command_fails_on_actual_public_recovery +# proves: public_inverse_falsification +# call: self::test_acceptance_command_fails_on_actual_public_recovery +# mutates: filesystem +# cleanup: tempdir_teardown +# id: check_native_candidate_cli_infrastructure_error_exits_two +# proves: candidate_process_boundary +# call: self::test_cli_infrastructure_error_exits_two +# mutates: none +# cleanup: none +# === END CHECKS === +"""Usage: WEAVE_SOURCES=/checkouts python -m unittest discover -s tests -p test_private_public.py -v. + +This suite constructs the explicit native-shift polynomial candidate and records +its public coefficient attack. Green tests mean the falsification is reproduced. +""" +from copy import deepcopy +from dataclasses import replace +from fractions import Fraction +from pathlib import Path +import json +import os +import random +import subprocess +import sys +import unittest +from unittest.mock import patch + +import private_public as pp +from cycle import Profile, canonical +from numeral import Refused, _blob, _uint + +ROOT = Path(__file__).resolve().parents[1] +SOURCES = Path(os.environ.get('WEAVE_SOURCES', ROOT/'sources')) +BASE = Profile.read((ROOT/'profiles'/'cycle-v1.json').read_bytes()) +PROFILE = replace(BASE, rounds=BASE.rounds[:1]) +CORPUS = b'actual corpus\x00\xff' +MATERIAL = bytes((i*73+41)%256 for i in range(256)) + + +class PrivatePublicTests(unittest.TestCase): + def setUp(self): + self.public, self.private = pp.keygen(PROFILE, CORPUS, SOURCES, material=MATERIAL) + + def test_private_native_states_preserve_full_frame(self): + origin, turns = pp._native(MATERIAL, SOURCES) + self.assertEqual(len(origin), 64) + self.assertEqual(len(turns), 8) + self.assertEqual(turns, tuple(Fraction(MATERIAL[i],256)+(MATERIAL[8+i]&1) for i in range(8))) + changed = bytearray(MATERIAL); changed[10] ^= 1 + _, other = pp._native(bytes(changed), SOURCES) + self.assertEqual(abs(other[2]-turns[2]), 1) + self.assertEqual(abs(pp._offsets(other)[2]-pp._offsets(turns)[2]), 256) + + def test_public_artifact_exposes_two_states_and_evaluation_coefficients(self): + obj = json.loads(self.public) + self.assertEqual(set(obj), {'law','key_origin','public_turns','coefficients','profile','corpus_hex','native_lock_sha256'}) + self.assertEqual(len(obj['public_turns']), 2) + self.assertEqual(len(obj['coefficients']), 65) + self.assertNotIn('material_hex', obj) + self.assertEqual(json.loads(self.private), {'law':pp.LAW,'material_hex':MATERIAL.hex()}) + self.assertEqual(obj['profile'], PROFILE.as_dict()) + self.assertEqual(bytes.fromhex(obj['corpus_hex']), CORPUS) + + def test_keygen_default_uses_fresh_material(self): + alternative = bytes((x+1)%256 for x in MATERIAL) + with patch.object(pp.secrets,'token_bytes',side_effect=[MATERIAL,alternative]) as entropy: + a = pp.keygen(PROFILE,CORPUS,SOURCES) + b = pp.keygen(PROFILE,CORPUS,SOURCES) + self.assertEqual(entropy.call_args_list[0].args,(256,)) + self.assertNotEqual(a,b) + + def test_expanded_public_law_equals_private_composition(self): + rng = random.Random(20261006) + for _ in range(32): + shifts = tuple(rng.randint(1,512) for _ in range(6)) + coefficients = pp._expand(shifts) + for x in (0,1,255,65536): + value = x + for shift in shifts: value = (value+shift)**2 + self.assertEqual(pp._evaluate(coefficients,x),value) + self.assertEqual(pp._invert(value,shifts),x) + self.assertEqual(pp.decompose(coefficients),shifts) + + def test_sender_never_calls_private_construction_or_inverse(self): + with patch.object(pp,'keygen',side_effect=AssertionError('keygen in sender')), \ + patch.object(pp,'_native',side_effect=AssertionError('private state in sender')), \ + patch.object(pp,'decompose',side_effect=AssertionError('inverse in sender')), \ + patch.object(pp,'recover',side_effect=AssertionError('private recovery in sender')): + packet = pp.send(b'ABxABy',self.public,SOURCES) + self.assertEqual(pp.recover(packet,self.public,self.private,SOURCES),b'ABxABy') + + def test_private_exact_recovery_binary_edge_cases(self): + for message in (b'',b'\x00\x00\x01',bytes(range(256)),b'ABxABy'*8): + with self.subTest(length=len(message)): + packet = pp.send(message,self.public,SOURCES) + self.assertEqual(pp.recover(packet,self.public,self.private,SOURCES),message) + + def test_every_native_state_materially_changes_binding(self): + packet = pp.send(b'ABxABy',self.public,SOURCES) + value = pp._packet(packet,self.public)[1] + for i in range(8): + changed = bytearray(MATERIAL); changed[i] = (changed[i]+1)%256 + public, private = pp.keygen(PROFILE,CORPUS,SOURCES,material=bytes(changed)) + other = pp.send(b'ABxABy',public,SOURCES) + self.assertNotEqual(pp._packet(other,public)[1],value) + self.assertEqual(pp.recover(other,public,private,SOURCES),b'ABxABy') + + def test_public_only_attack_recovers_without_private_path(self): + message = b'ABxABy\x00ABxABy' + packet = pp.send(message,self.public,SOURCES) + with patch.object(pp,'keygen',side_effect=AssertionError('attacker keygen')), \ + patch.object(pp,'_native',side_effect=AssertionError('attacker private states')), \ + patch.object(pp,'recover',side_effect=AssertionError('attacker private API')): + self.assertEqual(pp.public_recover(packet,self.public,SOURCES),message) + + def test_public_decomposition_recovers_equivalent_inverse_not_material(self): + _, turns = pp._native(MATERIAL,SOURCES) + recovered = pp.decompose(json.loads(self.public)['coefficients']) + self.assertEqual(recovered,pp._offsets(turns)[2:]) + self.assertEqual(len(recovered),6) + self.assertNotEqual(recovered,tuple(MATERIAL)) + + def test_private_api_rejects_missing_and_wrong_material(self): + packet = pp.send(b'message',self.public,SOURCES) + for private in (None,b'',self.public,canonical({'law':pp.LAW,'material_hex':'00'*256})): + with self.assertRaises(Refused): pp.recover(packet,self.public,private,SOURCES) + self.assertEqual(pp.public_recover(packet,self.public,SOURCES),b'message') + + def test_non_exact_root_is_refused_without_rounding(self): + packet = pp.send(b'message',self.public,SOURCES) + length,y = pp._packet(packet,self.public) + encoded = (y+1).to_bytes(((y+1).bit_length()+7)//8,'big') + damaged = packet[:36]+_uint(length)+_blob(encoded) + with self.assertRaisesRegex(Refused,'non-exact square'): + pp.recover(damaged,self.public,self.private,SOURCES) + with self.assertRaises(Refused): pp.public_recover(damaged,self.public,SOURCES) + + def test_malformed_public_artifacts_are_refused(self): + obj = json.loads(self.public) + variants=[] + for key,value in [('law','other'),('material_hex',MATERIAL.hex()),('public_turns',[[0,1]]), + ('coefficients',[1]),('native_lock_sha256','0'*64),('corpus_hex','AA'), + ('key_origin','ab')]: + bad=deepcopy(obj); bad[key]=value; variants.append(canonical(bad)) + bad=deepcopy(obj); bad['public_turns'][0]=[2,4]; variants.append(canonical(bad)) + bad=deepcopy(obj); bad['coefficients'][0]=True; variants.append(canonical(bad)) + bad=deepcopy(obj); bad['coefficients'][0]=-1; variants.append(canonical(bad)) + variants += [b'{"law":1,"law":2}',b'[]',b'\xff',b'x'*65537,json.dumps(obj,indent=2).encode()] + for value in variants: + with self.assertRaises(Refused): pp.send(b'm',value,SOURCES) + + def test_packet_identity_length_and_framing_are_checked(self): + packet=pp.send(b'message',self.public,SOURCES) + variants=[packet[:-1],packet+b'!',packet[:36]+_uint(pp.MAX_RECORD+1)+_blob(b'\x01'), + packet[:36]+_uint(1)+_blob(b'\x00\x01'),b'x'*(pp.MAX_WIRE+1)] + corrupted=bytearray(packet); corrupted[4]^=1; variants.append(bytes(corrupted)) + for value in variants: + with self.assertRaises(Refused): pp.public_recover(value,self.public,SOURCES) + + def test_budgets_and_private_material_admission(self): + for material in (b'',b'x'*255,b'x'*257,'text'): + with self.assertRaises(Refused): pp.keygen(PROFILE,CORPUS,SOURCES,material=material) + for corpus in (b'',b'x'*4097,'text'): + with self.assertRaises(Refused): pp.keygen(PROFILE,corpus,SOURCES,material=MATERIAL) + with self.assertRaises(Refused): pp.send(b'x'*257,self.public,SOURCES) + with self.assertRaises(Refused): pp.keygen(replace(BASE,rounds=BASE.rounds*2),CORPUS,SOURCES,material=MATERIAL) + + def test_decomposition_refuses_non_candidate_polynomial(self): + coefficients=json.loads(self.public)['coefficients'] + for index in (0,1,63): + altered=coefficients.copy(); altered[index]+=1 + with self.assertRaises(Refused): pp.decompose(altered) + + def test_three_round_fresh_process_construction_and_attack(self): + calls=[] + private_inputs=[] + original=pp._run + def observe(root,operation,public,data,private=None): + self.assertEqual(private is not None,operation=='recover') + files={str(p.relative_to(root)) for p in root.rglob('*') if p.is_file()} + self.assertFalse(any('profile' in p or 'test_' in p for p in files)) + if private is not None: + private_inputs.append(json.loads(private)['material_hex']) + calls.append(operation) + return original(root,operation,public,data,private) + with patch.object(pp,'_run',side_effect=observe): + report=pp.experiment(b'ABxABy\x00ABxABy',BASE,CORPUS,SOURCES) + self.assertEqual(calls,['send','recover','public-recover']) + self.assertEqual(report['status'],'FALSIFIED') + self.assertTrue(report['public_sender_completed']) + self.assertTrue(report['private_recovery_exact']) + self.assertTrue(report['public_recovery_exact']) + self.assertFalse(report['private_material_given_to_attacker']) + self.assertFalse(report['distinction_established']) + self.assertEqual(set(report['source_files']),set(pp.RUNTIME)|{ + 'inputs/ucns/src/ucns/axis_circle.py','inputs/ucns/src/ucns/direct_mobius.py'}) + self.assertEqual(len(private_inputs),1) + self.assertNotIn(private_inputs[0].encode(),canonical(report)) + + def test_source_failure_is_error_not_attack_resistance(self): + with patch.object(pp,'load_native',side_effect=Refused('missing native source')): + with self.assertRaisesRegex(Refused,'missing native source'): + pp.experiment(b'm',PROFILE,CORPUS,SOURCES,material=MATERIAL) + + def test_worker_failure_is_error_not_attack_resistance(self): + result=subprocess.CompletedProcess([],2,b'',b'infrastructure failure') + with patch.object(pp.subprocess,'run',return_value=result): + with self.assertRaisesRegex(RuntimeError,'worker failed'): + pp.experiment(b'm',PROFILE,CORPUS,SOURCES,material=MATERIAL) + + def test_acceptance_command_fails_on_actual_public_recovery(self): + result=subprocess.run([sys.executable,str(ROOT/'private_public.py'),'--sources',str(SOURCES), + '--require-distinction'],capture_output=True) + self.assertEqual(result.returncode,1,result.stderr.decode()) + report=json.loads(result.stdout) + self.assertEqual(report['status'],'FALSIFIED') + self.assertTrue(report['public_recovery_exact']) + self.assertFalse(report['distinction_established']) + + def test_cli_infrastructure_error_exits_two(self): + result=subprocess.run([sys.executable,str(ROOT/'private_public.py'),'--sources','/missing-weave-sources'], + capture_output=True) + self.assertEqual(result.returncode,2) + self.assertEqual(result.stdout,b'') + self.assertIn(b'experiment error',result.stderr) + + +if __name__=='__main__': unittest.main() +# ratios: loc_comments=187:106 imports_exports=14:1 calls_definitions=131:22