diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c7399c..884ce9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,10 @@ jobs: - name: Run Python tests run: pytest -q + - name: Run crypto-analysis quick screens + if: runner.os != 'Windows' + run: python tests/crypto_analysis/run_all_screens.py --profile quick --variants baseline,fast8x --out /tmp/tricube-quick + - name: CLI smoke run: | python -m tricube.cli hash --hex 616263 diff --git a/.gitignore b/.gitignore index e90a57b..7df4737 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,10 @@ dist/ *.egg-info/ c/build/ results/tmp/ +tests/crypto_analysis/results/ +paper/*.docx +paper/*.pdf +!paper/TriCube_Short_Manuscript_2026-05-20.docx *.bin *.o .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 6424d3e..874412b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## Unreleased + +Updates preparing the experimental `fast8x` stream path for review. + +- Added the opt-in `fast8x` C stream variant and documented its separate + domain, round profile, output rate, and benchmark status. +- Added a fixed `fast8x` seed-123 stream vector and C test coverage for that + vector. +- Added the reproducible crypto-analysis screen suite under `tests/`, with + separate screen modules and an all-in-one quick runner. +- Added CI coverage for C tests, Python tests, and the quick crypto-analysis + screen profile on non-Windows runners. +- Clarified that `fast8x` has been compared against the released TriCube + stream baseline in the same C ablation harness, while optimized SHA-2, + SHA-3/SHAKE, BLAKE2, and BLAKE3 library comparisons remain future work. +- Kept `paper/manuscript.md` as the source manuscript and added a short + manuscript DOCX under `paper/`. + ## 0.1.0 - 2026-05-19 Initial public repository preparation for TriCube. @@ -8,5 +26,4 @@ Initial public repository preparation for TriCube. - Added Python package with hash, XOF, and deterministic stream APIs. - Added fixed test vectors shared by the C and Python paths. - Added documentation for design, testing, limitations, comparison, and reproducibility. -- Added cleaned result summary from the May 2026 internal validation pass. - +- Added cleaned result summary from the May 2026 validation pass. diff --git a/MANIFEST.in b/MANIFEST.in index 5000aaf..cf60cc8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include python/README.md include LICENSE +include THIRD_PARTY_NOTICES.md include CITATION.cff include CHANGELOG.md include SECURITY.md @@ -14,6 +15,7 @@ prune results/raw prune benchmarks prune tools prune examples +prune experiments prune c exclude CONTRIBUTING.md exclude README.md diff --git a/README.md b/README.md index 7b2b6ac..12b402c 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,23 @@ TriCube is a cryptographic engineering research project, not a secure primitive. ## Current Status -The repository contains a standalone C11 implementation, a Python reference package, fixed test vectors, CLI tools, benchmarks, reproducibility notes, external-battery run scripts, a cleaned result summary, and a manuscript draft. +The repository contains a standalone C11 implementation, a Python reference package, fixed test vectors, CLI tools, benchmarks, reproducibility notes, external-battery run scripts, a result summary, and a manuscript. The strongest current result is that TriCube now has a concrete geometric construction with reproducible C/Python vectors and nontrivial statistical-battery evidence. The main open issues are low-bit PractRand warnings, incomplete cryptanalysis, and performance that is still below mature optimized hash implementations. +## Specification and Security Boundary + +The primitive is specified in [docs/specification.md](docs/specification.md). That document gives the public baseline state size, lane layout, tetrahedral decomposition, round transformation, modes, padding, length encoding, domain tags, and test vectors. + +The security boundary is in [docs/security-status.md](docs/security-status.md). TriCube has black-box development probes and statistical-battery results, but it does not have formal differential, rotational, algebraic, or reduced-round cryptanalysis. The terms used in this repository are deliberately narrow: a probe or screen is an engineering check for obvious failures, not a security proof. + +The reproducible black-box screen suite is in +[tests/crypto_analysis/](tests/crypto_analysis/). It records the +exact command, branch, commit, machine, seed, byte count, and status labels for +each compact run. The same folder also contains a first white-box round-model +analyzer that checks word-level schedule dependency and coverage without +claiming formal cryptanalysis. + ## Quick Start Build and test the C implementation: @@ -58,10 +71,16 @@ c/build/tricube hash --hex 616263 c/build/tricube hash path/to/file.bin c/build/tricube xof --hex 616263 --bytes 64 c/build/tricube stream --seed 123 --bytes 1048576 --out stream.bin +c/build/tricube stream --seed 123 --bytes 1048576 --out stream.bin --variant fast8x ``` The public C header is [c/include/tricube.h](c/include/tricube.h). It exposes fixed 256-bit digest mode, XOF mode, context-style update/finalize/squeeze functions, deterministic stream generation, and self-test support. +The experimental `fast8x` stream entry points are kept visible in +[c/src/tricube_fast8x.c](c/src/tricube_fast8x.c). The shared permutation and +variant profile live in [c/src/tricube.c](c/src/tricube.c), so `fast8x` remains +one named TriCube stream variant rather than a forked second implementation. + The Python CLI is available after installation: ```bash @@ -77,14 +96,24 @@ TriCube uses 32 lanes of 64 bits each. Twenty-seven lanes correspond to a 3 x 3 Input bytes are absorbed with domain separation and length encoding. Digest mode squeezes 32 bytes. XOF mode squeezes an arbitrary number of bytes. Stream mode initializes from a seed and emits deterministic blocks for statistical testing. +The diagrams in [docs/specification.md](docs/specification.md) show the state +layout, tetrahedral decomposition, and round flow. The diagrams are explanatory; +the tables and pseudocode in the specification are the normative source. + The candidate novelty is the cube/tetrahedral state evolution and propagation schedule. It is not the use of hashing, XOFs, ARX operations, or sponge-like absorb/squeeze structure, all of which are established design families. -See [docs/design.md](docs/design.md) for the construction details. +See [docs/specification.md](docs/specification.md) for the exact construction and [docs/design.md](docs/design.md) for a shorter design overview. ## Main Results The following tables summarize the May 2026 validation evidence. These are engineering and statistical-screening results, not security proofs. +The default stream path remains the released baseline. The repository also includes an +experimental `fast8x` stream variant for external statistical testing. It is +domain-separated from the baseline and must be requested explicitly with +`--variant fast8x`. The ablation evidence for that choice is summarized in +[tests/ablation_lab/](tests/ablation_lab/). + ### Statistical Batteries | Evaluation | Result | Notes | @@ -104,22 +133,26 @@ The following tables summarize the May 2026 validation evidence. These are engin | Truncated birthday collision checks | PASS | 16/24/32/48/64-bit prefix collision counts were close to birthday expectation under practical sample sizes. | | Full-digest collision smoke | PASS | 0 full digest collisions and 0 prefix64 collisions over 20,000 sampled messages. | | Message-bit diffusion | PASS | 16-round mean changed bits: 127.819 of 256 over 8,192 samples. | -| Differential probes | PASS | Across tested deltas and rounds, mean changed bits stayed near 128; no repeated output differences were observed. | -| Rotational probes | PASS | No exact rotational relation was observed; mean rotational distances stayed near 128 bits. | +| Black-box differential diffusion probe | PASS | Across tested deltas and rounds, mean changed bits stayed near 128; no repeated output differences were observed. | +| Black-box rotational relation probe | PASS | No exact rotational relation was observed; mean rotational distances stayed near 128 bits. | | Domain/tweak separation | PASS | 5/5 unique digests; minimum hamming distance from default case was 121 bits. | -| State-recovery screen | PASS | Next-byte prediction accuracy 0.00396061, near the random baseline of 1/256. | -| Overlap/fork stream test | PASS | 0 repeated 32-byte block overlaps across 4 streams and 262,144 tested blocks. | +| Black-box state-recovery/predictability screen | PASS | Next-byte prediction accuracy 0.00396061, near the random baseline of 1/256. | +| Overlap/fork stream uniqueness screen | PASS | 0 repeated 32-byte block overlaps across 4 streams and 262,144 tested blocks. | + +These probes are development gates. They do not search differential trails, bound differential probabilities, prove resistance to rotational distinguishers, perform SAT/MILP or Gröbner-basis analysis, or prove state-recovery resistance. See [docs/testing.md](docs/testing.md) for the exact meaning of each probe. ### Throughput -Stream throughput is usable for external batteries; hash throughput is still the main engineering weakness. Values below are from local May 2026 runs on an Apple M4 Pro Mac mini unless noted. +Stream throughput is usable for external batteries; hash throughput is still the main engineering weakness. Values below are from May 2026 runs on an Apple M4 Pro Mac mini unless noted. | Implementation / mode | Throughput | |---|---:| +| Experimental C `fast8x` stream variant | ~132.7 MiB/s | +| Released C baseline stream in the same ablation harness | ~69.8 MiB/s | | `tricube_tc256_xof_fast` stream candidate | ~80.2 MiB/s | | `tricube_geo256_chain_fast` stream candidate | ~62.3 MiB/s | | `tricube_tetra_block256_chain_fast` stream candidate | ~57.3 MiB/s | -| Current standalone C TriCube stream | ~51.5 MiB/s in refresh run; ~53.1 MiB/s in local package smoke run | +| Current standalone C TriCube stream | ~51.5 MiB/s in refresh run; ~53.1 MiB/s in package smoke run | | `sha256_counter_chain` Python harness control | ~51.3 MiB/s | | Current standalone C TriCube hash, 1024-byte messages, 16 rounds | ~14.7 MiB/s | @@ -127,7 +160,7 @@ These numbers are not a claim of competitiveness with optimized SHA-2, SHA-3, BL ### Interpretation -TriCube has moved past a sketch: it has a concrete geometric state model, a C implementation, a Python interface, fixed vectors, battery results, structural probes, and reproducible commands. The strongest positive evidence is the TestU01 Crush pass, the Dieharder result with no failures, the absence of obvious structural failures in the current probes, and the fact that the C stream path is fast enough for longer batteries. +TriCube now has a concrete geometric state model, a C implementation, a Python interface, fixed vectors, battery results, structural probes, and reproducible commands. The strongest positive evidence is the TestU01 Crush pass, the Dieharder result with no failures, the absence of obvious structural failures in the current probes, and the fact that the C stream path is fast enough for longer batteries. The strongest negative evidence is also clear: PractRand flagged low-bit behavior, cryptanalysis is incomplete, and hash throughput is not yet competitive. The responsible conclusion is that TriCube deserves further review and hardening, not security use. @@ -135,7 +168,7 @@ The full public evidence summary is [results/consolidated-results-2026-05-19.md] ## Reproducing Results -Quick local checks: +Quick checks: ```bash make -C c test @@ -151,28 +184,37 @@ make -C c all tools/run_practrand.sh 1073741824 tools/run_dieharder.sh 1073741824 tools/run_testu01.sh smallcrush 1073741824 +python benchmarks/bench_stream.py --bytes 268435456 --variants baseline,fast8x --skip-python ``` See [tools/run_stat_batteries.md](tools/run_stat_batteries.md), [docs/testing.md](docs/testing.md), and [docs/reproducibility.md](docs/reproducibility.md) before interpreting results. -## Security Limitations +## Third-Party Tools + +TriCube does not bundle SmokeRand, PractRand, Dieharder, TestU01, NIST STS, or +third-party hash implementations. The scripts in `tools/` assume those +programs are installed separately and used under their own upstream licenses. +See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for the public notice table. -TriCube is not secure for production use. The limitations are direct: +## Analysis Tooling -- no security proof; -- no independent cryptanalysis; -- no collision-resistance or preimage-resistance claim; -- incomplete reduced-round, differential, rotational, algebraic, and state-recovery analysis; -- unresolved low-bit PractRand warnings; -- no side-channel or constant-time review; -- performance is not competitive with mature optimized hashes; -- statistical batteries do not prove cryptographic security. +TriCube uses standard external statistical batteries where possible, and the +next layer of formal analysis should use established solver and algebra systems +rather than custom ad hoc replacements. Z3, SageMath, CryptoMiniSat, +CLAASP/CryptoSMT-style frameworks, and related tooling are useful only after a +verified TriCube reduced-round model exists. The custom work is the TriCube +model; the search and solving machinery should come from well-understood tools. + +The tool plan and optional setup checks are in [docs/tooling.md](docs/tooling.md). +The current probe definitions and limits are in [docs/testing.md](docs/testing.md). + +## Security Limitations -See [docs/security-status.md](docs/security-status.md) and [docs/limitations.md](docs/limitations.md) for the full security boundary. +TriCube is research-only. It still lacks independent cryptanalysis, formal reduced-round analysis, side-channel review, and complete long-run multi-seed battery campaigns. Statistical batteries and development probes are useful evidence, but they are not a security proof. See [docs/security-status.md](docs/security-status.md) and [docs/limitations.md](docs/limitations.md) for the full boundary. ## Paper and Citation -The manuscript draft is in [paper/](paper/). It explains where TriCube came from, the current construction, the available evidence, and the analysis still required before stronger claims would be responsible. +The source manuscript is [paper/manuscript.md](paper/manuscript.md). It explains where TriCube came from, the current construction, the available evidence, and the analysis still required before stronger claims would be responsible. Generated PDF and DOCX exports should be attached to releases rather than tracked as source files. If you use this repository in research, cite [CITATION.cff](CITATION.cff). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..bd61685 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,29 @@ +# Third-Party Notices + +TriCube is released under the MIT License. The Python package and wheel do not +bundle external statistical batteries, third-party cryptographic libraries, +large result logs, or prebuilt external-tool binaries. + +The repository references optional external tools so reviewers can reproduce the +statistical screening described in the results. Those tools must be installed +separately and used under their own upstream licenses. + +| Tool or project | How TriCube uses it | Bundled in TriCube? | License / source note | +|---|---|---:|---| +| SmokeRand | Optional statistical battery referenced in result summaries and reproduction notes. | No | Upstream repository reports MIT License: . | +| PractRand | Optional random-stream statistical testing through `RNG_test`. | No | SourceForge lists PractRand as Public Domain: . | +| Dieharder | Optional random-stream statistical battery through stdin generator mode. | No | Upstream COPYING grants use under GNU GPL v2 or later with the project's stated modification. See . | +| TestU01 | Optional SmallCrush, Crush, and BigCrush testing through a local stdin wrapper. | No | Current official TestU01-2009 repository reports Apache-2.0: . Older archives or mirrors may carry different terms; use the license shipped with the copy you install. | +| NIST STS | Optional NIST Statistical Test Suite checks. | No | NIST describes the STS software as public domain with a software disclaimer. See . | +| Z3 | Optional SMT solver for future reduced-round bit-vector models. | No | MIT License. See . | +| SageMath | Optional algebraic system for future Boolean polynomial and ANF experiments. | No | GPL-licensed open-source mathematics system. See . | +| CryptoMiniSat | Optional SAT solver for future CNF experiments. | No | Upstream project reports default MIT-licensed build material; optional integrations can alter licensing. See . | +| CLAASP | Optional future framework for automated analysis of symmetric primitives. | No | PyPI metadata lists GPLv3. See and . | +| CryptoSMT | Optional future SMT/SAT cryptanalysis framework. | No | Upstream project: . Use the license shipped with the installed copy. | +| ArxPy | Optional future ARX cryptanalysis framework to investigate. | No | Project documentation: . Use the license shipped with the installed copy. | +| SHA-2, SHA-3/SHAKE, BLAKE2, BLAKE3, Keccak, CubeHash, Xoodoo/Xoodyak, KangarooTwelve | Referenced for comparison and literature context. | No | These are citations and comparison targets only. TriCube does not copy their implementations. | +| Python `hashlib` | Used in local benchmark scripts for standard-library comparison. | No separate vendoring | Part of the Python standard library. | + +Result logs may mention external tool names, versions, and output summaries. +Those logs are evidence records only; they are not redistributed copies of the +external tools. diff --git a/benchmarks/bench_stream.py b/benchmarks/bench_stream.py index b7bac4c..225fcf8 100644 --- a/benchmarks/bench_stream.py +++ b/benchmarks/bench_stream.py @@ -1,38 +1,77 @@ from __future__ import annotations import argparse +import csv import subprocess import time from pathlib import Path -import tricube - def main() -> int: parser = argparse.ArgumentParser(description="Benchmark TriCube stream generation.") parser.add_argument("--bytes", type=int, default=1_048_576) parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--variants", default="baseline,fast8x", help="comma-separated C stream variants") + parser.add_argument("--csv", type=Path, help="optional CSV output path") + parser.add_argument("--skip-python", action="store_true", help="skip the slower pure-Python reference stream") args = parser.parse_args() root = Path(__file__).resolve().parents[1] cli = root / "c" / "build" / "tricube" + rows: list[dict[str, str | int | float]] = [] if cli.exists(): + for variant in [item.strip() for item in args.variants.split(",") if item.strip()]: + start = time.perf_counter() + subprocess.run( + [ + str(cli), + "stream", + "--seed", + str(args.seed), + "--bytes", + str(args.bytes), + "--out", + "-", + "--variant", + variant, + ], + stdout=subprocess.DEVNULL, + check=True, + ) + elapsed = time.perf_counter() - start + mib = args.bytes / (1024 * 1024) + rows.append( + { + "implementation": f"tricube_c_stream_{variant}", + "bytes": args.bytes, + "elapsed_s": elapsed, + "mib_per_s": mib / elapsed, + } + ) + print(f"tricube_c_stream_{variant},{args.bytes},{elapsed:.6f},{mib / elapsed:.3f} MiB/s") + if not args.skip_python: + import tricube + start = time.perf_counter() - subprocess.run( - [str(cli), "stream", "--seed", str(args.seed), "--bytes", str(args.bytes), "--out", "-"], - stdout=subprocess.DEVNULL, - check=True, - ) + tricube.stream(args.seed, args.bytes) elapsed = time.perf_counter() - start mib = args.bytes / (1024 * 1024) - print(f"tricube_c_stream,{args.bytes},{elapsed:.6f},{mib / elapsed:.3f} MiB/s") - start = time.perf_counter() - tricube.stream(args.seed, args.bytes) - elapsed = time.perf_counter() - start - mib = args.bytes / (1024 * 1024) - print(f"tricube_python_stream,{args.bytes},{elapsed:.6f},{mib / elapsed:.3f} MiB/s") + rows.append( + { + "implementation": "tricube_python_stream", + "bytes": args.bytes, + "elapsed_s": elapsed, + "mib_per_s": mib / elapsed, + } + ) + print(f"tricube_python_stream,{args.bytes},{elapsed:.6f},{mib / elapsed:.3f} MiB/s") + if args.csv: + args.csv.parent.mkdir(parents=True, exist_ok=True) + with args.csv.open("w", newline="") as fp: + writer = csv.DictWriter(fp, fieldnames=["implementation", "bytes", "elapsed_s", "mib_per_s"]) + writer.writeheader() + writer.writerows(rows) return 0 if __name__ == "__main__": raise SystemExit(main()) - diff --git a/c/CMakeLists.txt b/c/CMakeLists.txt index aba0263..45633e9 100644 --- a/c/CMakeLists.txt +++ b/c/CMakeLists.txt @@ -4,7 +4,7 @@ project(tricube C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) -add_library(tricube_core src/tricube.c) +add_library(tricube_core src/tricube.c src/tricube_fast8x.c) target_include_directories(tricube_core PUBLIC include) target_link_libraries(tricube_core PUBLIC m) @@ -17,7 +17,11 @@ target_link_libraries(test_vectors PRIVATE tricube_core) add_executable(test_stream tests/test_stream.c) target_link_libraries(test_stream PRIVATE tricube_core) +add_executable(test_stream_variants tests/test_stream_variants.c) +target_link_libraries(test_stream_variants PRIVATE tricube_core) + enable_testing() add_test(NAME tricube_self_test COMMAND tricube self-test) add_test(NAME tricube_vectors COMMAND test_vectors) add_test(NAME tricube_stream COMMAND test_stream) +add_test(NAME tricube_stream_variants COMMAND test_stream_variants) diff --git a/c/Makefile b/c/Makefile index 49375b5..3cd2962 100644 --- a/c/Makefile +++ b/c/Makefile @@ -5,9 +5,11 @@ LDFLAGS ?= -lm BUILD_DIR := build INCLUDE := -Iinclude LIB_OBJ := $(BUILD_DIR)/tricube.o +FAST8X_OBJ := $(BUILD_DIR)/tricube_fast8x.o CLI_OBJ := $(BUILD_DIR)/tricube_cli.o TEST_VECTOR_OBJ := $(BUILD_DIR)/test_vectors.o TEST_STREAM_OBJ := $(BUILD_DIR)/test_stream.o +TEST_STREAM_VARIANTS_OBJ := $(BUILD_DIR)/test_stream_variants.o .PHONY: all clean test @@ -19,10 +21,13 @@ $(BUILD_DIR): $(LIB_OBJ): src/tricube.c include/tricube.h | $(BUILD_DIR) $(CC) $(CFLAGS) $(INCLUDE) -c src/tricube.c -o $@ +$(FAST8X_OBJ): src/tricube_fast8x.c include/tricube.h | $(BUILD_DIR) + $(CC) $(CFLAGS) $(INCLUDE) -c src/tricube_fast8x.c -o $@ + $(CLI_OBJ): src/tricube_cli.c include/tricube.h | $(BUILD_DIR) $(CC) $(CFLAGS) $(INCLUDE) -c src/tricube_cli.c -o $@ -$(BUILD_DIR)/tricube: $(LIB_OBJ) $(CLI_OBJ) +$(BUILD_DIR)/tricube: $(LIB_OBJ) $(FAST8X_OBJ) $(CLI_OBJ) $(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@ $(TEST_VECTOR_OBJ): tests/test_vectors.c include/tricube.h | $(BUILD_DIR) @@ -31,16 +36,23 @@ $(TEST_VECTOR_OBJ): tests/test_vectors.c include/tricube.h | $(BUILD_DIR) $(TEST_STREAM_OBJ): tests/test_stream.c include/tricube.h | $(BUILD_DIR) $(CC) $(CFLAGS) $(INCLUDE) -c tests/test_stream.c -o $@ -$(BUILD_DIR)/test_vectors: $(LIB_OBJ) $(TEST_VECTOR_OBJ) +$(TEST_STREAM_VARIANTS_OBJ): tests/test_stream_variants.c include/tricube.h | $(BUILD_DIR) + $(CC) $(CFLAGS) $(INCLUDE) -c tests/test_stream_variants.c -o $@ + +$(BUILD_DIR)/test_vectors: $(LIB_OBJ) $(FAST8X_OBJ) $(TEST_VECTOR_OBJ) + $(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@ + +$(BUILD_DIR)/test_stream: $(LIB_OBJ) $(FAST8X_OBJ) $(TEST_STREAM_OBJ) $(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@ -$(BUILD_DIR)/test_stream: $(LIB_OBJ) $(TEST_STREAM_OBJ) +$(BUILD_DIR)/test_stream_variants: $(LIB_OBJ) $(FAST8X_OBJ) $(TEST_STREAM_VARIANTS_OBJ) $(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@ -test: $(BUILD_DIR)/tricube $(BUILD_DIR)/test_vectors $(BUILD_DIR)/test_stream +test: $(BUILD_DIR)/tricube $(BUILD_DIR)/test_vectors $(BUILD_DIR)/test_stream $(BUILD_DIR)/test_stream_variants $(BUILD_DIR)/tricube self-test $(BUILD_DIR)/test_vectors $(BUILD_DIR)/test_stream + $(BUILD_DIR)/test_stream_variants clean: rm -rf $(BUILD_DIR) diff --git a/c/include/tricube.h b/c/include/tricube.h index 026fea1..d32c2ae 100644 --- a/c/include/tricube.h +++ b/c/include/tricube.h @@ -14,6 +14,11 @@ extern "C" { #define TRICUBE_HEX_BYTES 65 #define TRICUBE_DEFAULT_ROUNDS 16 +typedef enum tricube_stream_variant { + TRICUBE_STREAM_BASELINE = 0, + TRICUBE_STREAM_FAST8X = 1 +} tricube_stream_variant; + enum { TRICUBE_OK = 0, TRICUBE_ERR_INVALID_ARGUMENT = 1, @@ -45,6 +50,16 @@ void tricube_free(tricube_ctx *ctx); int tricube_stream_seed(uint64_t seed, uint8_t *out, size_t n_bytes); int tricube_stream_write(FILE *out, uint64_t seed, uint64_t n_bytes); +int tricube_stream_seed_variant(uint64_t seed, uint8_t *out, size_t n_bytes, tricube_stream_variant variant); +int tricube_stream_write_variant(FILE *out, uint64_t seed, uint64_t n_bytes, tricube_stream_variant variant); +int tricube_stream_write_unbounded(FILE *out, uint64_t seed, tricube_stream_variant variant); +const char *tricube_stream_variant_name(tricube_stream_variant variant); +int tricube_stream_variant_from_name(const char *name, tricube_stream_variant *variant); + +int tricube_fast8x_stream_seed(uint64_t seed, uint8_t *out, size_t n_bytes); +int tricube_fast8x_stream_write(FILE *out, uint64_t seed, uint64_t n_bytes); +int tricube_fast8x_stream_write_unbounded(FILE *out, uint64_t seed); + int tricube_self_test(void); #ifdef __cplusplus diff --git a/c/src/tricube.c b/c/src/tricube.c index bbf530b..0170ac4 100644 --- a/c/src/tricube.c +++ b/c/src/tricube.c @@ -3,8 +3,8 @@ * * This file contains the standalone C implementation of the TriCube core. * The construction is experimental and is not validated for security-critical - * use. Some internal domain tags are retained from the May 2026 research - * prototype so published test vectors and result artifacts remain reproducible. + * use. Some domain tags are retained from the May 2026 research prototype so + * published test vectors and result artifacts remain reproducible. */ #include @@ -26,11 +26,53 @@ #define TC_RC_WORDS (32 * 24) #define TC_TETRA_COUNT (8 * 6) #define TC_EDGE_COUNT 54 +#define TC_SCHEDULE_ROUNDS 24 + +typedef struct tetra_op { + uint8_t a; + uint8_t b; + uint8_t c; + uint8_t d; + uint8_t r0; + uint8_t r1; + uint8_t r2; + uint8_t r3; + uint64_t rc0; + uint64_t rc1; + uint64_t rc2; + uint64_t rc3; +} tetra_op; + +typedef struct edge_op { + uint8_t a; + uint8_t b; + uint8_t r0; + uint8_t r1; + uint64_t rc; +} edge_op; + +typedef struct shell_op { + uint8_t lane; + uint8_t vertex; + uint8_t opposite; + uint8_t rot; + uint64_t rc; +} shell_op; + +typedef struct perm_op { + uint8_t src; + uint8_t rot; + uint64_t rc; +} perm_op; static uint64_t ROUND_CONSTANTS[TC_RC_WORDS]; static uint64_t IV[TC_STATE_WORDS]; static uint8_t TETRAHEDRA[TC_TETRA_COUNT][4]; static uint8_t EDGES[TC_EDGE_COUNT][2]; +static tetra_op TETRA_OPS[TC_SCHEDULE_ROUNDS][TC_TETRA_COUNT]; +static edge_op EDGE_OPS[TC_SCHEDULE_ROUNDS][TC_EDGE_COUNT]; +static shell_op SHELL_OPS[TC_SCHEDULE_ROUNDS][5]; +static perm_op PERM_OPS[TC_SCHEDULE_ROUNDS][TC_STATE_WORDS]; static bool TABLES_READY = false; static uint64_t DEFAULT_HASH32_STATE[TC_STATE_WORDS]; static bool DEFAULT_HASH32_STATE_READY = false; @@ -136,6 +178,59 @@ static void init_tables(void) { } } } + + for (int rnd = 0; rnd < TC_SCHEDULE_ROUNDS; rnd++) { + for (size_t tet_index = 0; tet_index < TC_TETRA_COUNT; tet_index++) { + uint8_t a = TETRAHEDRA[tet_index][0]; + uint8_t b = TETRAHEDRA[tet_index][1]; + uint8_t c = TETRAHEDRA[tet_index][2]; + uint8_t d = TETRAHEDRA[tet_index][3]; + if ((rnd + (int)tet_index) & 1) { + uint8_t tmp = b; + b = d; + d = tmp; + } + if ((rnd + (int)tet_index) & 2) { + uint8_t tmp = a; + a = c; + c = tmp; + } + size_t base = ((size_t)rnd * 32U + tet_index) % TC_RC_WORDS; + const uint8_t *rots = ROTATION_SETS[(rnd + (int)tet_index) & 3]; + TETRA_OPS[rnd][tet_index].a = a; + TETRA_OPS[rnd][tet_index].b = b; + TETRA_OPS[rnd][tet_index].c = c; + TETRA_OPS[rnd][tet_index].d = d; + TETRA_OPS[rnd][tet_index].r0 = rots[0]; + TETRA_OPS[rnd][tet_index].r1 = rots[1]; + TETRA_OPS[rnd][tet_index].r2 = rots[2]; + TETRA_OPS[rnd][tet_index].r3 = rots[3]; + TETRA_OPS[rnd][tet_index].rc0 = ROUND_CONSTANTS[base]; + TETRA_OPS[rnd][tet_index].rc1 = ROUND_CONSTANTS[(base + 7U) % TC_RC_WORDS]; + TETRA_OPS[rnd][tet_index].rc2 = ROUND_CONSTANTS[(base + 13U) % TC_RC_WORDS]; + TETRA_OPS[rnd][tet_index].rc3 = ROUND_CONSTANTS[(base + 21U) % TC_RC_WORDS]; + } + for (size_t edge_index = 0; edge_index < TC_EDGE_COUNT; edge_index++) { + EDGE_OPS[rnd][edge_index].a = EDGES[edge_index][0]; + EDGE_OPS[rnd][edge_index].b = EDGES[edge_index][1]; + EDGE_OPS[rnd][edge_index].rc = ROUND_CONSTANTS[((size_t)rnd * 37U + edge_index * 5U) % TC_RC_WORDS]; + EDGE_OPS[rnd][edge_index].r0 = (uint8_t)((edge_index + (size_t)rnd * 3U) % 61U + 1U); + EDGE_OPS[rnd][edge_index].r1 = (uint8_t)((edge_index * 7U + (size_t)rnd) % 61U + 1U); + } + for (size_t j = 0; j < 5; j++) { + uint8_t vertex = (uint8_t)(((size_t)rnd * 5U + j * 7U) % 27U); + SHELL_OPS[rnd][j].lane = SHELL_LANES[j]; + SHELL_OPS[rnd][j].vertex = vertex; + SHELL_OPS[rnd][j].opposite = (uint8_t)((vertex * 11U + 3U) % 27U); + SHELL_OPS[rnd][j].rc = ROUND_CONSTANTS[((size_t)rnd * 11U + j * 17U) % TC_RC_WORDS]; + SHELL_OPS[rnd][j].rot = (uint8_t)(((size_t)rnd + j * 9U) % 61U + 1U); + } + for (size_t i = 0; i < TC_STATE_WORDS; i++) { + PERM_OPS[rnd][i].src = (uint8_t)((i * 9U + 5U) & 31U); + PERM_OPS[rnd][i].rc = ROUND_CONSTANTS[((size_t)rnd * 32U + i) % TC_RC_WORDS] + i + (uint64_t)rnd; + PERM_OPS[rnd][i].rot = (uint8_t)((i * 5U + (size_t)rnd) % 61U + 1U); + } + } TABLES_READY = true; } @@ -156,6 +251,19 @@ static void write_le64(uint8_t *out, uint64_t value) { } } +static inline void emit_le64(uint8_t *out, size_t *pos, size_t n_bytes, uint64_t value) { + size_t p = *pos; + if (p + 8U <= n_bytes) { + write_le64(out + p, value); + *pos = p + 8U; + return; + } + for (size_t k = 0; k < 8U && p < n_bytes; k++) { + out[p++] = (uint8_t)((value >> (8U * k)) & 0xffU); + } + *pos = p; +} + static void write_le_u64_to_buf(uint8_t *out, uint64_t value) { write_le64(out, value); } @@ -185,86 +293,49 @@ static void absorb_bytes(uint64_t state[TC_STATE_WORDS], const uint8_t *data, si absorb_words(state, words, 8, block_index); } -static void mix_tetra64(uint64_t state[TC_STATE_WORDS], const uint8_t tet[4], int rnd, size_t tet_index) { - uint8_t a = tet[0]; - uint8_t b = tet[1]; - uint8_t c = tet[2]; - uint8_t d = tet[3]; - if ((rnd + (int)tet_index) & 1) { - uint8_t tmp = b; - b = d; - d = tmp; - } - if ((rnd + (int)tet_index) & 2) { - uint8_t tmp = a; - a = c; - c = tmp; - } - uint64_t x0 = state[a]; - uint64_t x1 = state[b]; - uint64_t x2 = state[c]; - uint64_t x3 = state[d]; - size_t base = ((size_t)rnd * 32U + tet_index) % TC_RC_WORDS; - uint64_t rc0 = ROUND_CONSTANTS[base]; - uint64_t rc1 = ROUND_CONSTANTS[(base + 7U) % TC_RC_WORDS]; - uint64_t rc2 = ROUND_CONSTANTS[(base + 13U) % TC_RC_WORDS]; - uint64_t rc3 = ROUND_CONSTANTS[(base + 21U) % TC_RC_WORDS]; - const uint8_t *rots = ROTATION_SETS[(rnd + (int)tet_index) & 3]; - - x0 += x1 + rc0; - x3 = rotl64(x3 ^ x0, rots[0]); - x2 += x3 + rc1; - x1 = rotl64(x1 ^ x2, rots[1]); - x0 += x1 + (rc2 ^ (uint64_t)tet_index); - x3 = rotl64(x3 ^ x0, rots[2]); - x2 += x3 + (rc3 + (uint64_t)rnd); - x1 = rotl64(x1 ^ x2, rots[3]); - - state[a] = x0; - state[b] = x1; - state[c] = x2; - state[d] = x3; -} - -static void edge_couple64(uint64_t state[TC_STATE_WORDS], int rnd) { - for (size_t edge_index = 0; edge_index < TC_EDGE_COUNT; edge_index++) { - size_t a = EDGES[edge_index][0]; - size_t b = EDGES[edge_index][1]; - uint64_t rc = ROUND_CONSTANTS[((size_t)rnd * 37U + edge_index * 5U) % TC_RC_WORDS]; - uint64_t left = state[a]; - uint64_t right = state[b]; - state[a] = left + rotl64(right ^ rc, (unsigned int)((edge_index + (size_t)rnd * 3U) % 61U + 1U)); - state[b] = right ^ rotl64(state[a] + rc + edge_index, (unsigned int)((edge_index * 7U + (size_t)rnd) % 61U + 1U)); - } -} - -static void shell_couple64(uint64_t state[TC_STATE_WORDS], int rnd) { - for (size_t j = 0; j < 5; j++) { - size_t lane = SHELL_LANES[j]; - size_t vertex = ((size_t)rnd * 5U + j * 7U) % 27U; - size_t opposite = (vertex * 11U + 3U) % 27U; - uint64_t rc = ROUND_CONSTANTS[((size_t)rnd * 11U + j * 17U) % TC_RC_WORDS]; - state[lane] += state[vertex] + rc; - state[opposite] ^= rotl64(state[lane] ^ state[vertex], (unsigned int)(((size_t)rnd + j * 9U) % 61U + 1U)); - } -} - static void permute64(uint64_t state[TC_STATE_WORDS], int rounds) { uint64_t tmp[TC_STATE_WORDS]; for (int rnd = 0; rnd < rounds; rnd++) { + int srnd = rnd % TC_SCHEDULE_ROUNDS; for (size_t tet_index = 0; tet_index < TC_TETRA_COUNT; tet_index++) { - mix_tetra64(state, TETRAHEDRA[tet_index], rnd, tet_index); + const tetra_op *op = &TETRA_OPS[srnd][tet_index]; + uint64_t x0 = state[op->a]; + uint64_t x1 = state[op->b]; + uint64_t x2 = state[op->c]; + uint64_t x3 = state[op->d]; + + x0 += x1 + op->rc0; + x3 = rotl64(x3 ^ x0, op->r0); + x2 += x3 + op->rc1; + x1 = rotl64(x1 ^ x2, op->r1); + x0 += x1 + (op->rc2 ^ (uint64_t)tet_index); + x3 = rotl64(x3 ^ x0, op->r2); + x2 += x3 + (op->rc3 + (uint64_t)rnd); + x1 = rotl64(x1 ^ x2, op->r3); + + state[op->a] = x0; + state[op->b] = x1; + state[op->c] = x2; + state[op->d] = x3; + } + for (size_t edge_index = 0; edge_index < TC_EDGE_COUNT; edge_index++) { + const edge_op *op = &EDGE_OPS[srnd][edge_index]; + uint64_t left = state[op->a]; + uint64_t right = state[op->b]; + state[op->a] = left + rotl64(right ^ op->rc, op->r0); + state[op->b] = right ^ rotl64(state[op->a] + op->rc + edge_index, op->r1); + } + for (size_t j = 0; j < 5; j++) { + const shell_op *op = &SHELL_OPS[srnd][j]; + state[op->lane] += state[op->vertex] + op->rc; + state[op->opposite] ^= rotl64(state[op->lane] ^ state[op->vertex], op->rot); } - edge_couple64(state, rnd); - shell_couple64(state, rnd); for (size_t i = 0; i < TC_STATE_WORDS; i++) { - tmp[i] = state[(i * 9U + 5U) & 31U]; + tmp[i] = state[PERM_OPS[srnd][i].src]; } memcpy(state, tmp, sizeof(tmp)); - size_t rc_offset = (size_t)rnd * 32U; for (size_t i = 0; i < TC_STATE_WORDS; i++) { - state[i] ^= rotl64(ROUND_CONSTANTS[(rc_offset + i) % TC_RC_WORDS] + i + (uint64_t)rnd, - (unsigned int)((i * 5U + (size_t)rnd) % 61U + 1U)); + state[i] ^= rotl64(PERM_OPS[srnd][i].rc, PERM_OPS[srnd][i].rot); } } } @@ -333,7 +404,6 @@ static bool use_default_hash32_state(uint64_t state[TC_STATE_WORDS], const uint8 static void squeeze_rate64(uint64_t state[TC_STATE_WORDS], uint64_t counter, uint8_t *out, size_t n_bytes) { size_t groups = (n_bytes + 7U) / 8U; - uint8_t word_bytes[8]; size_t pos = 0; for (size_t j = 0; j < groups; j++) { uint64_t a = state[(j * 5U + (size_t)counter) & 31U]; @@ -343,10 +413,31 @@ static void squeeze_rate64(uint64_t state[TC_STATE_WORDS], uint64_t counter, uin uint64_t word = a + rotl64(b ^ ROUND_CONSTANTS[(counter + j) % TC_RC_WORDS], (unsigned int)((j * 7U + 9U) % 61U + 1U)); word ^= rotl64(c + d + j + counter, (unsigned int)((j * 13U + 3U) % 61U + 1U)); - write_le64(word_bytes, word); - for (size_t k = 0; k < 8 && pos < n_bytes; k++) { - out[pos++] = word_bytes[k]; - } + emit_le64(out, &pos, n_bytes, word); + } +} + +static void squeeze_rate64_xmix(uint64_t state[TC_STATE_WORDS], uint64_t counter, uint8_t *out, size_t n_bytes) { + size_t groups = (n_bytes + 7U) / 8U; + size_t pos = 0; + for (size_t j = 0; j < groups; j++) { + uint64_t a = state[(j * 5U + (size_t)counter) & 31U]; + uint64_t b = state[(j * 11U + 7U) & 31U]; + uint64_t c = state[(j * 17U + 13U) & 31U]; + uint64_t d = state[(j * 23U + 19U) & 31U]; + uint64_t e = state[(j * 29U + (size_t)counter * 3U + 3U) & 31U]; + uint64_t rc0 = ROUND_CONSTANTS[(counter * 13U + j * 17U) % TC_RC_WORDS]; + uint64_t rc1 = ROUND_CONSTANTS[(counter * 29U + j * 31U + 11U) % TC_RC_WORDS]; + uint64_t word = a + rotl64(b ^ rc0, (unsigned int)((j * 7U + 9U) % 61U + 1U)); + word ^= rotl64(c + d + j + counter, (unsigned int)((j * 13U + 3U) % 61U + 1U)); + word += rotl64(e ^ rc1 ^ (counter + j * UINT64_C(0x9E3779B97F4A7C15)), + (unsigned int)((j * 19U + 17U) % 61U + 1U)); + word ^= rotl64(word, 23) ^ rotl64(word, 41); + word += rotl64(word ^ a ^ d, 17); + word ^= (word >> 31) ^ (word >> 47); + word += rotl64(word ^ b ^ c ^ rc0, 29); + word ^= word >> 33; + emit_le64(out, &pos, n_bytes, word); } } @@ -413,18 +504,95 @@ static void seed_to_tweak(uint64_t seed, uint8_t out[16]) { write_le64(out, seed); } -static int write_stream(FILE *out, uint64_t seed, uint64_t n_bytes, size_t block_size, int rounds) { - static const uint8_t domain[] = "TC-TETRA256-" "V" "2/STREAM"; +typedef struct stream_profile { + tricube_stream_variant variant; + const char *name; + const uint8_t *domain; + size_t domain_len; + int init_rounds; + int step_rounds; + size_t rate_bytes; + int use_xmix; +} stream_profile; + +static const uint8_t STREAM_DOMAIN_BASELINE[] = "TC-TETRA256-" "V" "2/STREAM"; +static const uint8_t STREAM_DOMAIN_FAST8X[] = "TC-TETRA256-" "V" "2/STREAM/FAST8X"; + +static const stream_profile STREAM_PROFILES[] = { + {TRICUBE_STREAM_BASELINE, "baseline", STREAM_DOMAIN_BASELINE, sizeof(STREAM_DOMAIN_BASELINE) - 1U, 12, 6, TC_RATE_BYTES, 0}, + {TRICUBE_STREAM_FAST8X, "fast8x", STREAM_DOMAIN_FAST8X, sizeof(STREAM_DOMAIN_FAST8X) - 1U, 8, 4, 256, 1}, +}; + +static const stream_profile *stream_profile_for_variant(tricube_stream_variant variant) { + for (size_t i = 0; i < sizeof(STREAM_PROFILES) / sizeof(STREAM_PROFILES[0]); i++) { + if (STREAM_PROFILES[i].variant == variant) { + return &STREAM_PROFILES[i]; + } + } + return NULL; +} + +const char *tricube_stream_variant_name(tricube_stream_variant variant) { + const stream_profile *profile = stream_profile_for_variant(variant); + return profile == NULL ? NULL : profile->name; +} + +int tricube_stream_variant_from_name(const char *name, tricube_stream_variant *variant) { + if (name == NULL || variant == NULL) { + return TRICUBE_ERR_INVALID_ARGUMENT; + } + if (strcmp(name, "baseline") == 0 || strcmp(name, "default") == 0 || strcmp(name, "released") == 0) { + *variant = TRICUBE_STREAM_BASELINE; + return TRICUBE_OK; + } + if (strcmp(name, "fast8x") == 0 || strcmp(name, "rounds8x") == 0 || strcmp(name, "r8x") == 0) { + *variant = TRICUBE_STREAM_FAST8X; + return TRICUBE_OK; + } + return TRICUBE_ERR_INVALID_ARGUMENT; +} + +static void stream_init_state(uint64_t state[TC_STATE_WORDS], uint64_t seed, const stream_profile *profile) { uint8_t seed_bytes[16]; + seed_to_tweak(seed, seed_bytes); + init_state(state, profile->domain, profile->domain_len, seed_bytes, sizeof(seed_bytes), 64); + absorb_bytes(state, seed_bytes, sizeof(seed_bytes), 0); + permute64(state, profile->init_rounds); +} + +static void stream_fill_bytes(uint64_t state[TC_STATE_WORDS], uint64_t seed, const stream_profile *profile, + uint64_t *counter, uint8_t *out, size_t n_bytes) { + size_t pos = 0; + while (pos < n_bytes) { + uint64_t words[4] = { + *counter, + *counter ^ UINT64_C(0x9E3779B97F4A7C15), + seed + *counter, + seed * UINT64_C(0xD6E8FEB86659FD93) + *counter, + }; + size_t take = n_bytes - pos; + if (take > profile->rate_bytes) { + take = profile->rate_bytes; + } + absorb_words(state, words, 4, *counter + 1U); + permute64(state, profile->step_rounds); + if (profile->use_xmix) { + squeeze_rate64_xmix(state, *counter, out + pos, take); + } else { + squeeze_rate64(state, *counter, out + pos, take); + } + pos += take; + (*counter)++; + } +} + +static int write_stream_profile(FILE *out, uint64_t seed, uint64_t n_bytes, size_t block_size, const stream_profile *profile) { uint64_t state[TC_STATE_WORDS]; uint8_t *block = NULL; uint64_t counter = 0; uint64_t written = 0; - seed_to_tweak(seed, seed_bytes); - init_state(state, domain, sizeof(domain) - 1, seed_bytes, sizeof(seed_bytes), 64); - absorb_bytes(state, seed_bytes, sizeof(seed_bytes), 0); - permute64(state, rounds); + stream_init_state(state, seed, profile); block = (uint8_t *)malloc(block_size); if (block == NULL) { @@ -433,28 +601,11 @@ static int write_stream(FILE *out, uint64_t seed, uint64_t n_bytes, size_t block } while (written < n_bytes) { - size_t pos = 0; - while (pos < block_size) { - uint64_t words[4] = { - counter, - counter ^ UINT64_C(0x9E3779B97F4A7C15), - seed + counter, - seed * UINT64_C(0xD6E8FEB86659FD93) + counter, - }; - size_t take = block_size - pos; - if (take > TC_RATE_BYTES) { - take = TC_RATE_BYTES; - } - absorb_words(state, words, 4, counter + 1U); - permute64(state, half_rounds(rounds)); - squeeze_rate64(state, counter, block + pos, take); - pos += take; - counter++; - } size_t emit = block_size; if ((uint64_t)emit > n_bytes - written) { emit = (size_t)(n_bytes - written); } + stream_fill_bytes(state, seed, profile, &counter, block, emit); if (fwrite(block, 1, emit, out) != emit) { free(block); return 3; @@ -465,6 +616,39 @@ static int write_stream(FILE *out, uint64_t seed, uint64_t n_bytes, size_t block return 0; } +static int write_stream_profile_unbounded(FILE *out, uint64_t seed, size_t block_size, const stream_profile *profile) { + uint64_t state[TC_STATE_WORDS]; + uint8_t *block = NULL; + uint64_t counter = 0; + + stream_init_state(state, seed, profile); + + block = (uint8_t *)malloc(block_size); + if (block == NULL) { + fprintf(stderr, "allocation failed for %zu byte block\n", block_size); + return 2; + } + + while (1) { + stream_fill_bytes(state, seed, profile, &counter, block, block_size); + if (fwrite(block, 1, block_size, out) != block_size) { + free(block); + return ferror(out) ? 3 : 0; + } + } +} + +static int stream_seed_profile(uint64_t seed, uint8_t *out, size_t n_bytes, const stream_profile *profile) { + if (n_bytes != 0 && out == NULL) { + return TRICUBE_ERR_INVALID_ARGUMENT; + } + uint64_t state[TC_STATE_WORDS]; + uint64_t counter = 0; + stream_init_state(state, seed, profile); + stream_fill_bytes(state, seed, profile, &counter, out, n_bytes); + return TRICUBE_OK; +} + static const uint8_t TRICUBE_DEFAULT_DOMAIN_BYTES[] = "TC-TETRA256-" "V" "2"; @@ -490,29 +674,42 @@ int tricube_hash(const uint8_t *data, size_t data_len, uint8_t out[TRICUBE_DIGES } int tricube_stream_seed(uint64_t seed, uint8_t *out, size_t n_bytes) { - if (n_bytes != 0 && out == NULL) { + return tricube_stream_seed_variant(seed, out, n_bytes, TRICUBE_STREAM_BASELINE); +} + +int tricube_stream_seed_variant(uint64_t seed, uint8_t *out, size_t n_bytes, tricube_stream_variant variant) { + const stream_profile *profile = stream_profile_for_variant(variant); + if (profile == NULL) { return TRICUBE_ERR_INVALID_ARGUMENT; } - FILE *fp = tmpfile(); - if (fp == NULL) { - return TRICUBE_ERR_IO; + return stream_seed_profile(seed, out, n_bytes, profile); +} + +int tricube_stream_write(FILE *out, uint64_t seed, uint64_t n_bytes) { + return tricube_stream_write_variant(out, seed, n_bytes, TRICUBE_STREAM_BASELINE); +} + +int tricube_stream_write_variant(FILE *out, uint64_t seed, uint64_t n_bytes, tricube_stream_variant variant) { + if (out == NULL) { + return TRICUBE_ERR_INVALID_ARGUMENT; } - int rc = write_stream(fp, seed, (uint64_t)n_bytes, 1U << 16, 12); - if (rc == 0) { - rewind(fp); - if (fread(out, 1, n_bytes, fp) != n_bytes) { - rc = TRICUBE_ERR_IO; - } + const stream_profile *profile = stream_profile_for_variant(variant); + if (profile == NULL) { + return TRICUBE_ERR_INVALID_ARGUMENT; } - fclose(fp); + int rc = write_stream_profile(out, seed, n_bytes, 1U << 20, profile); return rc == 0 ? TRICUBE_OK : TRICUBE_ERR_IO; } -int tricube_stream_write(FILE *out, uint64_t seed, uint64_t n_bytes) { +int tricube_stream_write_unbounded(FILE *out, uint64_t seed, tricube_stream_variant variant) { if (out == NULL) { return TRICUBE_ERR_INVALID_ARGUMENT; } - int rc = write_stream(out, seed, n_bytes, 1U << 16, 12); + const stream_profile *profile = stream_profile_for_variant(variant); + if (profile == NULL) { + return TRICUBE_ERR_INVALID_ARGUMENT; + } + int rc = write_stream_profile_unbounded(out, seed, 1U << 20, profile); return rc == 0 ? TRICUBE_OK : TRICUBE_ERR_IO; } diff --git a/c/src/tricube_cli.c b/c/src/tricube_cli.c index e5b1b86..42ce4ac 100644 --- a/c/src/tricube_cli.c +++ b/c/src/tricube_cli.c @@ -14,8 +14,9 @@ static void usage(const char *argv0) { " %s hash \n" " %s hash --hex HEX\n" " %s xof --hex HEX --bytes N\n" - " %s stream --seed N --bytes N --out FILE\n", - argv0, argv0, argv0, argv0, argv0, argv0); + " %s stream --seed N --bytes N --out FILE [--variant baseline|fast8x]\n" + " %s stream --seed N --unbounded --out - [--variant baseline|fast8x]\n", + argv0, argv0, argv0, argv0, argv0, argv0, argv0); } static uint64_t parse_u64(const char *value, const char *name) { @@ -167,6 +168,8 @@ static int cmd_stream(int argc, char **argv) { uint64_t seed = 0; uint64_t n_bytes = 0; const char *out_path = NULL; + int unbounded = 0; + tricube_stream_variant variant = TRICUBE_STREAM_BASELINE; for (int i = 2; i < argc; i++) { if (strcmp(argv[i], "--seed") == 0 && i + 1 < argc) { seed = parse_u64(argv[++i], "seed"); @@ -174,11 +177,22 @@ static int cmd_stream(int argc, char **argv) { n_bytes = parse_u64(argv[++i], "bytes"); } else if (strcmp(argv[i], "--out") == 0 && i + 1 < argc) { out_path = argv[++i]; + } else if (strcmp(argv[i], "--unbounded") == 0) { + unbounded = 1; + } else if (strcmp(argv[i], "--variant") == 0 && i + 1 < argc) { + if (tricube_stream_variant_from_name(argv[++i], &variant) != TRICUBE_OK) { + fprintf(stderr, "invalid stream variant; expected baseline or fast8x\n"); + return 2; + } } else { return 2; } } - if (n_bytes == 0 || out_path == NULL) { + if ((!unbounded && n_bytes == 0) || out_path == NULL) { + return 2; + } + if (unbounded && strcmp(out_path, "-") != 0) { + fprintf(stderr, "--unbounded is only allowed with --out -\n"); return 2; } FILE *out = strcmp(out_path, "-") == 0 ? stdout : fopen(out_path, "wb"); @@ -186,7 +200,7 @@ static int cmd_stream(int argc, char **argv) { perror(out_path); return 2; } - int rc = tricube_stream_write(out, seed, n_bytes); + int rc = unbounded ? tricube_stream_write_unbounded(out, seed, variant) : tricube_stream_write_variant(out, seed, n_bytes, variant); if (out != stdout) { fclose(out); } diff --git a/c/src/tricube_fast8x.c b/c/src/tricube_fast8x.c new file mode 100644 index 0000000..3cf7779 --- /dev/null +++ b/c/src/tricube_fast8x.c @@ -0,0 +1,27 @@ +/* + * TriCube fast8x stream entry points. + * + * fast8x is an experimental stream/XOF-oriented variant selected from the + * ablation lab because it improved throughput without the low-bit failures + * seen in faster tested variants. The core permutation, state schedule, and + * xmix extraction path remain centralized in tricube.c so the implementation + * does not fork into a second primitive. + * + * This variant is not a security claim and does not replace the baseline + * TriCube stream path. Use it explicitly through these functions or through + * the CLI option: --variant fast8x. + */ + +#include "tricube.h" + +int tricube_fast8x_stream_seed(uint64_t seed, uint8_t *out, size_t n_bytes) { + return tricube_stream_seed_variant(seed, out, n_bytes, TRICUBE_STREAM_FAST8X); +} + +int tricube_fast8x_stream_write(FILE *out, uint64_t seed, uint64_t n_bytes) { + return tricube_stream_write_variant(out, seed, n_bytes, TRICUBE_STREAM_FAST8X); +} + +int tricube_fast8x_stream_write_unbounded(FILE *out, uint64_t seed) { + return tricube_stream_write_unbounded(out, seed, TRICUBE_STREAM_FAST8X); +} diff --git a/c/tests/test_stream_variants.c b/c/tests/test_stream_variants.c new file mode 100644 index 0000000..d895cf6 --- /dev/null +++ b/c/tests/test_stream_variants.c @@ -0,0 +1,99 @@ +#include "tricube.h" + +#include +#include + +static const uint8_t FAST8X_SEED123_FIRST64[64] = { + 0x33, 0xd4, 0xd2, 0xda, 0x3a, 0xff, 0xf4, 0x06, + 0x18, 0x9a, 0x50, 0xb4, 0x23, 0x22, 0xc0, 0x3b, + 0x2f, 0x1f, 0x9c, 0x41, 0x14, 0x44, 0xd2, 0xb7, + 0x34, 0x75, 0x61, 0xfb, 0x9a, 0x88, 0x2b, 0x3f, + 0x4f, 0xcd, 0x0b, 0xbf, 0x30, 0x64, 0x34, 0xb8, + 0x8a, 0x63, 0x4e, 0x51, 0xd3, 0xf0, 0x26, 0xe1, + 0x46, 0x64, 0x69, 0xad, 0xc8, 0xb6, 0x88, 0xd4, + 0x50, 0x4f, 0x65, 0x35, 0x5b, 0xb6, 0x43, 0xee +}; + +static int expect_distinct(const char *name, const uint8_t *a, const uint8_t *b, size_t n) { + if (memcmp(a, b, n) == 0) { + fprintf(stderr, "%s unexpectedly matched\n", name); + return 1; + } + return 0; +} + +static int expect_deterministic(tricube_stream_variant variant, const char *name) { + uint8_t a[512]; + uint8_t b[512]; + uint8_t c[512]; + if (tricube_stream_seed_variant(123, a, sizeof(a), variant) != TRICUBE_OK) { + fprintf(stderr, "%s stream failed\n", name); + return 1; + } + if (tricube_stream_seed_variant(123, b, sizeof(b), variant) != TRICUBE_OK) { + fprintf(stderr, "%s repeat stream failed\n", name); + return 1; + } + if (tricube_stream_seed_variant(124, c, sizeof(c), variant) != TRICUBE_OK) { + fprintf(stderr, "%s changed-seed stream failed\n", name); + return 1; + } + if (memcmp(a, b, sizeof(a)) != 0) { + fprintf(stderr, "%s same-seed streams differ\n", name); + return 1; + } + if (memcmp(a, c, sizeof(a)) == 0) { + fprintf(stderr, "%s different-seed streams match\n", name); + return 1; + } + for (size_t i = 0; i < sizeof(a); i++) { + if (a[i] != 0) { + return 0; + } + } + fprintf(stderr, "%s stream was all zero\n", name); + return 1; +} + +int main(void) { + int failures = 0; + tricube_stream_variant parsed; + uint8_t baseline[256]; + uint8_t fast8x[256]; + uint8_t fast8x_named[256]; + + failures += expect_deterministic(TRICUBE_STREAM_BASELINE, "baseline"); + failures += expect_deterministic(TRICUBE_STREAM_FAST8X, "fast8x"); + + if (tricube_stream_variant_from_name("baseline", &parsed) != TRICUBE_OK || parsed != TRICUBE_STREAM_BASELINE) { + fprintf(stderr, "baseline variant parsing failed\n"); + failures++; + } + if (tricube_stream_variant_from_name("fast8x", &parsed) != TRICUBE_OK || parsed != TRICUBE_STREAM_FAST8X) { + fprintf(stderr, "fast8x variant parsing failed\n"); + failures++; + } + if (tricube_stream_variant_from_name("r8x", &parsed) != TRICUBE_OK || parsed != TRICUBE_STREAM_FAST8X) { + fprintf(stderr, "r8x variant alias parsing failed\n"); + failures++; + } + if (tricube_stream_variant_from_name("does-not-exist", &parsed) == TRICUBE_OK) { + fprintf(stderr, "invalid variant was accepted\n"); + failures++; + } + + tricube_stream_seed_variant(123, baseline, sizeof(baseline), TRICUBE_STREAM_BASELINE); + tricube_stream_seed_variant(123, fast8x, sizeof(fast8x), TRICUBE_STREAM_FAST8X); + tricube_fast8x_stream_seed(123, fast8x_named, sizeof(fast8x_named)); + failures += expect_distinct("fast8x vs baseline", fast8x, baseline, sizeof(fast8x)); + if (memcmp(fast8x, FAST8X_SEED123_FIRST64, sizeof(FAST8X_SEED123_FIRST64)) != 0) { + fprintf(stderr, "fast8x seed 123 first 64-byte vector changed\n"); + failures++; + } + if (memcmp(fast8x, fast8x_named, sizeof(fast8x)) != 0) { + fprintf(stderr, "named fast8x entry point differs from stream variant\n"); + failures++; + } + + return failures ? 1 : 0; +} diff --git a/docs/comparison.md b/docs/comparison.md index 5a0e355..2853c64 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -6,6 +6,12 @@ SHA-2 is standardized in NIST FIPS 180-4. SHA-3 and SHAKE are standardized in NI TriCube also resembles sponge and ARX systems in broad structure: it absorbs input, permutes state, and squeezes output using addition, xor, and rotation operations. Those ingredients are not novel by themselves. +Ascon is useful here as a documentation model, not as a maturity comparison. +NIST SP 800-232 specifies the Ascon family by first defining the permutation, +state representation, constants, rates, padding, and modes. TriCube's +specification follows that style for clarity, but TriCube is not standardized +and has not received comparable public cryptanalytic review. + ## What May Be Distinctive The part worth studying is the state topology: @@ -22,11 +28,23 @@ That structure may or may not survive cryptanalytic review. The repository keeps - NIST FIPS 180-4, Secure Hash Standard: https://csrc.nist.gov/pubs/fips/180-4/upd1/final - NIST FIPS 202, SHA-3 Standard: https://csrc.nist.gov/pubs/fips/202/final +- NIST SP 800-232, Ascon-Based Lightweight Cryptography Standards for Constrained Devices: https://doi.org/10.6028/NIST.SP.800-232 +- Ascon v1.2, Journal of Cryptology: https://doi.org/10.1007/s00145-021-09398-9 - BLAKE2: https://www.blake2.net/ - BLAKE3 implementation and specification links: https://github.com/BLAKE3-team/BLAKE3 - Keccak Team: https://keccak.team/ - KangarooTwelve: https://keccak.team/kangarootwelve.html - CubeHash reference material: https://ehash.isec.tugraz.at/wiki/CubeHash.html -The comparison status is incomplete until TriCube has a full benchmark set against optimized C implementations and independent cryptanalysis against the reduced-round and full-round construction. - +The current public benchmark comparison is intentionally narrower than a +competitive hash-library shootout. The released TriCube stream path and the +experimental `fast8x` stream path have been compared in the same C ablation +harness: `fast8x` measured 132.655 MiB/s on a 256 MiB stream run, while the +released baseline measured 69.796 MiB/s in that same run. The repo also reports +project controls and battery-screen results for `fast8x`. + +What remains incomplete is the broader comparison that would matter for public +performance claims: optimized C implementations of SHA-256, SHA3/SHAKE, +BLAKE2, BLAKE3, and other mature hash/XOF libraries on the same hardware, plus +independent cryptanalysis against the reduced-round and full-round TriCube +construction. diff --git a/docs/design.md b/docs/design.md index 978d5ba..5d18b8e 100644 --- a/docs/design.md +++ b/docs/design.md @@ -6,6 +6,8 @@ This document describes the current public construction. It does not claim that ## State Model +![TriCube state layout](figures/state-layout.svg) + The internal state has 32 lanes of 64 bits each, for a total of 2048 bits. Twenty-seven lanes are interpreted as the vertices of a 3 x 3 x 3 grid. That grid contains eight unit cube cells. Each cube cell has eight vertices. The remaining five lanes are shell lanes used to carry length, domain, and global coupling information. @@ -20,6 +22,8 @@ where each coordinate is in `{0, 1, 2}`. ## Tetrahedral Decomposition +![TriCube tetrahedral decomposition](figures/tetrahedral-decomposition.svg) + Each cube cell is decomposed into six tetrahedra. For a cube with local vertices numbered from 0 to 7, the local tetrahedra are: ```text @@ -37,6 +41,8 @@ This tetrahedral decomposition is the main structural distinction from a flat AR ## Round Function +![TriCube round flow](figures/round-flow.svg) + A TriCube round applies four steps: 1. Local tetrahedral mixing over each tetrahedron. @@ -52,9 +58,15 @@ TriCube absorbs message blocks into selected lanes, applies partial permutation Digest mode emits 32 bytes. XOF mode emits an arbitrary number of bytes by continuing the squeeze schedule. Stream mode initializes from a seed and emits deterministic stream bytes for statistical testing. -The construction uses domain separation strings internally so hash, XOF, and stream behavior do not share the same state initialization path. Some internal domain strings retain earlier prototype labels so the May 2026 test vectors remain reproducible. The public project name is TriCube. +The default stream path is the released baseline. The C API also exposes an +experimental `fast8x` stream variant for continued testing. `fast8x` is +domain-separated from the baseline, uses fewer rounds in the stream update path, +widens the stream extraction rate, and adds an xmix output layer. It +does not change digest mode, XOF mode, fixed vectors, or the default stream +behavior. + +The construction uses domain separation strings so hash, XOF, and stream behavior do not share the same state initialization path. Some implementation domain strings retain earlier prototype labels so the May 2026 test vectors remain reproducible. The public project name is TriCube. ## What Is Not Claimed TriCube is not a standardized sponge construction, not a proven permutation, not a keyed MAC, and not a validated random bit generator. The current design is a candidate that needs external review and stronger analysis before it can responsibly make cryptographic claims. - diff --git a/docs/figures/round-flow.svg b/docs/figures/round-flow.svg new file mode 100644 index 0000000..17dfc7f --- /dev/null +++ b/docs/figures/round-flow.svg @@ -0,0 +1,62 @@ + + TriCube round flow + The order of operations in one TriCube permutation round. + + + + + + + + One TriCube Permutation Round + All layers update the same 2048-bit state. Tetrahedral, edge, and shell layers are in-place; order is therefore part of the specification. + + + + State S + 32 lanes + 64 bits each + + + + Tetra mix + 48 neighborhoods + ARX, in-place + + + + Edge couple + 54 grid edges + in-place + + + + Shell couple + 5 shell lanes + global feedback + + + + Permute + T[i]=S[9i+5] + mod 32 + + + round constant injection completes the round, then the next round begins + + + + + Mode use + Hash/XOF absorb message blocks, finalize with length and output length, then squeeze output. + Stream mode initializes from a seed/domain, absorbs a counter schedule, permutes, then squeezes deterministic output for testing. + + diff --git a/docs/figures/state-layout.svg b/docs/figures/state-layout.svg new file mode 100644 index 0000000..fa921bc --- /dev/null +++ b/docs/figures/state-layout.svg @@ -0,0 +1,120 @@ + + TriCube state layout + A 3 by 3 by 3 vertex-lane grid plus five shell/global lanes. + + + + + + + + TriCube 2048-bit State Layout + State lanes S[0]..S[31] are 64-bit words. Lanes S[0]..S[26] form a 3 x 3 x 3 vertex grid. Lanes S[27]..S[31] are shell/global lanes. + + + + + + + x + y + z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + S0 + S1 + S2 + S3 + S4 + S5 + S6 + S7 + S8 + + S9 + S10 + S11 + S12 + S13 + S14 + S15 + S16 + S17 + + S18 + S19 + S20 + S21 + S22 + S23 + S24 + S25 + S26 + + + + + Shell/global lanes + These lanes are ordinary 64-bit state words used for global coupling. + They are not external entropy sources. + S27 + S28 + S29 + S30 + S31 + + + Coordinate rule + index(x,y,z) = x + 3(y + 3z) + x,y,z are each 0, 1, or 2 + lane word size = 64 bits + + diff --git a/docs/figures/tetrahedral-decomposition.svg b/docs/figures/tetrahedral-decomposition.svg new file mode 100644 index 0000000..15ea633 --- /dev/null +++ b/docs/figures/tetrahedral-decomposition.svg @@ -0,0 +1,61 @@ + + TriCube cube-to-tetrahedra decomposition + A unit cube with eight local vertices and the six tetrahedral neighborhoods used by TriCube. + + + + + Cube Cell Decomposition into Six Tetrahedra + Each 2 x 2 x 2 cell uses the same local schedule. The six tetrahedra share vertices, so in-place updates make the listed order part of the specification. + + + + + + + + + + + + + + + + + v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7 + + The long body diagonal v0-v7 anchors all six local tetrahedra. + + + + + Local tetrahedron order + 1. (v0, v1, v3, v7) + 2. (v0, v3, v2, v7) + 3. (v0, v2, v6, v7) + 4. (v0, v6, v4, v7) + 5. (v0, v4, v5, v7) + 6. (v0, v5, v1, v7) + + Global order: cz, cy, cx over {0,1}, + then the six local tetrahedra above. + + diff --git a/docs/limitations.md b/docs/limitations.md index e384456..f2f9dd0 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -15,16 +15,38 @@ TriCube is not ready for security use. The current project should be treated as ## Engineering Limitations - The C implementation is portable C11 but not heavily optimized. -- Performance is below mature optimized hash implementations. +- The experimental `fast8x` stream path improves same-harness stream + throughput over the released baseline, from 69.796 MiB/s to 132.655 MiB/s + in the 256 MiB ablation run. The missing performance comparison is against + optimized SHA-2, SHA-3/SHAKE, BLAKE2, and BLAKE3 libraries on the same + hardware. - The context API currently buffers input before finalization, so very large file hashing should prefer CLI/file-oriented paths until streaming internals are hardened. - Windows CI and wheel packaging need regular verification before public package release. ## Research Limitations - Reduced-round attacks are incomplete. -- Differential and rotational analysis are incomplete. +- Formal differential trail analysis is incomplete. +- Formal rotational-distinguisher analysis is incomplete. - Algebraic structure analysis is incomplete. +- The current white-box work is limited to word-level schedule dependency and + does not yet provide SAT, MILP, SMT, Gröbner, or trail-search results. +- The specification is now written as the normative description, but it still + needs an independent clean-room implementation to prove that a reviewer can + implement it without reading `c/src/tricube.c`. - Birthday collision experiments are small relative to cryptographic claims. - Long multi-seed PractRand, TestU01 Crush reruns, TestU01 BigCrush, larger NIST STS, and SmokeRand full campaigns remain open. +## Terminology Correction + +Earlier project notes used shorter phrases such as differential probes, +rotational probes, and algebraic screens. The public documentation now uses +more precise names: black-box differential diffusion probe, black-box +rotational relation probe, small black-box algebraic degree screen, +collision/birthday sanity check, overlap/fork stream uniqueness screen, +black-box state-recovery/predictability screen, and low-bit diagnostic screen. +These are development gates intended to find obvious failures before deeper +analysis. Formal differential, rotational, algebraic, collision, and +state-recovery cryptanalysis remains future work. + These limitations are intentional in the public documentation. They prevent the project from implying a stronger security status than the evidence supports. diff --git a/docs/reproducibility.md b/docs/reproducibility.md index 4fec870..dc81534 100644 --- a/docs/reproducibility.md +++ b/docs/reproducibility.md @@ -1,6 +1,6 @@ # Reproducibility -This repository is organized so a reviewer can rebuild the C implementation, install the Python package, run tests, regenerate vectors, and rerun the public benchmark scripts without relying on private machine paths. +This repository is organized so a reviewer can rebuild the C implementation, install the Python package, run tests, regenerate vectors, and rerun the public benchmark scripts without relying on machine-specific paths. ## Environment @@ -13,12 +13,19 @@ Recommended baseline: Optional external tools: +- Z3; +- SageMath; +- CryptoMiniSat; - PractRand; - Dieharder; - TestU01 wrappers; - NIST STS; - SmokeRand. +These tools are not bundled in the TriCube repository or PyPI package. Install +them separately and follow their upstream licenses. See +[THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md) for the public notice table. + ## Rebuild ```bash @@ -49,9 +56,57 @@ The C and Python implementations should agree on the fixed vectors. ```bash python benchmarks/bench_hash_sizes.py --quick python benchmarks/bench_throughput.py --quick -python benchmarks/bench_stream.py --bytes 1048576 +python benchmarks/bench_stream.py --bytes 1048576 --variants baseline,fast8x +python benchmarks/bench_stream.py --bytes 268435456 --variants baseline,fast8x --skip-python +``` + +The `fast8x` stream variant is an experimental optimized path. It is not the +default and does not replace the baseline stream. The ablation record explaining +why it was added is in [tests/ablation_lab/](../tests/ablation_lab/). The +public snapshot reports `fast8x` at 132.655 MiB/s on a 256 MiB stream run, +compared with 69.796 MiB/s for the released baseline in the same harness. That +is a same-machine comparison against the TriCube baseline, not a claim that +`fast8x` has been competitively benchmarked against optimized SHA-2, SHA-3, +BLAKE2, or BLAKE3 implementations. + +## Development Screens + +The compact development screens are reproducible without storing large raw +streams: + +```bash +python tests/crypto_analysis/run_all_screens.py \ + --profile quick \ + --variants baseline,fast8x \ + --out tests/crypto_analysis/results/quick-latest + +python tests/crypto_analysis/screens/low_bit_diagnostics.py \ + --variants baseline,fast8x \ + --bytes 16777216 \ + --out tests/crypto_analysis/results/low-bit-latest + +python tests/crypto_analysis/screens/whitebox_round_model.py \ + --rounds 24 \ + --out tests/crypto_analysis/results/whitebox-latest ``` +These screens write compact JSON, Markdown, and CSV summaries. They do not +replace external batteries or formal cryptanalysis. + +## Optional Analysis Tooling + +Check local solver and battery availability: + +```bash +python tests/crypto_analysis/tooling/check_tools.py +python tests/crypto_analysis/tooling/z3_smoke.py +sage -python tests/crypto_analysis/tooling/sage_smoke.py +``` + +Missing optional tools are reported as `NOT_INSTALLED`. They are not repository +test failures. See [docs/tooling.md](tooling.md) for the tool plan and the +custom TriCube model work required before solver results should be interpreted. + ## External Batteries External batteries are not bundled. The scripts in `tools/` assume a locally built `c/build/tricube` and available external tools on `PATH`. diff --git a/docs/security-status.md b/docs/security-status.md index f73c5aa..75e6b72 100644 --- a/docs/security-status.md +++ b/docs/security-status.md @@ -2,7 +2,7 @@ TriCube is experimental and should not be used for security-critical work. -The current implementation has deterministic tests, fixed vectors, C/Python agreement checks, internal statistical checks, and several external-battery evaluations. Those results are useful for engineering triage. They are not a proof of security. +The current implementation has deterministic tests, fixed vectors, C/Python agreement checks, project statistical checks, and several external-battery evaluations. Those results are useful for engineering triage. They are not a proof of security. ## Known Evidence @@ -18,20 +18,55 @@ The current public repository includes: - TestU01 Crush PASS, 144/144; - Dieharder battery result of 109 PASS, 2 WEAK, and 0 FAIL; - PractRand 1 GiB WARN because of unresolved low-bit warnings; +- experimental `fast8x` stream benchmark result of 132.655 MiB/s on a 256 + MiB run, compared with 69.796 MiB/s for the released baseline in the same + ablation harness; +- `fast8x` screening evidence through PractRand 1 GiB, SmokeRand express 7/7, + TestU01 SmallCrush 15/15, and 16 MiB sanity probes; +- a first white-box word-dependency model showing full 32-lane dependency by + round 2 at 64-bit lane granularity; - scripts for PractRand, Dieharder, TestU01, and NIST STS workflows. The PractRand 1 GiB evaluation reached the final level with no anomalies in 2050 final-level results, but earlier levels flagged suspicious or unusual low-bit behavior. That result is classified as WARN. +The structural probes in the result summary are black-box development screens: +the black-box differential diffusion probe, black-box rotational relation +probe, small black-box algebraic degree screen, collision/birthday sanity +check, overlap/fork stream uniqueness screen, black-box +state-recovery/predictability screen, and low-bit diagnostic screen. They are +useful for finding obvious diffusion, rotation, overlap, prediction, collision, +and low-bit warning patterns. They are not formal differential cryptanalysis, +formal rotational cryptanalysis, algebraic cryptanalysis, collision-resistance +evidence, or state-recovery proofs. + +The white-box round model is stronger than a black-box output probe in one +narrow way: it inspects the specified tetrahedron, edge, shell, and permutation +schedule directly. Its current result is only a word-level reachability result. +It does not model bit-level differential probabilities, rotational trails, +algebraic equations, or attack cost. + +The reproducible versions of these screens now live in +`tests/crypto_analysis/`. They record compact JSON, Markdown, and CSV +summaries with branch, commit, command, seed, sample count, byte count, and +PASS/WARN/FAIL/BLOCKED/NOT_RUN status labels. + +The open-source tooling plan is in [tooling.md](tooling.md). The intended path +for deeper work is to build a verified reduced-round TriCube model and then use +standard solvers and algebra systems such as Z3, SageMath, SAT solvers, and +cryptanalysis frameworks where they fit. The repo does not treat custom +black-box screens as substitutes for those tools. + ## Unknowns The following work has not been completed: - independent cryptanalysis; - reduced-round attack study; -- differential trail analysis; -- rotational symmetry analysis; -- algebraic degree and invariant analysis; -- state-recovery attempts; +- formal differential trail analysis; +- formal rotational-distinguisher analysis; +- algebraic degree and invariant analysis at useful scale; +- SAT/SMT/MILP or Gröbner-style reduced-round modeling; +- state-recovery attack attempts; - collision and near-collision search at meaningful scales; - domain-separation review; - multi-seed long-run PractRand, TestU01 Crush reruns, and TestU01 BigCrush campaigns; diff --git a/docs/specification.md b/docs/specification.md new file mode 100644 index 0000000..53bae35 --- /dev/null +++ b/docs/specification.md @@ -0,0 +1,552 @@ +# TriCube Specification + +This document is the normative description of the current public TriCube +baseline construction. A conforming implementation must produce the test +vectors in Section 12 and follow the state layout, round transformation, +absorb/finalize/squeeze rules, domain tags, and byte ordering specified here. +The C implementation in `c/src/tricube.c` is the current reference +implementation, but the intended direction is specification first: +implementations should be checked against this document and the fixed vectors, +not treated as the only source of meaning. + +This document is not a security proof and it is not a standard. + +Established primitive families such as Ascon specify exact state size, round +transformations, constants, modes, rates, and padding before presenting security +claims or implementation results. TriCube follows that documentation style for +clarity. TriCube is not standardized and has not received comparable public +cryptanalytic review. + +## 1. Status and Scope + +TriCube is an experimental hash, extendable-output function, and deterministic +research stream generator. This specification covers the current public +baseline construction unless a variant is explicitly named. The words "must," +"shall," and "required" identify behavior that is part of the specified +construction. Implementation notes are labeled separately. + +This document does not specify a production cryptographic standard. It does not +establish collision resistance, preimage resistance, pseudorandomness, or +security for real deployments. TriCube must not be used for passwords, +signatures, message authentication, key derivation, encryption, consensus, or +any other security-critical purpose. + +The optimized `fast8x` stream path is specified only as an experimental stream +variant. Rejected ablation-lab variants such as `fast8x512`, `fast4x`, and +other high-throughput candidates are not public baseline schemes in this +repository. + +## 1.1 Diagrams + +The figures below are explanatory, not a replacement for the tables and +pseudocode. If a diagram and the text disagree, the text is normative. + +- [State layout](figures/state-layout.svg) +- [Tetrahedral decomposition](figures/tetrahedral-decomposition.svg) +- [Round flow](figures/round-flow.svg) + +## 2. Parameters + +| Parameter | Baseline value | +|---|---:| +| State size | 2048 bits | +| Word size | 64 bits | +| Number of state lanes | 32 | +| Vertex-grid lanes | 27, lanes `S[0]` through `S[26]` | +| Shell/global lanes | 5, lanes `S[27]` through `S[31]` | +| Vertex grid | `3 x 3 x 3` vertices | +| Cube cells | `2 x 2 x 2`, total 8 cube cells | +| Tetrahedra per cube cell | 6 | +| Tetrahedral neighborhoods per round | 48 | +| Edge neighborhoods per round | 54 grid-adjacent edges | +| Schedule period | 24 rounds for precomputed schedules | +| Message absorb block | 64 bytes | +| Baseline squeeze rate | 192 bytes | +| Digest output | 256 bits, 32 bytes | +| XOF output | arbitrary byte length | +| Stream output | deterministic research stream | +| Baseline hash/XOF rounds | 16 finalization rounds | +| Hash/XOF per-message-block rounds | `half_rounds(16) = 8` | +| Empty-message block | 9-byte block beginning with `0x80` | +| Finalization trailer | 33 bytes | +| Word byte order | little-endian | +| Message length encoding | low 64-bit little-endian byte length plus a zero high word | +| Output-length encoding | 64-bit little-endian encoded output length in trailer | +| Hash domain tag | `TC-TETRA256-V2` | +| Stream baseline domain tag | `TC-TETRA256-V2/STREAM` | +| State-initialization prefix | `TriCube-Tetra256-v2-native` | +| Stream seed encoding | 64-bit little-endian seed in a 16-byte tweak, high 8 bytes zero | + +The `V2` text in domain tags and initialization strings is a legacy +compatibility tag from development. It is part of the current vector-compatible +implementation. The public project name is TriCube. + +All 64-bit additions are modulo `2^64`. `rotl64(x, r)` is a left rotation of a +64-bit word by `r mod 64`. + +Round constants and the initial vector are generated at initialization with a +SplitMix64 stream: + +- `ROUND_CONSTANTS[0..767] = SplitMix64(seed = 0x5452494355424556)`; +- `IV[0..31] = SplitMix64(seed = 0x5445545241435542)`. + +The SplitMix64 step is: + +```text +x = x + 0x9E3779B97F4A7C15 +z = x +z = (z xor (z >> 30)) * 0xBF58476D1CE4E5B9 +z = (z xor (z >> 27)) * 0x94D049BB133111EB +return z xor (z >> 31) +``` + +## 3. State Layout + +![TriCube state layout](figures/state-layout.svg) + +The state is an array of 32 unsigned 64-bit words: + +```text +S[0], S[1], ..., S[31] +``` + +Lanes `0..26` are vertex lanes in a `3 x 3 x 3` grid. Coordinates use +`x, y, z in {0,1,2}` and map to lanes by: + +```text +index(x, y, z) = x + 3 * (y + 3 * z) +``` + +| Lane | Coordinate | +|---:|---| +| 0 | `(0,0,0)` | +| 1 | `(1,0,0)` | +| 2 | `(2,0,0)` | +| 3 | `(0,1,0)` | +| 4 | `(1,1,0)` | +| 5 | `(2,1,0)` | +| 6 | `(0,2,0)` | +| 7 | `(1,2,0)` | +| 8 | `(2,2,0)` | +| 9 | `(0,0,1)` | +| 10 | `(1,0,1)` | +| 11 | `(2,0,1)` | +| 12 | `(0,1,1)` | +| 13 | `(1,1,1)` | +| 14 | `(2,1,1)` | +| 15 | `(0,2,1)` | +| 16 | `(1,2,1)` | +| 17 | `(2,2,1)` | +| 18 | `(0,0,2)` | +| 19 | `(1,0,2)` | +| 20 | `(2,0,2)` | +| 21 | `(0,1,2)` | +| 22 | `(1,1,2)` | +| 23 | `(2,1,2)` | +| 24 | `(0,2,2)` | +| 25 | `(1,2,2)` | +| 26 | `(2,2,2)` | + +Lanes `S[27]..S[31]` are shell/global lanes. They do not correspond to grid +vertices. They participate in absorption and round mixing through the shell +coupling layer. + +## 4. Tetrahedral Decomposition + +![TriCube tetrahedral decomposition](figures/tetrahedral-decomposition.svg) + +The vertex grid contains eight unit cube cells. A cube at cell coordinate +`(cx, cy, cz)` has local vertices: + +```text +v0 = index(cx, cy, cz) +v1 = index(cx+1, cy, cz) +v2 = index(cx, cy+1, cz) +v3 = index(cx+1, cy+1, cz) +v4 = index(cx, cy, cz+1) +v5 = index(cx+1, cy, cz+1) +v6 = index(cx, cy+1, cz+1) +v7 = index(cx+1, cy+1, cz+1) +``` + +Each cube is decomposed into these six local tetrahedra: + +```text +(v0, v1, v3, v7) +(v0, v3, v2, v7) +(v0, v2, v6, v7) +(v0, v6, v4, v7) +(v0, v4, v5, v7) +(v0, v5, v1, v7) +``` + +The tetrahedra overlap through shared vertices. The implementation processes +all 48 tetrahedra in a deterministic order and updates the state in place, so +shared vertices carry order-dependent effects within a round. + +The global tetrahedron order is generated by iterating `cz`, then `cy`, then +`cx`, each over `{0,1}`, and then iterating the six local tetrahedra above. + +## 5. Round Function + +![TriCube round flow](figures/round-flow.svg) + +The round function is an in-place transformation on all 32 lanes. For +`round r = 0 .. R-1`, with `sr = r mod 24`, the implementation applies: + +```text +for each tetrahedron t in 0..47: + tetrahedral_mix(S, sr, r, t) + +for each grid edge e in 0..53: + edge_couple(S, sr, e) + +for each shell coupling j in 0..4: + shell_couple(S, sr, j) + +for i in 0..31: + T[i] = S[(9*i + 5) mod 32] +S = T + +for i in 0..31: + S[i] = S[i] xor rotl64(RC[(32*sr + i) mod 768] + i + r, + ((5*i + sr) mod 61) + 1) +``` + +The current implementation precomputes the 24-round schedule from these rules +for speed. The schedule is an implementation optimization and must not change +the resulting transformation. + +## 6. Tetrahedral Mixing Layer + +For tetrahedron index `t`, the base lanes `(a,b,c,d)` are taken from the +tetrahedron table. Orientation changes by round and tetrahedron index: + +```text +if ((r + t) & 1): swap b and d +if ((r + t) & 2): swap a and c +``` + +The rotation set is selected by `(r + t) mod 4`: + +| Selector | Rotations `(r0,r1,r2,r3)` | +|---:|---| +| 0 | `(17, 29, 41, 53)` | +| 1 | `(23, 31, 47, 59)` | +| 2 | `(19, 37, 43, 61)` | +| 3 | `(13, 27, 39, 55)` | + +Round constants are: + +```text +base = (32*r + t) mod 768 +rc0 = RC[base] +rc1 = RC[(base + 7) mod 768] +rc2 = RC[(base + 13) mod 768] +rc3 = RC[(base + 21) mod 768] +``` + +The in-place tetrahedral ARX update is: + +```text +x0 = S[a]; x1 = S[b]; x2 = S[c]; x3 = S[d] + +x0 = x0 + x1 + rc0 +x3 = rotl64(x3 xor x0, r0) +x2 = x2 + x3 + rc1 +x1 = rotl64(x1 xor x2, r1) +x0 = x0 + x1 + (rc2 xor t) +x3 = rotl64(x3 xor x0, r2) +x2 = x2 + x3 + (rc3 + r) +x1 = rotl64(x1 xor x2, r3) + +S[a] = x0; S[b] = x1; S[c] = x2; S[d] = x3 +``` + +Because tetrahedra share vertices and updates are in place, tetrahedron order is +part of the specification. + +## 7. Edge-Coupling Layer + +Edges are all axis-adjacent pairs in the `3 x 3 x 3` vertex grid. They are +enumerated by iterating `z`, then `y`, then `x`, and for each vertex adding the +positive `x`, positive `y`, and positive `z` neighbor when present. This yields +54 edges. + +For edge index `e` with lanes `(a,b)`, define: + +```text +rc = RC[(37*r + 5*e) mod 768] +rot0 = ((e + 3*r) mod 61) + 1 +rot1 = ((7*e + r) mod 61) + 1 +left = S[a] +right = S[b] +S[a] = left + rotl64(right xor rc, rot0) +S[b] = right xor rotl64(S[a] + rc + e, rot1) +``` + +The layer updates in place, so edge order matters. + +## 8. Shell/Global Lanes + +The shell lanes are: + +```text +shell_lanes = [27, 28, 29, 30, 31] +``` + +For shell index `j = 0..4`: + +```text +lane = shell_lanes[j] +vertex = (5*r + 7*j) mod 27 +opposite = (11*vertex + 3) mod 27 +rc = RC[(11*r + 17*j) mod 768] +rot = ((r + 9*j) mod 61) + 1 + +S[lane] = S[lane] + S[vertex] + rc +S[opposite] = S[opposite] xor rotl64(S[lane] xor S[vertex], rot) +``` + +The shell lanes are state words used for global coupling and metadata mixing. +They are not treated as entropy sources. + +## 9. Lane Permutation + +After tetrahedral, edge, and shell coupling, the implementation applies: + +```text +T[i] = S[(9*i + 5) mod 32] for i in 0..31 +S = T +``` + +Then each lane receives a round-dependent constant injection: + +```text +S[i] = S[i] xor rotl64(RC[(32*r + i) mod 768] + i + r, + ((5*i + r) mod 61) + 1) +``` + +The precomputed schedule stores this as `src`, `rc`, and `rot` per round and +lane. + +## 10. Absorb, Finalize, and Squeeze + +### Initialization + +Initialization starts with `S = IV`. The initialization header is: + +```text +"TriCube-Tetra256-v2-native" +uint16_le(domain_len) +uint16_le(tweak_len) +uint32_le(encoded_outlen) +domain bytes +tweak bytes +``` + +The header is split into 64-byte blocks. Each block is absorbed with +`absorb_bytes`, then `permute64(S, 4)` is applied. + +The absorption of eight 64-bit words `W[j]` at block index `b` is: + +```text +lane = (7*j + 5*b) mod 32 +mate = (lane + 11 + j) mod 32 +shell = shell_lanes[(j + b) mod 5] + +S[lane] = S[lane] xor (W[j] + 0xD6E8FEB86659FD93 + b + j) +S[mate] = S[mate] + rotl64(W[j] xor S[lane], ((11*j + b) mod 63) + 1) +S[shell] = S[shell] xor rotl64(S[lane] + S[mate] + W[j], + ((13*j + 7) mod 63) + 1) +``` + +`absorb_bytes` reads each `W[j]` as a little-endian 64-bit word padded with +zero bytes when the input block is shorter than 64 bytes. + +### Hash Mode + +Hash mode uses domain tag `TC-TETRA256-V2`, encoded output length `32`, and 16 +finalization rounds. + +For each nonempty message block: + +```text +absorb_bytes(S, block, block_index) +permute64(S, 8) +``` + +For the empty message, a 9-byte block beginning with `0x80` is absorbed and +then `permute64(S, 8)` is applied. + +Finalization absorbs a 33-byte trailer: + +```text +bytes 0..15 = uint128_le(message_length_bytes), high 64 bits currently zero +bytes 16..23 = uint64_le(encoded_outlen) +bytes 24..31 = uint64_le(number_of_message_blocks) +byte 32 = 0x80 +``` + +After the trailer: + +```text +absorb_bytes(S, trailer, block_index + 1) +permute64(S, 16) +squeeze_rate64(S, counter=0, out, 32) +``` + +### XOF Mode + +XOF mode uses the same domain tag and finalization structure as hash mode, but +with encoded output length `0`. Output is produced in chunks of at most 192 +bytes by `squeeze_rate64`. + +If more output is needed after a squeeze chunk, the state absorbs: + +```text +words = [ + counter, + encoded_outlen, + message_length_bytes, + counter xor 0xA5A5A5A5A5A5A5A5 +] +``` + +with block index `message_block_count + counter + 2`, then applies +`permute64(S, 8)`. + +### Baseline Stream Mode + +Stream mode is a deterministic research stream, not a random bit generator for +security use. + +Baseline stream mode uses domain tag `TC-TETRA256-V2/STREAM`, a 16-byte seed +tweak, 12 initialization rounds, 6 rounds per stream step, and 192 output bytes +per state update. The seed tweak is: + +```text +uint64_le(seed) || 8 zero bytes +``` + +Stream initialization: + +```text +init_state(S, stream_domain, seed_tweak, outlen=64) +absorb_bytes(S, seed_tweak, block_index=0) +permute64(S, 12) +``` + +For each stream counter `c`: + +```text +words = [ + c, + c xor 0x9E3779B97F4A7C15, + seed + c, + seed * 0xD6E8FEB86659FD93 + c +] +absorb_words(S, words, 4, c + 1) +permute64(S, 6) +squeeze_rate64(S, c, output, up to 192 bytes) +``` + +The CLI writes stream data in 1 MiB chunks, but chunking does not affect output. + +## 11. Baseline vs Experimental Variants + +Only the baseline and `fast8x` are implemented in this repository. + +| Variant | Purpose | Init rounds | Step rounds | Output rate | Output layer | Status | +|---|---|---:|---:|---:|---|---| +| `baseline` | Preserved public reference stream | 12 | 6 | 192 bytes | `squeeze_rate64` | default | +| `fast8x` | Faster stream/XOF testing path | 8 | 4 | 256 bytes | `squeeze_rate64_xmix` | experimental opt-in | + +The `fast8x` variant is domain-separated with tag +`TC-TETRA256-V2/STREAM/FAST8X`. It is available through the C API and CLI, but +it does not replace the baseline. Faster ablation-lab variants are not public +reference schemes in this repository. + +## 12. Test Vectors + +These vectors are produced by the current C CLI. + +| Case | Input | Output | +|---|---|---| +| Hash empty | empty byte string | `7fcaaa35165277bcaca583e23ef1d3545705e14d39f3ed7a802b1275d920cf49` | +| Hash `abc` | `616263` | `779403a9c748fc3213493953fc17309367b37161c00dc19059c14db63774e11e` | +| XOF `abc`, 64 bytes | `616263` | `6118c4b547c28533b968d6b7fc0b171817d8d9b1ced9e8c832dc7903e73baadcb6ce828c5f50d697bffddaef772c0b89d9314970df8cb5f15e95af6143e0667c` | +| Baseline stream seed 123, first 64 bytes | seed `123` | `9ccace5701711cc2b47c06bf5a1a2b2b0bb5df09a8fb47a473ea18e34fef3695b7e233322a0b04a691402be1c630f070d954848a5c4013e8c745968288216d98` | +| Experimental fast8x stream seed 123, first 64 bytes | seed `123`, `--variant fast8x` | `33d4d2da3afff406189a50b42322c03b2f1f9c411444d2b7347561fb9a882b3f4fcd0bbf306434b88a634e51d3f026e1466469adc8b688d4504f65355bb643ee` | + +The packaged vector file is `python/src/tricube/data/tricube_vectors.json`. + +## 13. Implementation Notes + +The standalone C implementation is the public reference implementation in this +repository. The Python package provides a reference implementation and tests +against fixed vectors. + +Precomputed schedules in `c/src/tricube.c` are implementation optimizations. +They are derived from the algorithms above and must not change public outputs. + +The context API currently buffers message input before finalization. This is an +engineering limitation, not a primitive-level feature. + +## 14. Security Considerations + +TriCube has no security proof, collision-resistance proof, preimage-resistance +proof, or independent cryptanalytic review. + +The current project has not completed formal differential cryptanalysis, +rotational cryptanalysis, algebraic analysis, trail search, SAT/MILP analysis, +Gröbner-basis analysis, reduced-round attacks, side-channel review, or +production implementation review. + +Statistical batteries and black-box probes can find problems. Passing them does +not establish cryptographic security. + +## 15. Current Development Probes and What They Are Not + +The public result summary uses several probe and screen names. These are not +part of the TriCube primitive specification, but they are documented here +because their names can otherwise be misunderstood. + +The black-box differential diffusion probe flips selected input differences and +measures output Hamming distance, output-bit bias, and repeated output +differences across selected reduced-round settings. It does not model +differential propagation inside the round function. It does not search +differential trails, compute maximum differential probability, or replace +differential cryptanalysis. + +The black-box rotational relation probe applies selected word rotations and +measures rotational distance and exact preserved-relation counts across selected +reduced-round settings. It does not prove resistance to rotational +distinguishers. + +The small black-box algebraic degree screen samples selected variables and +output bits and estimates algebraic-normal-form degree under a limited budget. +It does not perform SAT, MILP, Gröbner-basis, full ANF, invariant, or integral +analysis. + +The black-box state-recovery/predictability screen measures simple predictor +behavior against random baselines. It does not model or recover the internal +2048-bit state. + +The overlap/fork stream uniqueness screen checks for repeated blocks across +related streams. It does not prove stream independence. + +The collision and birthday screens compare small practical sample counts +against birthday expectations. They do not establish collision resistance. + +The detailed current probe descriptions are maintained in +`docs/testing.md`. Passing any of these probes means that no obvious failure was +found under the tested budget. It is not a security validation. + +## References + +- NIST SP 800-232, *Ascon-Based Lightweight Cryptography Standards for + Constrained Devices: Authenticated Encryption, Hash, and Extendable Output + Functions*. +- Dobraunig, Eichlseder, Mendel, and Schläffer, "Ascon v1.2: Lightweight + Authenticated Encryption and Hashing," *Journal of Cryptology*, 2021. diff --git a/docs/testing.md b/docs/testing.md index d4bb5a2..db12856 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,16 +1,12 @@ # Testing -TriCube uses several layers of testing. Each layer answers a different question. +TriCube uses three different kinds of tests, and they should not be collapsed into one security claim. Unit tests check implementation behavior. Statistical batteries look for detectable non-randomness in output streams. The black-box probes in this repository are development gates: they can find obvious failures or warning patterns, but they do not replace white-box cryptanalysis. -Unit tests verify deterministic behavior, output lengths, vector agreement, CLI behavior, and basic stream separation. They do not evaluate security. +The current public wording is intentionally conservative. A `PASS` means that no issue was detected under the stated test budget. It does not mean that TriCube is secure, collision resistant, preimage resistant, or safe for production cryptography. -Internal statistical checks measure byte entropy, bit balance, serial correlation, chi-square behavior, block entropy, avalanche response, throughput, and simple repetition/cycle smoke checks. These are useful for finding obvious flaws. +## Local Implementation Tests -External batteries such as PractRand, Dieharder, TestU01, SmokeRand, and NIST STS are stronger statistical screens. They can identify suspicious output structure, but they still do not prove cryptographic security. - -Cryptanalytic analysis is a separate requirement. TriCube still needs reduced-round, differential, rotational, algebraic, collision-search, fork/overlap, and state-recovery studies. - -## Local Tests +Run the C and Python checks before interpreting any statistical result: ```bash make -C c test @@ -18,6 +14,8 @@ python -m pip install -e ".[test]" pytest -q ``` +These tests cover deterministic behavior, output lengths, test-vector agreement, C/Python agreement, CLI behavior, domain separation, and package import behavior. They are implementation tests, not security tests. + ## CLI Smoke Tests ```bash @@ -25,11 +23,277 @@ c/build/tricube self-test c/build/tricube hash --hex 616263 c/build/tricube xof --hex 616263 --bytes 64 c/build/tricube stream --seed 123 --bytes 1024 --out results/tmp/stream.bin +c/build/tricube stream --seed 123 --bytes 1024 --out results/tmp/stream-fast8x.bin --variant fast8x +``` + +The default stream path is the released baseline. The `fast8x` stream variant is experimental and must be requested explicitly. The compact ablation record is in [tests/ablation_lab/](../tests/ablation_lab/). + +## Status Labels + +TriCube result tables use the following labels: + +| Label | Meaning | +|---|---| +| `PASS` | No issue was detected under this test budget. | +| `WARN` | The result is suspicious or needs follow-up, but is not a conclusive failure. | +| `FAIL` | The test or tool reported a clear failure under its criteria. | +| `BLOCKED` | A missing tool, missing binary, input exhaustion, timeout, or harness issue prevented the test from completing. | +| `NOT_RUN` | The test was available in principle but was not attempted in that run. | + +EOF, SIGPIPE, timeout, and input exhaustion are harness conditions. They must not be recorded as statistical `PASS` results. + +## Testing Methodology and Limits + +This section explains what each test family is normally meant to detect, what TriCube currently does, what it does not prove, and what stronger work would require. + +### Avalanche and Diffusion Tests + +Avalanche tests check whether small input changes produce broad output changes. The usual engineering target for a 256-bit digest is that flipping one input bit changes roughly half the output bits. This is related to the strict avalanche criterion introduced by Webster and Tavares. + +TriCube currently measures output Hamming distance, output-bit flip probability, and bit-influence spread for selected message, seed, and stream perturbations. The current black-box diffusion screens are implemented in [tests/crypto_analysis/run_all_screens.py](../tests/crypto_analysis/run_all_screens.py). In the quick profile, each variant is checked with 64 samples for each selected delta class. The compact output is written to `differential_screen.csv`, `differential_screen.md`, `summary.json`, and `summary.md`. + +This is a useful development check, but it does not model the round function internally. A stronger version would include per-round hooks, explicit difference-propagation models for the ARX layers, and reduced-round trail search. + +### Differential Cryptanalysis vs. the Current Black-Box Differential Diffusion Probe + +Differential cryptanalysis studies how input differences propagate through a primitive and searches for differential characteristics or trails with probabilities high enough to exploit. The classic Biham-Shamir DES work is the reference point for the term. + +TriCube currently runs a **black-box differential diffusion probe**. It applies fixed and random seed deltas, then compares the first 32 bytes of deterministic stream output. The fixed delta classes are: + +- single bit; +- single byte; +- single 64-bit word; +- all-low-bit mask; +- checkerboard mask; +- high-bit mask; +- adjacent-bit mask. + +The random delta classes are low-weight, medium-weight, and full-weight deltas. The metrics are mean/min/max output Hamming distance, maximum output-bit bias, a chi-square score for bit flips, repeated output differences, and the top repeated difference count. The screen accepts variants such as `baseline` and `fast8x`, seeds, sample counts, and an output directory: + +```bash +python tests/crypto_analysis/run_all_screens.py \ + --profile quick \ + --variants baseline,fast8x \ + --out tests/crypto_analysis/results/quick-latest +``` + +This is not formal differential cryptanalysis. It does not search trails, compute maximum differential probability, build a white-box propagation model, or establish resistance to differential attacks. A real differential program for TriCube would need to model modular addition, XOR, rotation, tetrahedral lane schedules, constants, and output extraction, then search reduced-round trails and estimate attack complexity. + +### Rotational Cryptanalysis vs. the Current Black-Box Rotational Relation Probe + +Rotational cryptanalysis studies whether rotations of input words propagate through ARX systems in a structured way. Khovratovich and Nikolic's ARX work is the relevant reference family. + +TriCube currently runs a **black-box rotational relation probe**. It tests rotations `1, 2, 3, 4, 5, 7, 8, 13, 16, 17, 31, 32, 33, 47, 63`. For each rotation, it rotates the seed, generates the first 32 bytes of stream output, and compares that output against a word-rotated version of the unrotated output. The metrics are exact rotational relation count, mean rotational distance, min/max distance, maximum output-bit bias, and a z-score for mean distance. + +This is not formal rotational cryptanalysis. It does not prove resistance to rotational distinguishers. A stronger version would need a white-box model of how rotations move through modular additions, xors, constants, lane schedules, shell/global lanes, and output extraction. + +### Algebraic Analysis vs. the Current Small Black-Box Algebraic Degree Screen + +Algebraic analysis represents a primitive as Boolean or finite-field equations and searches for low-degree structure, invariants, or solvable systems. Serious algebraic work can involve full ANF extraction, SAT/SMT/MILP encodings, Gröbner-basis methods, or invariant searches. + +TriCube currently runs a **small black-box algebraic degree screen**. The screen varies selected input bits, evaluates selected output bits, applies a Möbius transform over the sampled truth table, and reports min/mean/max ANF degree plus the number of low-degree output bits. The quick profile uses 8 variables and 32 output bits; the standard profile uses 10 variables and 64 output bits. The code refuses variable counts above 12 because the cost is exponential. + +This screen is deliberately small. It can catch obvious low-degree black-box behavior, but it is not full algebraic cryptanalysis. A stronger version would require a verified symbolic model of the TriCube round function and a search for exploitable relations or invariants. + +### Collision, Birthday, and Near-Collision Checks + +Collision resistance is a cryptographic property. Small collision tests cannot validate it, but they can catch gross defects such as accidental truncation, repeated outputs, broken domain separation, or poor prefix behavior. + +TriCube currently runs a **collision/birthday sanity check** and a **near-collision sanity check**. The collision screen samples deterministic outputs, checks full 256-bit digest repeats, and compares prefix collision counts for 16, 24, 32, 40, 48, and 64-bit prefixes against birthday expectations. The near-collision screen samples output pairs and records Hamming-distance summaries. + +The quick profile uses 512 outputs and 256 sampled pairs. The standard profile uses 4096 outputs and 2048 sampled pairs. These counts are far below cryptographic collision-resistance validation. Larger campaigns would need much larger samples, careful memory-efficient counting, multi-seed coverage, and independent reproduction. + +### Overlap/Fork Stream Uniqueness Screen + +Fork and overlap tests look for repeated blocks within one stream or across related streams. They are useful for stream-mode engineering because repeated blocks or same-position equality across adjacent seeds can indicate a catastrophic state or counter bug. + +TriCube currently runs an **overlap/fork stream uniqueness screen**. It checks adjacent seeds, low-weight seed differences, and a high-bit seed difference. It evaluates 16, 32, and 64-byte block sizes. The quick profile uses 1 MiB per seed; the standard profile uses 16 MiB per seed. The metrics are repeated blocks within a stream, overlaps across streams, same-position equality, and adjacent-seed prefix Hamming distance over the first 1024 bytes. + +This screen does not prove stream independence or pseudorandomness. A stronger version would expand seed classes, stream sizes, and block-position analysis, then combine it with PractRand seed-target modes or a custom white-box related-seed study. + +### State-Recovery and Predictability Screen + +State-recovery attacks exploit knowledge of a transition or output function to recover internal state. A black-box prediction test is much weaker. + +TriCube currently runs a **black-box state-recovery/predictability screen**. It splits generated bytes into train and test halves, then evaluates a next-byte frequency predictor, a one-byte-context predictor, bit-position majority accuracy, and Berlekamp-Massey linear complexity on selected bit streams. The quick profile uses 1 MiB total stream data and 4096 bits per selected bit stream; the standard profile uses 16 MiB and 16,384 bits. + +This is not state-recovery cryptanalysis. A real state-recovery attempt would exploit the exact transition function, output extraction, state size, and round schedule, then quantify work factor or demonstrate recovery on reduced-round variants. + +### Low-Bit Diagnostic Screen + +Low-bit diagnostics matter because the May 2026 PractRand run reported low-bit NS3 warnings. Internal diagnostics are weaker than PractRand, but they are useful for isolating likely failure modes before long battery runs. + +TriCube currently runs a **low-bit diagnostic screen**. It measures: + +- lowest bit of each byte; +- lowest bit of each 32-bit word; +- lowest bit of each 64-bit word; +- low-nibble distribution; +- bit-position frequency over all 64 bit positions; +- low-bit transition counts; +- lag-1 correlation for low and high bit streams. + +The standalone low-bit command is: + +```bash +python tests/crypto_analysis/screens/low_bit_diagnostics.py \ + --variants baseline,fast8x \ + --bytes 16777216 \ + --out tests/crypto_analysis/results/low-bit-latest +``` + +If this screen misses a PractRand warning, PractRand takes priority. The local diagnostic is a microscope, not a replacement for a battery. + +### White-Box Round-Model Analysis + +The black-box probes above observe output bytes. The repository also +contains a first white-box schedule analyzer: + +```bash +python tests/crypto_analysis/screens/whitebox_round_model.py \ + --rounds 24 \ + --out tests/crypto_analysis/results/whitebox-latest +``` + +This script implements the specified tetrahedron, edge, shell, and permutation +schedule directly. It tracks which original 64-bit lanes can influence each +later 64-bit lane after each modeled round, and it records schedule-coverage +facts such as tetrahedron count, edge count, shell-lane coverage, and rotation +constant coverage. + +The latest local run found that word-level dependency reaches all 32 modeled +source lanes by round 2 for the tracked state lanes and for the first 32 output +bytes. That is useful structural information, but it is not a security bound. +It does not model bit-level differential probability, rotational trails, +algebraic degree, SAT/SMT/MILP constraints, or attack complexity. + +## Current Screen Suite + +The reproducible screen suite lives in [tests/crypto_analysis/](../tests/crypto_analysis/). It records date, branch, commit, machine, OS, Python version, command line, variants, seeds, sample counts, bytes generated, output paths, and status counts. + +Profiles are intentionally modest so they can run on a laptop: + +| Profile | Samples | Algebraic variables | Algebraic output bits | Collision samples | Near pairs | Stream bytes per variant | BM bits | +|---|---:|---:|---:|---:|---:|---:|---:| +| `quick` | 64 | 8 | 32 | 512 | 256 | 1 MiB | 4096 | +| `standard` | 256 | 10 | 64 | 4096 | 2048 | 16 MiB | 16384 | + +Run the quick suite: + +```bash +python tests/crypto_analysis/run_all_screens.py \ + --profile quick \ + --variants baseline,fast8x \ + --out tests/crypto_analysis/results/quick-latest +``` + +Run the standard suite when runtime allows: + +```bash +python tests/crypto_analysis/run_all_screens.py \ + --profile standard \ + --variants baseline,fast8x \ + --out tests/crypto_analysis/results/standard-latest ``` -## Interpreting Failures +Each run writes `summary.json`, `summary.md`, one CSV and Markdown table per screen, and `all_screens.csv`. It does not store large raw streams. + +Each screen can also be run on its own from `tests/crypto_analysis/screens/`. +The individual entry points are: + +| Screen | Script | +|---|---| +| Black-box differential diffusion probe | `tests/crypto_analysis/screens/differential_screen.py` | +| Black-box rotational relation probe | `tests/crypto_analysis/screens/rotational_screen.py` | +| Small black-box algebraic degree screen | `tests/crypto_analysis/screens/algebraic_degree_screen.py` | +| Collision/birthday and near-collision sanity checks | `tests/crypto_analysis/screens/collision_screen.py` | +| Overlap/fork stream uniqueness screen | `tests/crypto_analysis/screens/overlap_fork_screen.py` | +| Black-box state-recovery/predictability screen | `tests/crypto_analysis/screens/state_recovery_screen.py` | +| Low-bit diagnostic screen | `tests/crypto_analysis/screens/low_bit_diagnostics.py` | +| White-box word-dependency model | `tests/crypto_analysis/screens/whitebox_round_model.py` | +| External battery availability check | `tests/crypto_analysis/screens/external_batteries.py` | + +The all-in-one runner imports these same modules. There is no second hidden +implementation of the screens. + +## From Screens to Proper Cryptanalysis + +The current screens are useful because they are cheap, reproducible, and good +at catching obvious mistakes. They are not enough for a cryptographic primitive +claim. The next level is to build a verified TriCube model and run bounded +white-box searches with established tooling. + +| Current screen | What it catches | Proper next test | Suggested open-source tooling | +|---|---|---|---| +| Black-box differential diffusion probe | Bad avalanche, repeated output differences, output-bit bias under selected deltas. | Reduced-round differential trail search with modular-addition difference modeling and probability estimates. | Z3, MILP tooling, CLAASP, CryptoSMT, ArxPy if the TriCube schedule can be represented cleanly. | +| Black-box rotational relation probe | Obvious preserved word rotations in stream output. | White-box rotational trail search through constants, additions, rotations, shell lanes, and output extraction. | Z3 or another SMT solver with a custom ARX model; ArxPy-style methods if adaptable. | +| Small black-box algebraic degree screen | Trivial low-degree behavior under a small sampled input space. | Symbolic algebraic-degree growth, invariant search, and reduced-round equation systems. | SageMath, SAT solvers, SMT solvers, Gröbner-style tooling where appropriate. | +| Collision/birthday sanity check | Gross digest repetition, broken truncation, and prefix-count anomalies at practical sample sizes. | Reduced-round collision or preimage search with validated constraints and expected-cost reporting. | Z3, CryptoMiniSat, CLAASP/CryptoSMT-style encodings. | +| Overlap/fork stream uniqueness screen | Repeated stream blocks and related-seed overlap bugs. | Related-seed or related-state analysis over the reduced-round transition and output layer. | Z3/SAT plus a custom related-seed model. | +| Black-box state-recovery/predictability screen | Simple next-byte, bit-majority, n-gram, and linear-complexity predictability. | Reduced-round state-recovery equations and output-inversion experiments. | SMT/SAT solvers and a bit-exact reduced-round model. | +| Low-bit diagnostic screen | Local low-bit frequency, transition, lag, and byte-position anomalies. | Targeted reduced-round low-bit propagation model and multi-seed external battery campaign. | Z3, SageMath, custom ARX model, PractRand, TestU01, Dieharder. | + +The practical path is: + +1. keep the black-box screens as regression gates; +2. keep using external statistical batteries for stream output; +3. build a bit-exact reduced-round TriCube model from the specification; +4. validate that model against known vectors and reduced-round reference + outputs; +5. use Z3, SageMath, SAT, MILP, or cryptanalysis frameworks to search bounded + reduced-round problems; +6. report solver results with assumptions, round counts, constraints, and + failure conditions. + +The tooling plan is in [docs/tooling.md](tooling.md). The first reusable +TriCube-specific schedule model is +[`tests/crypto_analysis/models/tricube_schedule.py`](../tests/crypto_analysis/models/tricube_schedule.py). + +## External Statistical Batteries + +External batteries are separate from the black-box development probes. They are stronger empirical statistical screens, but they still do not prove cryptographic security. + +TriCube does not bundle PractRand, Dieharder, TestU01, SmokeRand, NIST STS, or their binaries. Install each tool from its upstream source or package manager and follow that tool's license. The repository notice table is [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md), and command templates are in [tools/run_stat_batteries.md](../tools/run_stat_batteries.md). + +The recommended profiles are: + +| Profile | Purpose | Suggested checks | +|---|---|---| +| Quick | Catch obvious regressions before longer runs. | Internal screens, PractRand 256 MiB, SmokeRand express, TestU01 SmallCrush. | +| Standard | Produce a useful development evidence snapshot. | PractRand 1-10 GiB, Dieharder full, TestU01 Crush, NIST STS documented run. | +| Long | Look for slow-forming statistical problems and multi-seed instability. | PractRand 100 GiB+, TestU01 BigCrush, SmokeRand full, multi-seed campaign. | + +Harness failures must be separated from statistical failures. EOF, SIGPIPE, input exhaustion, and timeout should be recorded as `BLOCKED` unless the downstream tool reports a valid statistical result before the harness stops. + +## What the Probes Are Not + +The black-box differential diffusion probe does not model differential propagation through the round function. It does not search trails. It does not compute or bound maximum differential probability. It does not replace differential cryptanalysis. + +The black-box rotational relation probe does not prove resistance to rotational distinguishers. It only checks selected rotations for obvious preserved relations. + +The small black-box algebraic degree screen does not perform SAT, SMT, MILP, Gröbner-basis, full ANF, invariant, or integral cryptanalysis. It only samples small black-box algebraic behavior for selected variables and output bits. + +The collision/birthday sanity check is not collision-resistance evidence. The sample sizes are practical engineering checks, not cryptographic-scale searches. + +The overlap/fork stream uniqueness screen does not prove stream independence or safe related-seed behavior. + +The black-box state-recovery/predictability screen is not a state-recovery proof. It does not model or recover the internal state. + +Passing these probes means no obvious failure was detected under the tested budget. It does not establish security. + +## Terminology Correction + +Earlier project notes used phrases such as "differential probes," "rotational probes," and "algebraic screens." These are intentionally named probes/screens because they are not formal cryptanalysis. They are black-box development checks meant to find obvious failures before deeper analysis. Formal differential, rotational, and algebraic cryptanalysis remains future work. -A unit-test failure is an implementation failure. A statistical battery failure may indicate a structural weakness, a stream-format problem, a test-harness problem, or a reduced-quality region that needs diagnosis. A timeout is not a randomness failure; it is a performance or harness blocker. +## Method References -Passing statistical batteries should be recorded as evidence of no detected failure under those conditions, not as a security claim. +The documentation style and limits above are based on standard cryptographic and statistical-testing references: +- Biham and Shamir, "Differential Cryptanalysis of DES-like Cryptosystems," *Journal of Cryptology*, 1991, DOI: https://doi.org/10.1007/BF00630563. +- Khovratovich and Nikolic, "Rotational Cryptanalysis of ARX," FSE 2010, IACR PDF: https://www.iacr.org/archive/fse2010/61470339/61470339.pdf. +- Courtois and Pieprzyk, "Cryptanalysis of Block Ciphers with Overdefined Systems of Equations," ASIACRYPT 2002, ePrint: https://eprint.iacr.org/2002/044. +- NIST SP 800-22 Rev. 1a, "A Statistical Test Suite for Random and Pseudorandom Number Generators for Cryptographic Applications": https://csrc.nist.gov/projects/random-bit-generation/documentation-and-software. +- L'Ecuyer and Simard, "TestU01: A C Library for Empirical Testing of Random Number Generators," ACM TOMS 2007, DOI: https://doi.org/10.1145/1268776.1268777. +- PractRand documentation: https://pracrand.sourceforge.net/. +- Dieharder manual: https://rurban.github.io/dieharder/manual/dieharder.html. +- SmokeRand repository: https://github.com/alvoskov/SmokeRand. diff --git a/docs/tooling.md b/docs/tooling.md new file mode 100644 index 0000000..022944d --- /dev/null +++ b/docs/tooling.md @@ -0,0 +1,107 @@ +# Analysis Tooling + +TriCube should not grow a custom cryptanalysis ecosystem when good public +tools already exist. The custom part is the TriCube model: the 32-lane state, +the 3 x 3 x 3 vertex mapping, tetrahedral schedule, edge coupling, shell +coupling, lane permutation, constants, modes, padding, and output extraction. +Once that model is correct, established solvers and statistical batteries can +be used for reduced-round searches and external screening. + +This repository does not vendor solver frameworks or statistical batteries. +They must be installed separately and used under their own licenses. The local +scripts only check availability, run tiny smoke tests, or provide wrappers +around TriCube's stream output. + +## Tool Status + +| Area | Tool | Role | Current status | Needed TriCube-specific work | +|---|---|---|---|---| +| Statistical batteries | PractRand | Progressive random-stream testing through `RNG_test`. | Already used; optional external tool. | Keep stream wrapper commands, preserve warnings, and run multi-seed long profiles before promotion. | +| Statistical batteries | TestU01 | SmallCrush, Crush, and BigCrush empirical RNG batteries. | Already used through a local stdin adapter. | Keep adapter reproducible, separate EOF/harness failures from statistical failures, and record tool version. | +| Statistical batteries | Dieharder | Broad empirical RNG battery. | Already used; optional external tool. | Ensure enough stream input for full runs; report weak rows separately from failures. | +| Statistical batteries | SmokeRand | Independent stream battery with express/full modes. | Already used; optional external tool. | Document express/full commands and keep timeout separate from statistical failure. | +| Statistical batteries | NIST STS | SP 800-22 statistical test suite. | Already used as an optional external check. | Document sequence count, bit length, parse rules, and failed-proportion handling. | +| SMT | Z3 | Bit-vector model checking, reduced-round satisfiability, and bounded search. | Recommended next tooling; smoke check provided. | Build and validate a bit-exact reduced-round TriCube model before searching trails or collisions. | +| SAT | CryptoMiniSat | CNF/SAT experiments for reduced-round Boolean models. | Optional future tool. | Export a verified CNF model for reduced-round components and compare results against known vectors. | +| Algebraic | SageMath | Boolean polynomial rings, ANF experiments, and algebraic prototyping. | Recommended for small reduced-round algebraic experiments; smoke check provided. | Export reduced-round components small enough for exact ANF or polynomial experiments. | +| ARX analysis framework | CLAASP | Automated analysis of symmetric primitives, including MILP, SMT, algebraic, avalanche, and statistical modules. | Future investigation. | Determine whether TriCube's tetrahedral in-place schedule can be represented cleanly in CLAASP. | +| ARX analysis framework | CryptoSMT | SMT/SAT-based cryptanalysis framework for symmetric primitives. | Future investigation; likely useful only after model adaptation. | Adapt TriCube as a custom ARX/hash primitive and verify reduced-round outputs. | +| ARX analysis framework | ArxPy | SMT-based XOR differential, rotational-XOR, and impossible-differential work on ARX primitives. | Future investigation. | Determine whether TriCube's nonstandard state topology and shell lanes fit its primitive interface. | +| MILP / CP-SAT | OR-Tools | Constraint programming and integer optimization experiments. | Optional; not cryptanalysis-specific. | Use only if a clear MILP/CP-SAT formulation is defined and validated. | +| MILP / LP | PuLP / SciPy | Lightweight LP/MILP prototyping when a full cryptanalysis framework is not needed. | Optional; not a default dependency. | Use for small modeling experiments only; avoid presenting generic optimization as cryptanalysis. | + +## What Must Be Custom + +The open-source tools do not know TriCube. They cannot search meaningful +trails or equations until a TriCube-specific model exists. The required model +must encode: + +- the 32-lane, 2048-bit state; +- the 27 vertex lanes and 5 shell/global lanes; +- the `3 x 3 x 3` vertex mapping and `2 x 2 x 2` cube cells; +- the 48 tetrahedral neighborhoods and their orientation schedule; +- the 54 grid-edge couplings; +- the shell/global lane coupling schedule; +- the global lane permutation; +- the round constants and rotation schedules; +- absorb, finalize, squeeze, stream seeding, length encoding, and domain tags; +- reduced-round hooks that can be checked against known vectors. + +After that model is validated, external tools can be used to search for +reduced-round differential trails, rotational distinguishers, algebraic +low-degree relations, state-recovery equations, and reduced-round +collision/preimage examples. Until then, black-box probes and word-dependency +models are development gates, not cryptanalysis. + +The reusable lane-schedule model is +[`tests/crypto_analysis/models/tricube_schedule.py`](../tests/crypto_analysis/models/tricube_schedule.py). +It verifies the current schedule counts: 27 vertex lanes, 5 shell lanes, 8 cube +cells, 48 tetrahedra, 54 edges, and a 32-lane permutation. It is useful +foundation code, but it is still a word-level schedule model rather than a +solver-ready attack model. + +## Local Tool Checks + +Optional tool availability can be checked without making those tools package +dependencies: + +```bash +python tests/crypto_analysis/tooling/check_tools.py +``` + +Small solver smoke tests are available: + +```bash +python tests/crypto_analysis/tooling/z3_smoke.py +sage -python tests/crypto_analysis/tooling/sage_smoke.py +``` + +These smoke tests only verify local setup. They do not analyze TriCube. + +## References and License Notes + +- Z3 is a Microsoft Research SMT solver distributed under the MIT License: + . +- SageMath is a GPL-licensed open-source mathematical software system: + . +- CryptoMiniSat is an open-source SAT solver; the upstream repository reports + default MIT-licensed build material, with license caveats for optional + integrations: + . +- CLAASP is the Cryptographic Library for Automated Analysis of Symmetric + Primitives; its PyPI metadata lists GPLv3: + and . +- CryptoSMT is an SMT/SAT-based tool for cryptanalysis of symmetric primitives: + . +- ArxPy documents SMT-based XOR differential, rotational-XOR, and impossible + differential work for ARX primitives: . +- TestU01 is described by L'Ecuyer and Simard, ACM TOMS 2007: + . +- PractRand documentation is at . +- Dieharder documentation is at + . +- NIST STS is documented in NIST SP 800-22 and on the NIST random-bit + generation software page: + . +- SmokeRand is referenced from its upstream repository: + . diff --git a/paper/README.md b/paper/README.md new file mode 100644 index 0000000..4b7a78e --- /dev/null +++ b/paper/README.md @@ -0,0 +1,7 @@ +# Paper + +This folder contains TriCube manuscript materials. + +- `manuscript.md` is the source manuscript. +- `TriCube_Short_Manuscript_2026-05-20.docx` is a short Word manuscript version. +- `figures/` and `references.bib` contain supporting manuscript assets. diff --git a/paper/TriCube_Experimental_Geometric_Hash_XOF.docx b/paper/TriCube_Experimental_Geometric_Hash_XOF.docx deleted file mode 100644 index 41f0cb1..0000000 Binary files a/paper/TriCube_Experimental_Geometric_Hash_XOF.docx and /dev/null differ diff --git a/paper/TriCube_Experimental_Geometric_Hash_XOF.pdf b/paper/TriCube_Experimental_Geometric_Hash_XOF.pdf deleted file mode 100644 index f46068b..0000000 Binary files a/paper/TriCube_Experimental_Geometric_Hash_XOF.pdf and /dev/null differ diff --git a/paper/TriCube_Short_Manuscript_2026-05-20.docx b/paper/TriCube_Short_Manuscript_2026-05-20.docx new file mode 100644 index 0000000..e42116a Binary files /dev/null and b/paper/TriCube_Short_Manuscript_2026-05-20.docx differ diff --git a/paper/figures/round-flow.svg b/paper/figures/round-flow.svg new file mode 100644 index 0000000..17dfc7f --- /dev/null +++ b/paper/figures/round-flow.svg @@ -0,0 +1,62 @@ + + TriCube round flow + The order of operations in one TriCube permutation round. + + + + + + + + One TriCube Permutation Round + All layers update the same 2048-bit state. Tetrahedral, edge, and shell layers are in-place; order is therefore part of the specification. + + + + State S + 32 lanes + 64 bits each + + + + Tetra mix + 48 neighborhoods + ARX, in-place + + + + Edge couple + 54 grid edges + in-place + + + + Shell couple + 5 shell lanes + global feedback + + + + Permute + T[i]=S[9i+5] + mod 32 + + + round constant injection completes the round, then the next round begins + + + + + Mode use + Hash/XOF absorb message blocks, finalize with length and output length, then squeeze output. + Stream mode initializes from a seed/domain, absorbs a counter schedule, permutes, then squeezes deterministic output for testing. + + diff --git a/paper/figures/state-layout.svg b/paper/figures/state-layout.svg new file mode 100644 index 0000000..fa921bc --- /dev/null +++ b/paper/figures/state-layout.svg @@ -0,0 +1,120 @@ + + TriCube state layout + A 3 by 3 by 3 vertex-lane grid plus five shell/global lanes. + + + + + + + + TriCube 2048-bit State Layout + State lanes S[0]..S[31] are 64-bit words. Lanes S[0]..S[26] form a 3 x 3 x 3 vertex grid. Lanes S[27]..S[31] are shell/global lanes. + + + + + + + x + y + z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + S0 + S1 + S2 + S3 + S4 + S5 + S6 + S7 + S8 + + S9 + S10 + S11 + S12 + S13 + S14 + S15 + S16 + S17 + + S18 + S19 + S20 + S21 + S22 + S23 + S24 + S25 + S26 + + + + + Shell/global lanes + These lanes are ordinary 64-bit state words used for global coupling. + They are not external entropy sources. + S27 + S28 + S29 + S30 + S31 + + + Coordinate rule + index(x,y,z) = x + 3(y + 3z) + x,y,z are each 0, 1, or 2 + lane word size = 64 bits + + diff --git a/paper/figures/tetrahedral-decomposition.svg b/paper/figures/tetrahedral-decomposition.svg new file mode 100644 index 0000000..15ea633 --- /dev/null +++ b/paper/figures/tetrahedral-decomposition.svg @@ -0,0 +1,61 @@ + + TriCube cube-to-tetrahedra decomposition + A unit cube with eight local vertices and the six tetrahedral neighborhoods used by TriCube. + + + + + Cube Cell Decomposition into Six Tetrahedra + Each 2 x 2 x 2 cell uses the same local schedule. The six tetrahedra share vertices, so in-place updates make the listed order part of the specification. + + + + + + + + + + + + + + + + + v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7 + + The long body diagonal v0-v7 anchors all six local tetrahedra. + + + + + Local tetrahedron order + 1. (v0, v1, v3, v7) + 2. (v0, v3, v2, v7) + 3. (v0, v2, v6, v7) + 4. (v0, v6, v4, v7) + 5. (v0, v4, v5, v7) + 6. (v0, v5, v1, v7) + + Global order: cz, cy, cx over {0,1}, + then the six local tetrahedra above. + + diff --git a/paper/manuscript.md b/paper/manuscript.md index 2b8fa52..8a15cb8 100644 --- a/paper/manuscript.md +++ b/paper/manuscript.md @@ -1,106 +1,493 @@ -# TriCube: An Experimental Geometric Hash and XOF Candidate Based on Tetrahedral State Evolution +# TriCube: Experimental Evaluation of a Tetrahedral/Cube-Connected Hash and XOF Primitive + +Preliminary Technical Manuscript — Public Review Draft Steven Reid -2026 +Independent Researcher | sreid1118@gmail.com + +May 20, 2026 | Apple M4 Pro / macOS 15.5 • Python 3.13/3.14 • Apple clang 17.0.0 + +SCOPE NOTICE: This is a preliminary public-review manuscript. Evidence consists of unit tests, project benchmarks, black-box development probes, a first word-level white-box schedule model, and external statistical-battery results. No wording constitutes a claim of cryptographic security. PractRand low-bit warnings remain unresolved and formal cryptanalysis remains future work. ## Abstract -TriCube is an experimental hash, extendable-output function, and deterministic stream generator based on cube-connected state evolution. The current construction uses a 2048-bit state arranged as a 3 x 3 x 3 vertex grid plus shell lanes. The eight cube cells of the grid are decomposed into tetrahedra, and each round combines local tetrahedral ARX mixing, edge-coupled propagation, shell coupling, and global lane permutation. This paper documents the public construction, implementation status, test vectors, early statistical evidence, known limitations, and the work required before any security claim would be responsible. TriCube is not presented as cryptographically secure or production ready. +We report the design and experimental evaluation of TriCube, a candidate hash function and extendable-output function (XOF) based on tetrahedral/cube-connected state evolution. The current public baseline uses a 2048-bit state with thirty-two 64-bit lanes. Twenty-seven lanes form a 3 × 3 × 3 vertex grid; five shell/global lanes provide additional global coupling. The eight cube cells of the grid are decomposed into six tetrahedra each, giving 48 tetrahedral neighborhoods per full pass. Each round combines local tetrahedral ARX mixing, grid-edge coupling, shell/global coupling, and a deterministic lane permutation. + +Evaluation covers unit tests and vector agreement, project statistical benchmarks, black-box development probes, and external statistical batteries. The public baseline and the experimental fast8x stream variant are separated by domain tags and by API/CLI selection. The evidence supports continued review and engineering work; it does not establish collision resistance, preimage resistance, pseudorandomness, or security for deployment. + +Keywords: hash function, XOF, geometric primitive, tetrahedral decomposition, cube-connected state, sponge construction, PractRand, TestU01, black-box development probes ## 1. Introduction -TriCube grew out of a series of notebook and local-repository experiments on recursive geometric mixing, entropy diagnostics, and byte-stream generators. Several earlier candidates used cube or tetrahedron language but did not explicitly model a tetrahedral state topology. The current version was separated from that broader archive into a narrower project: a concrete geometric hash/XOF candidate with standalone C code, Python reference code, fixed vectors, and reproducible tests. +The design of cryptographic hash functions is a mature but active area. SHA-2 [1], SHA-3 (Keccak) [2], BLAKE2 [3], and BLAKE3 [4] have undergone extensive public cryptanalysis and are widely deployed. The dominant paradigm for efficient software hash designs is ARX: Add-Rotate-XOR, which combines modular addition, bitwise rotation, and exclusive-or to achieve diffusion and non-linearity within a fixed state. -The purpose of this release is not to claim a new secure hash. The purpose is to make the construction inspectable enough for review. The repository removes unsupported claims, keeps the implementation deterministic, and records both positive and negative evidence. +TriCube explores an alternative: geometric state evolution. The current public baseline maps a 2048-bit state onto a 3 × 3 × 3 cube-vertex grid plus five shell/global lanes. State advancement uses local tetrahedral mixing over 48 tetrahedral neighborhoods, edge-coupling passes over 54 grid-adjacent vertex pairs, shell/global coupling, and a deterministic lane permutation. This structure is intended to make the construction inspectable and testable, not to imply a security claim. -## 2. Design Goals +The motivation is exploratory. We do not claim any advantage in security or efficiency over established designs. We document the construction, measure its empirical statistical behavior, and probe it with targeted black-box development screens to characterize what is and is not known. The results are a development snapshot as of May 19, 2026: encouraging as a first filter, but insufficient to support any cryptographic security claim. -The design goals are: +## 2. Related Work -1. Preserve the geometric TriCube idea in an explicit state model. -2. Avoid depending on SHA, BLAKE, or another mature hash as the internal primitive. -3. Provide fixed hash and XOF behavior with deterministic test vectors. -4. Provide a stream mode for statistical testing. -5. Keep the implementation small enough to audit. -6. State limitations directly. +The established hash landscape is dominated by ARX and permutation-based designs. SHA-2 [1] uses Merkle-Damgård with an ARX compression function. SHA-3 (Keccak) [2] is a sponge construction over a 1600-bit state organized as a 5×5 matrix of 64-bit lanes, using bitwise permutation steps (θ, ρ, π, χ, ι). BLAKE2/BLAKE3 [3][4] are ARX designs derived from ChaCha with tree-hashing modes for parallelism. All have received extensive public cryptanalysis. -TriCube is not designed as a drop-in replacement for SHA-2, SHA-3/SHAKE, BLAKE2, or BLAKE3. +CubeHash [5] is a close name-level comparison because it also uses cube language, although its state layout and round function differ substantially. TriCube also sits near sponge and ARX designs, because it absorbs input, permutes state, and squeezes output using addition, rotation, and XOR. Established families such as Ascon specify exact state size, round transformations, constants, modes, rates, and padding before making scheme-level claims; TriCube follows that documentation style for clarity, but it is not standardized and has not received comparable cryptanalytic review. -## 3. Construction +## 3. Construction Design -The TriCube state contains 32 64-bit lanes. Lanes 0 through 26 map to vertices in a 3 x 3 x 3 grid. The mapping is: +3.1 State Representation -```text -index(x, y, z) = x + 3 * (y + 3 * z). -``` +The current public TriCube baseline uses a 2048-bit state represented as thirty-two 64-bit words S[0] through S[31]. Lanes S[0] through S[26] are vertex lanes in a 3 × 3 × 3 grid. Coordinates x, y, z ∈ {0,1,2} map to lane index x + 3(y + 3z). Lanes S[27] through S[31] are shell/global lanes. They are state words used for global coupling and metadata mixing, not external entropy sources. + +![TriCube state layout](figures/state-layout.svg) + +All words are interpreted little-endian when absorbing or emitting bytes. All 64-bit additions are modulo 2^64. ROTL64(x, r) denotes a left rotation by r mod 64 bits. -The grid contains eight cube cells. Each cube cell is decomposed into six tetrahedra: +3.2 Tetrahedral Decomposition + +The 3 × 3 × 3 vertex grid contains eight unit cube cells. For a cube at cell coordinate (cx, cy, cz), the local vertices are v0=(cx,cy,cz), v1=(cx+1,cy,cz), v2=(cx,cy+1,cz), v3=(cx+1,cy+1,cz), v4=(cx,cy,cz+1), v5=(cx+1,cy,cz+1), v6=(cx,cy+1,cz+1), and v7=(cx+1,cy+1,cz+1), mapped through the lane-index formula above. + +Each cube cell is decomposed into six tetrahedral neighborhoods: (v0,v1,v3,v7), (v0,v3,v2,v7), (v0,v2,v6,v7), (v0,v6,v4,v7), (v0,v4,v5,v7), and (v0,v5,v1,v7). Across eight cube cells this gives 48 tetrahedral neighborhoods per full pass. The tetrahedra share vertices and are updated in a deterministic order, so the in-place update order is part of the construction. + +![TriCube tetrahedral decomposition](figures/tetrahedral-decomposition.svg) + +3.3 Round Function + +A TriCube round applies four layers in sequence: tetrahedral local mixing, grid-edge coupling, shell/global coupling, and lane permutation with constant injection. The implementation precomputes a 24-round schedule for speed, but the schedule is derived from the deterministic rules in docs/specification.md and must not change the public output. + +![TriCube round flow](figures/round-flow.svg) ```text -(0, 1, 3, 7) -(0, 3, 2, 7) -(0, 2, 6, 7) -(0, 6, 4, 7) -(0, 4, 5, 7) -(0, 5, 1, 7) +for round r in 0..R-1: + for each tetrahedron t in deterministic order: + apply orientation-dependent four-lane ARX mix in place + for each axis-adjacent grid edge e in deterministic order: + couple the two endpoint lanes in place + for each shell/global lane j in 0..4: + mix shell lane S[27+j] with a scheduled vertex and opposite vertex + permute all 32 lanes by T[i] = S[(9*i + 5) mod 32] + xor a round-dependent rotated constant into each lane ``` -Across all cube cells this gives 48 tetrahedral groups. A round applies orientation-dependent ARX mixing to these groups, then couples adjacent grid vertices, then mixes shell lanes, then applies a deterministic lane permutation. +The tetrahedral ARX layer uses rotation sets (17,29,41,53), (23,31,47,59), (19,37,43,61), and (13,27,39,55), selected by round and tetrahedron index. Round constants are generated by a SplitMix64 stream from fixed seeds and are specified normatively by the generator rule rather than by listing all 768 constants in the manuscript. -## 4. Modes +3.4 Absorb, Finalize, and Squeeze -Hash mode emits a 256-bit digest. XOF mode emits an arbitrary number of bytes. Stream mode initializes the state from a seed and emits deterministic bytes for external statistical batteries. The stream mode is a test interface, not a random-number generator claim. +Hash mode uses the domain tag TC-TETRA256-V2, a 64-byte message absorb block, a 33-byte finalization trailer, and a 16-round final permutation before squeezing a 256-bit digest. The V2 substring is a legacy compatibility tag in the domain string and does not change the public project name, which is TriCube. -## 5. Implementation +The finalization trailer contains the little-endian message length, encoded output length, message-block count, and a final 0x80 marker. XOF mode uses the same construction with encoded output length zero and produces output in chunks of up to 192 bytes. Stream mode uses a separate domain tag and a 64-bit little-endian seed tweak. The default stream path remains the released baseline. -The repository contains a C11 implementation with a public header, CLI, Makefile, CMake file, and C tests. It also contains a Python package with `tricube.hash`, `tricube.hexdigest`, `tricube.xof`, and `tricube.stream`. The Python path is a reference implementation for scripting and vector checks; the C path is the primary performance path. +The exact parameter table, lane table, tetrahedron schedule, round pseudocode, absorb/finalize/squeeze rules, endianness, padding, domain separation, and test vectors are maintained in docs/specification.md. That file is the reference specification for independent reimplementation. -## 6. Test Vectors +## 4. Implementation -The public 256-bit digest vectors are: +The public repository is organized around a standalone C11 implementation and a small Python package. The C implementation in c/src/tricube.c is the reference implementation for the public baseline. The public header c/include/tricube.h exposes fixed 256-bit digest mode, XOF mode, context-style update/finalize/squeeze functions, deterministic stream generation, and self-test support. -| Message | Digest | -|---|---| -| empty string | `7fcaaa35165277bcaca583e23ef1d3545705e14d39f3ed7a802b1275d920cf49` | -| `abc` | `779403a9c748fc3213493953fc17309367b37161c00dc19059c14db63774e11e` | -| `TriCube` | `5b4c461fe975dfba72fc9b2fbcf04e3a1a807c83fa4502c238ee4fe48b736d13` | +The Python package exposes tricube.hash, tricube.hexdigest, tricube.xof, tricube.stream, tricube.self_test, and security_notice. The Python path is intended for scripting, packaging, and vector checks; the C path is the primary implementation path for external battery testing and throughput work. -## 7. Current Evidence +The experimental fast8x stream entry points are implemented as a named opt-in stream variant. They are domain-separated from the baseline and do not alter hash vectors, XOF vectors, or the default stream behavior. The shared permutation and fast8x profile live in c/src/tricube.c, while the public fast8x entry points are exposed through c/src/tricube_fast8x.c. -The May 2026 validation pass found that the C stream path was deterministic and fast enough for longer external statistical batteries. In the public refresh run, the standalone C stream path measured about 51.5 MiB/s on an Apple M4 Pro Mac mini; a local package smoke run measured about 53.1 MiB/s. The current hash path remains slower, measuring about 14.7 MiB/s for 1024-byte messages at 16 rounds. +The public repo is https://github.com/RRG314/tricube-hash. It excludes raw exploratory material, machine-specific paths, and unpromoted high-speed variants that failed low-bit screens. -The external statistical-battery evidence includes SmokeRand express PASS (7/7), NIST STS standard check PASS, TestU01 SmallCrush PASS (15/15), TestU01 Crush PASS (144/144), and a Dieharder battery result of 109 PASS, 2 WEAK, and 0 FAIL. PractRand remains the main warning: the 1 GiB expanded run ended with no final-level anomalies in 2050 final-level results, but earlier levels reported suspicious and unusual low-bit behavior. That warning is the most important current test finding. +## 5. Test Environment -## 8. Limitations +All results were collected in a single session on the following machine. Throughput results are specific to this hardware; statistical results should be consistent within sampling variance across platforms. -TriCube has no proof of collision resistance, preimage resistance, second-preimage resistance, indifferentiability, or output pseudorandomness. No independent cryptanalysis has been completed. Reduced-round analysis, differential trails, rotational behavior, algebraic analysis, birthday collision campaigns, state-recovery attempts, and side-channel review are all incomplete. +| Parameter | Value | +| --- | --- | +| Host | Mac mini local research machine | +| Operating System | macOS 15.5 for public result summary; later local checks on macOS with Python 3.14 | +| Kernel | Darwin / arm64 | +| CPU | Apple M4 Pro (arm64) | +| Memory | 48 GiB | +| Logical / Physical CPUs | 12 / 12 | +| Python | CPython 3.13.2 in original research runs; CPython 3.14.3 in later local checks | +| NumPy | not required by the public package tests | +| pytest | 9.0.3 in local package checks | +| C Compiler | Apple clang 17.0.0 | +| C Standard | -std=c11 -O3 -DNDEBUG | +| Test date | May 19–20, 2026 | -The C implementation is portable and usable, but not optimized like mature hash libraries. Throughput is not currently competitive with optimized SHA-2, SHA-3, BLAKE2, or BLAKE3 implementations. +Table 1. Hardware and software environment. -## 9. Comparison to Known Work +Key reproduction commands: -TriCube shares broad patterns with sponge and ARX designs: absorb input, permute state, and squeeze output. Those ideas are established. The candidate contribution is the explicit tetrahedral/cube-connected state evolution. This should be compared against SHA-2, SHA-3/SHAKE, BLAKE2, BLAKE3, CubeHash, Keccak-family permutations, Xoodoo/Xoodyak-like permutations, and KangarooTwelve before any novelty claim is made. +.venv/bin/python -m pytest -q -## 10. Reproducibility +python benchmarks/bench_stream.py --bytes 104857600 -The public repository is intended to be reproducible from source: +tools/run_practrand.sh 1073741824 -```bash -make -C c test -python -m pip install -e ".[test]" -pytest -q -python benchmarks/bench_stream.py --bytes 1048576 -``` +This manuscript keeps the main claim narrow. The body describes the construction and the strongest current evidence. Large command transcripts, raw battery logs, and generated tables belong in the repository artifacts rather than in the main paper. + +## 6. Unit Tests and Internal Benchmarks + +### 6.1 Unit Tests + +The Python test suite verifies byte-level correctness against expected-output test vectors and invariant properties: expected outputs for all implementation variants, byte-by-byte agreement between the Python native path and the C binary, and invariant properties (non-zero output, distinct seeds produce distinct output, correct output length). Result: 15 passed in 4.90 seconds; no failures, errors, or skips. + +### 6.2 Internal Statistical Benchmark + +The project benchmark measures throughput, byte entropy (Shannon H(X) over 256 byte values; maximum 8.000 bits/byte), bit balance, serial correlation at lag 1 (Pearson ρ₁), and avalanche mean across all variants. Preserved slow baseline paths were capped at 65,536 bytes; production paths were measured at 1,048,576 bytes. + +| Generator | Status | Bytes | MiB/s | Entropy (bits/byte) | Bit Balance | Serial Corr ρ₁ | Avalanche | Rep. Blocks | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| tricube_tc256_xof | PASS | 65,536 | 22.37 | 7.996873 | 0.4998 | 0.000985 | 0.4988 | 0 | +| tricube_tc256_xof_fast | PASS | 1,048,576 | 74.90 | 7.999817 | 0.4999 | 0.000759 | 0.5020 | 0 | +| tricube_geo256_chain | PASS | 65,536 | 0.12 | 7.997174 | 0.4997 | 0.001813 | 0.4951 | 0 | +| tricube_geo256_chain_fast | PASS | 1,048,576 | 62.35 | 7.999848 | 0.5000 | 0.000753 | 0.4990 | 0 | +| tricube_tetra_block256_chain | PASS | 1,048,576 | 10.67 | 7.999802 | 0.4998 | -0.000870 | 0.5038 | 0 | +| tricube_tetra_block256_chain_fast | PASS | 1,048,576 | 57.67 | 7.999822 | 0.5001 | 0.000616 | 0.5038 | 0 | +| tricube_tetra_block256_native | PASS | 65,536 | 0.059 | 7.996933 | 0.4993 | -0.004718 | 0.5042 | 0 | +| legacy native TriCube prototype | PASS | 1,048,576 | 0.23 | 7.999798 | 0.4999 | -0.001290 | 0.5012 | 0 | +| standalone C TriCube stream | PASS | 1,048,576 | 44.30 | 7.999798 | 0.4999 | -0.001290 | 0.5012 | 0 | +| sha256_counter_chain (ctrl) | PASS | 1,048,576 | 48.11 | 7.999830 | 0.5001 | 0.000219 | 0.5035 | 0 | +| tricube_fast8x_stream_c | PASS | 16,777,216 | 132.65 | 7.999991 | 0.499993 | 0.000102 | 0.4996 | 0 | + +Table 2. Internal statistical benchmark. All TriCube variants passed all sanity gates. Entropy: 7.997–8.000 bits/byte. Bit balance: within 0.001 of 0.5. Serial correlation |ρ₁| < 0.005. Avalanche mean: 0.495–0.504. + +### 6.3 Throughput (100 MiB) + +A 100 MiB stream benchmark assessed sustained throughput for the original Python recovery candidates. Those measurements are useful implementation evidence but not a competitive hash benchmark. The later C ablation harness measured the released baseline at 69.796 MiB/s and the experimental fast8x stream variant at 132.655 MiB/s on a 256 MiB stream run. Optimized native implementations of BLAKE3, SHA-2, and SHA-3 can operate much faster on modern hardware, so these numbers should be read as prototype engineering results. + +| Generator | Throughput | Bytes Measured | Notes | +| --- | --- | --- | --- | +| tricube_geo256 | 0.122 MiB/s | 1,048,576 | Baseline cap (preserved reference path) | +| tricube_geo256_fast | 59.197 MiB/s | 104,857,600 | Full 100 MiB run | +| tricube_tc256_xof | 22.174 MiB/s | 1,048,576 | Baseline cap (preserved reference path) | +| tricube_tc256_xof_fast | 82.219 MiB/s | 104,857,600 | Full 100 MiB run — fastest Python variant | +| tricube_tetra_block256 | 9.692 MiB/s | 104,857,600 | Full 100 MiB run | +| tricube_tetra_block256_fast | 54.889 MiB/s | 104,857,600 | Full 100 MiB run | +| tricube_tetra_block256_native | 0.056 MiB/s | 1,048,576 | Native squeeze cap | +| legacy TriCube prototype | 0.221 MiB/s | 1,048,576 | Native squeeze cap | +| sha256_counter_chain | 48.598 MiB/s | 104,857,600 | Reference control (same Python harness) | +| tricube_c_stream_fast8x | 132.655 MiB/s | 268,435,456 | Experimental opt-in stream variant; cleanest optimized ablation candidate | +| tricube_c_stream_baseline | 69.796 MiB/s | 268,435,456 | Released baseline in the same ablation harness | + +Table 3. Throughput summary. The Python-candidate rows come from the original 100 MiB harness; the C baseline and fast8x rows come from the later ablation harness on the Apple M4 Pro local research machine. + +6.4 fast8x Stream Variant Update + +After the original May 2026 evaluation, an ablation pass tested faster stream/XOF paths. The optimized candidate included in the repository is fast8x. It uses a separate stream domain tag, 8 initialization rounds, 4 per-block stream rounds, a 256-byte stream rate, and an additional TriCube-family output mixing layer. It must be requested explicitly with --variant fast8x. + +In the ablation harness, fast8x measured 132.655 MiB/s on a 256 MiB stream run, compared with 69.796 MiB/s for the released baseline in the same harness. A later 64 MiB smoke run measured 130.940 MiB/s for fast8x and 73.067 MiB/s for the baseline. The ablation record lists fast8x as clean through PractRand 1 GiB, SmokeRand express 7/7, TestU01 SmallCrush 15/15, and 16 MiB sanity probes with no repeated 32-byte blocks. + +Faster variants were not promoted. fast8x_batch reached 311.98 MiB/s but failed PractRand low-bit FPF checks; fast8x_wide reached 285.71 MiB/s but showed very suspicious low-bit rows; fast8x512 reached 230.74 MiB/s but had recurring low-bit warnings. The public rule is conservative: speed gains that introduce repeatable low-bit warnings do not get promoted. + +## 7. Structural Checks and Black-Box Development Probes + +The following probes are black-box development screens using observable input/output behavior. They are development evidence, not cryptographic validation. They do not model differential trails, prove resistance to rotational distinguishers, perform SAT/MILP or Gröbner-basis analysis, or prove state-recovery resistance. + +### 7.1 Structural Gap Checks + +| Check | Status | Key Quantitative Result | Elapsed (s) | +| --- | --- | --- | --- | +| full_digest_collision_smoke | PASS | 20,000 pairs; 0 full collisions; 0 prefix-64 collisions | 73.98 | +| low_weight_input_test | PASS | 2,561 unique outputs; min/mean/max Hamming = 103/128.3/150 | 9.42 | +| sampled_near_collision_distance | PASS | 4,096 pairs; min/mean/max Δbits = 103/128.2/157 | 33.64 | +| domain_tweak_separation | PASS | 5 tweaks; min pairwise Hamming = 121 | — | +| bit_influence_spread | PASS | mean flip p = 0.5010; range 0.426–0.570 | — | +| overlap_fork_stream_uniqueness | PASS | 4 streams × 262,144 blocks; 0 overlapping 32-byte blocks | 0.26 | + +Table 4. Structural gap checks. 20,000 collision pairs, 4,096 near-collision pairs, domain separation, low-weight inputs, and overlap/fork stream uniqueness checks all returned PASS under the tested budget. + +### 7.2 Black-Box Differential Diffusion and Rotational Relation Probes + +| Status | Cases | Mean Δbits Range | Max Output-bit Bias | Repeated Diffs | Elapsed (s) | +| --- | --- | --- | --- | --- | --- | +| PASS | 36 | 127.704–128.348 | 0.042969 | 0 | 386.5 s | + +Table 5. Black-box differential diffusion probe (36 cases, 6 delta patterns × 6 round counts, 2,048 samples each). Mean output difference was 127.7–128.3 bits, max bias was 0.043, and no repeated differences were observed. This is a black-box development screen, not trail-based differential cryptanalysis. + +| Status | Cases | Mean Rot. Dist. Range | Exact Rot. Relations | Elapsed (s) | +| --- | --- | --- | --- | --- | +| PASS | 36 | 127.570–128.284 | 0 | 393.8 s | + +Table 6. Black-box rotational relation probe (36 cases, 6 rotation amounts × 6 round counts, 2,048 samples each). Mean rotational distance stayed near 128.0, and no exact rotational relations were observed. This is not formal rotational cryptanalysis. + +### 7.3 Small Black-Box Algebraic Degree Screen + +The small black-box algebraic degree screen estimates sampled Boolean degree over 10 input variables for selected output bits. It is intended to catch obvious low-degree behavior. It does not perform SAT, MILP, Gröbner-basis analysis, full ANF extraction, or invariant cryptanalysis. + +| Rounds | Vars | Out Bits | Status | Min Degree | Mean Degree | Max Degree | Low-deg Outputs | Elapsed (s) | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | 10 | 128 | PASS | 9 | 9.578 | 10 | 0 | 1.66 | +| 2 | 10 | 128 | PASS | 9 | 9.531 | 10 | 0 | 1.78 | +| 4 | 10 | 128 | PASS | 9 | 9.516 | 10 | 0 | 2.06 | +| 8 | 10 | 128 | PASS | 9 | 9.531 | 10 | 0 | 2.61 | +| 12 | 10 | 128 | PASS | 9 | 9.523 | 10 | 0 | 3.20 | +| 16 | 10 | 128 | PASS | 9 | 9.359 | 10 | 0 | 4.04 | + +Table 7. Small black-box algebraic degree screen at six round counts. Sampled ANF degree was near the 10-variable ceiling at all tested rounds; zero low-degree sampled outputs were detected. This does not replace formal algebraic cryptanalysis. + +### 7.4 Overlap/Fork Stream Uniqueness and State-Recovery/Predictability Screens + +| Status | Streams | Bytes/Stream | Block Size | Blocks Tested | Overlaps | Mean Pfx Hamming | Elapsed (s) | +| --- | --- | --- | --- | --- | --- | --- | --- | +| PASS | 4 | 4,194,304 | 32 B | 524,288 | 0 | 4,074 bits | 0.49 s | + +Table 8. Overlap/fork stream uniqueness screen. No overlapping 32-byte blocks across 4 streams × 524,288 blocks. + +| Status | Train Bytes | Test Bytes | Next-byte Acc. | Bit Accuracy | BM LC Ratio | Elapsed (s) | +| --- | --- | --- | --- | --- | --- | --- | +| PASS | 2,097,152 | 2,097,152 | 0.00396 | 0.50009 | 0.5000 | 7.35 s | + +Table 9. Black-box state-recovery/predictability screen. Next-byte accuracy (0.00396) is at chance (1/256 ≈ 0.00391). BM linear complexity ratio 0.5000, consistent with a non-linear sequence. + +### 7.5 Reduced-Round Combined Probe + +A combined reduced-round screen ran the black-box differential diffusion and black-box rotational relation probes across 72 cases and six round counts. All 72 cases returned PASS under the tested budget. This result is useful triage evidence, but it does not bound reduced-round attack complexity. + +Figure 4. Black-box development-probe dashboard. (a) Differential diffusion mean Δbits, all near ideal 128.0. (b) Rotational relation distance, all near 128.0, zero exact relations. (c) sampled ANF degree vs. rounds, near variable count 10. (d) black-box state-recovery/predictability screen near chance baselines. + +### 7.6 White-Box Round-Model Check + +The repository now includes a first white-box schedule model in tests/crypto_analysis/screens/whitebox_round_model.py. Unlike the black-box probes, this script reads the specified tetrahedron, edge, shell, and lane-permutation rules directly. It tracks word-level dependency: which original 64-bit lanes can influence which later 64-bit lanes after each round. + +The 24-round local run reports 48 tetrahedral neighborhoods and 54 edge neighborhoods per round. At word granularity, all tracked state lanes reached full 32-lane dependency by round 2, and the first 32 output bytes also reached full 32-lane dependency by round 2. The same report records that shell vertex and opposite-lane schedules touch all 27 vertex lanes over the 24-round schedule period, and that edge and permutation layers use all rotation counts 1 through 61. + +This is useful schedule evidence. It is not a differential trail search, rotational analysis, algebraic model, SAT/MILP result, or state-recovery attack. It only says that the current schedule gives broad word-level reachability quickly. + +## 8. External Statistical Battery Evaluation + +External statistical batteries apply diverse tests to pseudorandom output, checking for deviations from true-random behavior. Passing batteries is necessary but far from sufficient for cryptographic quality — many constructions with exploitable weaknesses pass standard batteries at moderate data volumes [10]. All batteries were run against the C binary via stdin32 streaming adapter. + +| Battery | Generator | Status | Bytes Requested | Runtime | Notes | +| --- | --- | --- | --- | --- | --- | +| Dieharder (full) | standalone C TriCube stream | PASS | ~1 TiB consumed | ~75 s | 109 PASSED, 2 WEAK, 0 FAILED — complete run | +| TestU01 Crush | standalone C TriCube stream | PASS | ~multi-GiB | 26m 22s | 144 tests, 0 failed, 0 suspected — all passed | +| PractRand 128 MiB | standalone C TriCube stream | WARN | 134,217,728 | 3.1 s | No stdout anomalies; WARN from SIGPIPE harness | +| PractRand 1 GiB | standalone C TriCube stream | WARN | 1,073,741,824 | 152 s | Low-bit NS3 anomalies at 1 MiB and 512 MiB | +| PractRand 64 MiB | tricube_geo256_fast | PASS | 67,108,864 | 1.8 s | No anomalies; producer exited cleanly | +| PractRand 64 MiB | sha256_counter_chain | WARN | 67,108,864 | 1.9 s | SIGPIPE harness warning; no stdout anomalies | +| PractRand 1 GiB | fast8x | PASS | 1,073,741,824 | ablation-lab | Clean in the optimized-variant screen; continued multi-seed testing still required | +| SmokeRand express | fast8x | PASS | express battery | ablation-lab | 7/7 tests passed | +| TestU01 SmallCrush | fast8x | PASS | 15 tests | ablation-lab | 15/15 tests passed | + +Table 10. External battery summary. WARN entries with no stdout anomalies reflect SIGPIPE harness exit, not statistical failures. The PractRand 1 GiB WARN is a real statistical finding. + +Figure 5. External battery results. (a) Dieharder: 109 PASSED, 2 WEAK (sts_serial), 0 FAILED. (b) TestU01 Crush: 186 p-values, near-uniform, none anomalous. (c) PractRand 1 GiB checkpoints with NS3 R-statistic overlay: anomalies at 1 MiB and 512 MiB; final 1 GiB checkpoint clean. + +### 8.1 Dieharder — Complete Run + +A complete Dieharder 3.31.2beta run consuming ~1 TiB produced 109 PASSED, 2 WEAK, 0 FAILED across 111 assessments. The two WEAK results are both sts_serial at ntuple 15 (p=0.99994) and ntuple 16 (p=0.99636). In Dieharder, WEAK means the p-value is in the extreme tail but not extreme enough to be FAILED. With 111 tests at a 0.005 tail threshold, the expected number of WEAK results is ≈1.1, making 2 within normal variation. Mean p-value across all assessments: 0.2299 (stddev 0.1487, error-rate 0.0). + +| Test Family | Assessments | PASSED | WEAK | FAILED | Notes | +| --- | --- | --- | --- | --- | --- | +| diehard_* (classic Diehard suite) | 13 | 13 | 0 | 0 | birthdays, operm5, rank_32x32, rank_6x8, bitstream, opso, oqso, dna, count_1s_str/byt, parking_lot, 2dsphere, 3dsphere | +| diehard_squeeze, diehard_runs (×2), diehard_craps (×2) | 5 | 5 | 0 | 0 | All new vs. prior partial artifact | +| marsaglia_tsang_gcd (×2) | 2 | 2 | 0 | 0 | | +| sts_monobit, sts_runs, sts_serial (ntup 1–16, 32 rows) | 35 | 33 | 2 | 0 | WEAK: sts_serial ntup=15 (p=0.99994) and ntup=16 (p=0.99636); expected at this sample size | +| rgb_bitdist (ntup 1–12) | 12 | 12 | 0 | 0 | | +| rgb_minimum_distance (dim 2–5) | 4 | 4 | 0 | 0 | | +| rgb_permutations (ntup 2–5) | 4 | 4 | 0 | 0 | | +| rgb_lagged_sum (lag 0–32) | 33 | 33 | 0 | 0 | | +| rgb_kstest_test | 1 | 1 | 0 | 0 | | +| dab_bytedistrib, dab_dct | 2 | 2 | 0 | 0 | | +| dab_filltree2 (ntup 0–1) | 2 | 2 | 0 | 0 | Test 207 skipped by Dieharder binary | +| dab_monobit2 | 1 | 1 | 0 | 0 | | +| TOTAL | 111 | 109 | 2 | 0 | Mean p-value 0.2299; stddev 0.1487; error-rate 0.0 | + +Table 11. Full Dieharder battery breakdown. 109 PASSED, 2 WEAK, 0 FAILED across 111 rows. + +### 8.2 TestU01 Crush — Complete Run + +Full TestU01 Crush (144 tests) was completed against the C generator. Of 186 individual p-values extracted, none fell below 0.001 or above 0.999 (range: 0.003–0.998). The battery reported "All tests were passed" after 26m 22s CPU time. Crush applies ~14× as many tests as SmallCrush, including bit-level distribution, autocorrelation, and high-dimensional uniformity tests. Passing without failure or suspected result is meaningful evidence of output quality, within the caveat that battery tests cannot certify cryptographic security. + +| Metric | Value | +| --- | --- | +| Battery | TestU01 Crush (v1.2.3) | +| Generator | standalone C TriCube stream (stdin32) | +| Host | Mac mini local research machine (Apple M4 Pro public summary) | +| Number of statistics | 144 | +| Total CPU time | 00:26:22.53 | +| Tests FAILED | 0 | +| Tests suspected | 0 | +| p-values extracted | 186 (across multi-statistic tests) | +| p < 0.001 (anomalous) | 0 | +| p > 0.999 (anomalous) | 0 | +| p-value range | 0.003 – 0.998 (all within expected uniform distribution) | +| Summary verdict | All tests were passed | + +Table 12. TestU01 Crush summary. 144 tests, 186 p-values, all within expected uniform range. CPU time: 26m 22s. + +### 8.3 PractRand Evaluation -External batteries can be run with the scripts in `tools/`, provided the tools are installed locally. +IMPORTANT: The PractRand 1 GiB run produced real statistical anomalies — not harness artifacts — at the 1 MiB and 512 MiB checkpoints. These require targeted follow-up. + +Short comparison runs (64–128 MiB) against multiple generators showed no stdout anomalies for any non-degenerate generator. The sha256_counter_chain control also received WARN wrapper status from SIGPIPE, confirming this is a harness-level classification issue common to all streaming generators. The zero_stream control correctly received FAIL, confirming the harness detects bad generators. + +The expanded 1 GiB run (RNG_test stdin32 -tlmin 1KB -tlmax 1GB -tf 2 -te 1, seed 123) produced the following anomalies: + +| Data Level | Test ID | Statistic | p-value | PractRand Assessment | +| --- | --- | --- | --- | --- | +| 1 MiB | [Low1/64]NS3[2:hw:both] | R=+5.1 | ~1.4×10⁻⁷ | suspicious | +| 1 MiB | [Low1/64]NS3[2:hw:all-] | R=+4.4 | ~1.9×10⁻⁶ | unusual | +| 1 MiB | [Low4/32]NS3[4:hw:both] | R=+4.9 | ~3.8×10⁻⁷ | mildly suspicious | +| 1 MiB | [Low4/32]NS3[4:hw:all-] | R=+4.0 | ~1.2×10⁻⁵ | unusual | +| 2 MiB | [Low1/64]NS3[2:hw:both] | R=+4.1 | ~1.7×10⁻⁵ | unusual | +| 512 MiB | [Low1/32]NS3[1:pd:both] | R=+4.0 | ~3.5×10⁻⁵ | unusual | +| 1 GiB | (final checkpoint) | — | — | no anomalies in 2050 test results | + +Table 13. PractRand 1 GiB anomalies. NS3 low-bit failures at 1 MiB and 512 MiB are real statistical findings. Final 1 GiB checkpoint was clean. + +The NS3 (Near-Neighbor Spatial) test probes for bit-pattern non-uniformity. The Low1/64 and Low4/32 prefixes localize anomalies to the lowest bit(s) of 64-bit and 32-bit output words — a known indicator of insufficient low-bit diffusion in the round function. The clean final 1 GiB checkpoint does not resolve this: PractRand can show anomalies at intermediate volumes when the statistical signal is weak. Whether this reflects genuine low-bit structure or sampling variance requires extended runs (multiple seeds, ≥10 GiB) and targeted low-bit diagnostics. + +## 9. Discussion and Limitations + +### 9.1 What the Evidence Supports and Does Not Support + +The accumulated evidence is consistent with TriCube producing output statistically close to uniform over the tested data volumes and probe budgets. This is encouraging as a first filter. However, battery-passing and black-box probe-passing are not security certification. The current evidence does not establish collision resistance, preimage resistance, pseudorandomness, or real-world security. + +The evidence explicitly does not support: cryptographic security of any kind; clean PractRand passage for the baseline stream path, because the 1 GiB NS3 anomalies remain unresolved; resistance to formal differential, rotational, algebraic, state-recovery, or side-channel attacks; or performance competitiveness with mature optimized hash libraries. + +### 9.2 Design Observations and Open Questions + +The geometric approach is of intellectual interest because tetrahedral mixing and edge-coupling provide a different local interaction pattern than the uniform G-function of BLAKE or Keccak's bitwise permutation, potentially producing a different algebraic structure. Whether that structure is harder or easier to attack is unknown. The NS3 anomalies are a concrete hint: if the edge-coupling pass — which XORs rotated words — does not propagate low-bit differences uniformly, the rotation constants may need targeted recalibration for low-bit diffusion. The primary engineering weaknesses are performance below mature optimized hash libraries, the need for independent reimplementation from the specification, and the absence of formal white-box cryptanalysis beyond the current word-level schedule model. + +## 10. Future Work + +The next work is deliberately narrower than the list of possible cryptanalytic tasks. + +Formal cryptanalysis should be built on standard tooling rather than custom ad +hoc tests. Candidate tools include Z3 or related SMT solvers, SAT solvers such +as CryptoMiniSat, SageMath for small Boolean-polynomial experiments, and +ARX-oriented frameworks such as CLAASP, CryptoSMT, or ArxPy where TriCube's +nonstandard tetrahedral schedule can be represented correctly. The required +custom component is a verified reduced-round TriCube model, not a replacement +for the solver and algebra systems themselves. + +First, the PractRand low-bit warning needs a multi-seed campaign. The correct next run is at least three seeds at 10 GiB or more, followed by focused low-bit diagnostics if the NS3 pattern repeats. + +Second, the new word-level schedule model should be extended into a reduced-round white-box program. That program should model modular-addition differences, rotation propagation, and output extraction well enough to search for high-probability differential trails. Until that exists, the current differential diffusion probe remains only a black-box development gate. + +Third, algebraic work should start with reduced-round Boolean or word-level encodings. A useful first target is not a full attack; it is a verified SAT, SMT, or MILP model that reproduces known reduced-round input/output behavior and can then search for collisions, preimages, invariants, or impossible states at small round counts. + +Fourth, the specification should receive an independent implementation test. A reviewer should implement TriCube from docs/specification.md without reading c/src/tricube.c, then compare against the fixed vectors and report any ambiguity. + +Finally, engineering work should continue on the C implementation and benchmark it against BLAKE3, SHA-256, SHA-512, and SHA-3 under equivalent conditions. Speed improvements that reintroduce low-bit warnings should remain excluded. ## 11. Conclusion -TriCube is a concrete experimental candidate, not a secure primitive. The strongest current result is that the geometric construction has been made deterministic, implemented in standalone C, wrapped in Python, and tested enough to identify the next hard questions. The main blocking issue is cryptanalytic evidence, especially the PractRand low-bit warning and the absence of reduced-round and differential analysis. +TriCube is an experimental hash function and XOF whose current public baseline is defined by tetrahedral/cube-connected evolution over a 2048-bit state. Implemented in both C and Python with a comprehensive test harness, the May 2026 evaluation produced positive statistical evidence, a clear PractRand low-bit warning, and a concrete optimization path represented by the experimental fast8x stream variant. The correct conclusion is continued review and hardening, not security deployment. + +## Acknowledgements + +Funding: This work received no external funding. + +Conflicts of interest: The author declares none. + +AI assistance: AI-assisted tooling was used during implementation, testing orchestration, and manuscript preparation. All benchmark outputs and experimental results reflect actual execution on the hardware described in Section 5. All interpretations and claim boundaries were reviewed by the author through direct inspection of machine-generated artifacts. The AI tooling did not generate, fabricate, or modify any benchmark data. + +Data and code availability: The source code and result artifacts are maintained in a public GitHub repository at https://github.com/RRG314/tricube-hash. Reproduction commands are provided in Appendix A. + +## References + +[1] NIST. FIPS 180-4: Secure Hash Standard. 2015. + +[2] NIST. FIPS 202: SHA-3 Standard — Permutation-Based Hash and XOF. 2015. + +[3] Aumasson et al. BLAKE2: Simpler, Smaller, Fast as MD5. RFC 7693, IETF, 2015. + +[4] O'Brien et al. BLAKE3 One Function, Fast Everywhere. GitHub/IACR, 2020. + +[5] Bernstein. CubeHash specification (2.B.1). NIST SHA-3 Submission, 2009. + +[6] Bertoni, Daemen et al. Keccak reference 3.0. SHA-3 Submission, 2011. + +[7] Daemen et al. Xoodyak, a Lightweight Cryptographic Scheme. IACR ToSC 2020(4). + +[8] Bertoni et al. KangarooTwelve: Fast Hashing Based on Keccak-p. ACNS 2018. + +[9] Bertoni et al. Sponge Functions. ECRYPT Hash Workshop, 2007. + +[10] Hellekalek & Wegenkittl. Empirical Evidence Concerning AES. ACM TOMACS, 2003. + +[11] Dworkin (NIST). SP 800-22 Rev. 1a: NIST Statistical Test Suite. 2010. + +[12] Brown. PractRand: Practical tests for random number generators. pracrand.sourceforge.net, 2010–. + +[13] Marsaglia. Dieharder: A random number test suite. Duke University, http://webhome.phy.duke.edu/~rgb/General/dieharder.php. + +[14] L'Ecuyer & Simard. TestU01: A C Library for Empirical Testing of Random Number Generators. ACM TOMS 33(4), 2007. + +[15] Biham & Shamir. Differential Cryptanalysis of DES-like Cryptosystems. J. Cryptology 4(1), 1991. + +[16] Webster & Tavares. On the Design of S-boxes (SAC). CRYPTO 1985, LNCS 218. + +[17] Daemen & Rijmen. The Design of Rijndael. Springer, 2002. + +[18] Aumasson & Meier. Zero-sum Distinguishers for Reduced Keccak-f. Rump CRYPTO 2009. + +[19] Courtois & Pieprzyk. Cryptanalysis of Block Ciphers with Overdefined Systems. ASIACRYPT 2002. + +[20] Khovratovich & Nikolic. Rotational Cryptanalysis of ARX. FSE 2010. + +[21] NIST. SP 800-232: Ascon-Based Lightweight Cryptography Standards for Constrained Devices. 2025. + +[22] Dobraunig, Eichlseder, Mendel, and Schläffer. Ascon v1.2: Lightweight Authenticated Encryption and Hashing. Journal of Cryptology, 2021. + +## Appendix A: Reproduction Command Index + +Commands are relative to the public repository root. Commands were executed during the May 19, 2026 test session. + +Unit tests: + +.venv/bin/python -m pytest -q + +Internal statistical benchmark: + +.venv/bin/python benchmarks/bench_tricube.py --out-dir .../tricube_refresh_2026-05-19 + +100 MiB stream benchmark: + +python benchmarks/bench_stream.py --bytes 104857600 + +C binary build: + +make -C c all + +PractRand 1 GiB: + +tools/run_practrand.sh 1073741824 + +Differential diffusion / rotational relation / algebraic degree / overlap-fork uniqueness / state-recovery-predictability / reduced-round probes: + +Probe scripts from the original archive must be cleaned before inclusion; public summaries are in docs/testing.md and results/consolidated-results-2026-05-19.md. + +## Appendix B: Result Artifact Index + +Public summary artifacts are kept under results/ and tests/ablation_lab/ in the TriCube repository. Older raw archive artifacts are listed only for provenance when they are not included publicly. + +| Artifact | Exists | Covers | +| --- | --- | --- | +| tricube_refresh_2026-05-19 | yes | Section 6: Project Benchmark | +| tricube_structural_gaps_standard_2026-05-19 | yes | Section 7.1: Structural Gap Checks | +| tricube_attack_differential_standard_2026-05-19 | yes | Section 7.2: Black-box Differential Diffusion Probe | +| tricube_attack_rotational_standard_2026-05-19 | yes | Section 7.2: Black-box Rotational Relation Probe | +| tricube_attack_algebraic_standard_2026-05-19 | yes | Section 7.3: Small Black-box Algebraic Degree Screen | +| tricube_attack_overlap_fork_standard_2026-05-19 | yes | Section 7.4: Overlap/Fork Stream Uniqueness Screen | +| tricube_attack_state_recovery_standard_2026-05-19 | yes | Section 7.4: Black-Box State-Recovery/Predictability Screen | +| tricube_attack_reduced_round_standard_2026-05-19 | yes | Section 7.5: Reduced-Round Probe | +| manual_dieharder_1tb_full-e7244043.log | yes | Section 8.1: Full Dieharder (109P/2W/0F) | +| crush_stdout-89028ed3.txt | yes | Section 8.2: TestU01 Crush — 144 tests passed | +| external_batteries_practrand_2026-05-19 | yes | Section 8.3: Short PractRand Runs | +| practrand_1gb_20260519_193201 | yes | Section 8.3: PractRand 1 GiB (WARN) | +| tricube_stream_benchmark_100mb_2026-05-19.txt | yes | Section 6.3: 100 MiB Throughput | + +## Appendix C: Glossary + +ANF (Algebraic Normal Form). The unique multilinear polynomial representation of a Boolean function. High ANF degree is a necessary (but not sufficient) condition for resistance to algebraic attacks. + +ARX (Add-Rotate-XOR). Design paradigm using modular addition, bitwise rotation, and XOR as primitives. BLAKE and ChaCha are ARX designs. + +Avalanche / SAC. A property requiring that flipping one input bit changes ~half the output bits (mean ≈ 0.5). The Strict Avalanche Criterion (SAC) is a formal version. + +NS3 (Near-Neighbor Spatial test, v3). A PractRand test checking for bit-pattern non-uniformity. Low-bit prefixes (Low1/64, Low4/32) indicate anomalies localized to the lowest bit(s) of output words. + +PractRand. Progressive randomness battery by Chris Doty-Humphrey, testing at increasing data volumes. Designed to detect biases requiring large data to manifest. -## Declarations +SIGPIPE. Unix signal sent to a process writing to a closed pipe. In streaming battery setups, SIGPIPE is the normal generator exit mode when the battery stops reading; it is not a statistical finding. -This work received no external funding. The author reports no external financial conflict of interest related to this repository. AI assistance was used to organize code, documentation, tests, and manuscript material. The design, claims, limitations, and interpretation remain the responsibility of the author. +Sponge construction. Framework using absorb (XOR input into permuted state) and squeeze (extract output, permute between blocks) phases. Introduced by Bertoni et al. -Code availability: https://github.com/RRG314/tricube-hash +XOF (Extendable Output Function). A hash-like construction producing arbitrary-length output from fixed-length input, used for key derivation and stream generation. diff --git a/paper/references.bib b/paper/references.bib index 8fca838..2d2f8f5 100644 --- a/paper/references.bib +++ b/paper/references.bib @@ -36,9 +36,141 @@ @misc{cubehash url = {https://ehash.isec.tugraz.at/wiki/CubeHash.html} } +@techreport{nist_sp_800_232_ascon, + author = {S{\"o}nmez Turan, Meltem and McKay, Kerry A. and Chang, Donghoon and Kang, Jinkeon and Kelsey, John}, + title = {{Ascon-Based Lightweight Cryptography Standards for Constrained Devices}}, + institution = {{National Institute of Standards and Technology}}, + type = {{NIST Special Publication}}, + number = {800-232}, + year = {2025}, + doi = {10.6028/NIST.SP.800-232}, + url = {https://doi.org/10.6028/NIST.SP.800-232} +} + +@article{ascon_v12_joc, + author = {Dobraunig, Christoph and Eichlseder, Maria and Mendel, Florian and Schl{\"a}ffer, Martin}, + title = {{Ascon v1.2: Lightweight Authenticated Encryption and Hashing}}, + journal = {{Journal of Cryptology}}, + volume = {34}, + number = {3}, + pages = {33}, + year = {2021}, + doi = {10.1007/s00145-021-09398-9}, + url = {https://doi.org/10.1007/s00145-021-09398-9} +} + +@article{testu01, + author = {L'Ecuyer, Pierre and Simard, Richard}, + title = {{TestU01: A C Library for Empirical Testing of Random Number Generators}}, + journal = {{ACM Transactions on Mathematical Software}}, + volume = {33}, + number = {4}, + pages = {22}, + year = {2007}, + doi = {10.1145/1268776.1268777}, + url = {https://www.iro.umontreal.ca/~lecuyer/myftp/papers/testu01.pdf} +} + +@misc{practrand, + author = {Doty-Humphrey, Chris}, + title = {{PractRand Random Number Test Suite}}, + url = {https://pracrand.sourceforge.net/} +} + +@misc{dieharder, + author = {Brown, Robert G.}, + title = {{Dieharder: A Random Number Test Suite}}, + url = {https://webhome.phy.duke.edu/~rgb/General/dieharder.php} +} + @misc{nist_sts, author = {{National Institute of Standards and Technology}}, title = {{Random Bit Generation Documentation and Software}}, url = {https://csrc.nist.gov/Projects/random-bit-generation/Documentation-and-Software} } +@misc{smokerand, + author = {Alvoskov, Nikolay}, + title = {{SmokeRand}}, + url = {https://github.com/alvoskov/SmokeRand} +} + +@article{biham_shamir_differential_1991, + author = {Biham, Eli and Shamir, Adi}, + title = {{Differential Cryptanalysis of DES-like Cryptosystems}}, + journal = {{Journal of Cryptology}}, + volume = {4}, + number = {1}, + pages = {3--72}, + year = {1991}, + doi = {10.1007/BF00630563}, + url = {https://doi.org/10.1007/BF00630563} +} + +@inproceedings{khovratovich_nikolic_rotational_2010, + author = {Khovratovich, Dmitry and Nikolic, Ivica}, + title = {{Rotational Cryptanalysis of ARX}}, + booktitle = {{Fast Software Encryption, 17th International Workshop, FSE 2010}}, + year = {2010}, + pages = {333--346}, + doi = {10.1007/978-3-642-13858-4_19}, + url = {https://www.iacr.org/archive/fse2010/61470339/61470339.pdf} +} + +@misc{courtois_pieprzyk_xsl_2002, + author = {Courtois, Nicolas T. and Pieprzyk, Josef}, + title = {{Cryptanalysis of Block Ciphers with Overdefined Systems of Equations}}, + howpublished = {{Cryptology ePrint Archive, Paper 2002/044}}, + year = {2002}, + url = {https://eprint.iacr.org/2002/044} +} + +@inproceedings{webster_tavares_sboxes_1985, + author = {Webster, Alan F. and Tavares, Stafford E.}, + title = {{On the Design of S-Boxes}}, + booktitle = {{Advances in Cryptology -- CRYPTO '85 Proceedings}}, + year = {1986}, + pages = {523--534}, + doi = {10.1007/3-540-39799-X_41}, + url = {https://doi.org/10.1007/3-540-39799-X_41} +} + +@inproceedings{z3_tacas_2008, + author = {de Moura, Leonardo and Bj{\o}rner, Nikolaj}, + title = {{Z3: An Efficient SMT Solver}}, + booktitle = {{Tools and Algorithms for the Construction and Analysis of Systems}}, + year = {2008}, + pages = {337--340}, + doi = {10.1007/978-3-540-78800-3_24}, + url = {https://doi.org/10.1007/978-3-540-78800-3_24} +} + +@misc{sagemath, + author = {{The Sage Developers}}, + title = {{SageMath, the Sage Mathematics Software System}}, + url = {https://www.sagemath.org/} +} + +@misc{cryptominisat, + author = {Soos, Mate}, + title = {{CryptoMiniSat SAT Solver}}, + url = {https://github.com/msoos/cryptominisat} +} + +@misc{claasp, + author = {{TII Cryptanalysis Team}}, + title = {{CLAASP: Cryptographic Library for Automated Analysis of Symmetric Primitives}}, + url = {https://claasp.readthedocs.io/} +} + +@misc{cryptosmt, + author = {{CryptoSMT Project}}, + title = {{CryptoSMT: A Tool for Cryptanalysis of Symmetric Primitives Based on SMT/SAT Solvers}}, + url = {https://github.com/kste/cryptosmt} +} + +@misc{arxpy, + author = {{ArxPy Project}}, + title = {{ArxPy: SMT-Based Analysis of ARX Primitives}}, + url = {https://ranea.github.io/ArxPy/} +} diff --git a/python/README.md b/python/README.md index 8f78c9c..60130a9 100644 --- a/python/README.md +++ b/python/README.md @@ -13,7 +13,7 @@ The full project repository is: https://github.com/RRG314/tricube-hash -The current manuscript draft is kept with the repository: +The current manuscript is kept with the repository: https://github.com/RRG314/tricube-hash/tree/main/paper @@ -78,12 +78,22 @@ tricube hash --hex 616263 This package contains the clean Python reference implementation and fixed test vectors. The public GitHub repository also contains the standalone C implementation, benchmarks, external-battery scripts, documentation, and the -manuscript draft. +manuscript. The current evidence supports continued research and external review. It does not establish collision resistance, preimage resistance, pseudorandomness, or security for real applications. +## Third-Party Tools + +The PyPI package does not bundle SmokeRand, PractRand, Dieharder, TestU01, NIST +STS, third-party hash implementations, external binaries, raw battery logs, or +manuscript files. External batteries referenced by the repository must be +installed separately and used under their own upstream licenses. The repository +notice table is maintained at: + +https://github.com/RRG314/tricube-hash/blob/main/THIRD_PARTY_NOTICES.md + ## License TriCube is released under the MIT License. diff --git a/results/README.md b/results/README.md index 4a65904..e301224 100644 --- a/results/README.md +++ b/results/README.md @@ -1,10 +1,21 @@ # Results -This directory contains concise public-facing result summaries, not raw archive dumps. +This directory contains concise public-facing result summaries, not large uncurated logs. - `consolidated-results-2026-05-19.md` is the main cleaned evidence snapshot. - `local-validation-2026-05-19.md` records the local build, test, package, CLI, and smoke benchmark run for this public repo. - `raw/practrand-1gb-summary-2026-05-19.md` summarizes the most important external-battery warning. - `raw/practrand-1gb-stdout-2026-05-19.txt` preserves the corresponding PractRand output log. +The fast8x stream optimization is documented under +`tests/ablation_lab/` rather than as a large result-log bundle. That record +explains why `fast8x` was added as an explicit experimental stream variant and +why faster low-bit-warning variants were not promoted. + +The key fast8x throughput result is 132.655 MiB/s on a 256 MiB stream run, +versus 69.796 MiB/s for the released TriCube baseline in the same C ablation +harness. That is a baseline comparison inside the TriCube implementation +family. Optimized SHA-2, SHA-3/SHAKE, BLAKE2, and BLAKE3 library comparisons +remain future work. + Large external-battery logs should normally be attached to releases or stored outside the main repository unless they are small enough and important enough to inspect directly. diff --git a/results/consolidated-results-2026-05-19.md b/results/consolidated-results-2026-05-19.md index 5642934..2ce50d1 100644 --- a/results/consolidated-results-2026-05-19.md +++ b/results/consolidated-results-2026-05-19.md @@ -2,6 +2,70 @@ TriCube remains experimental. These results support continued testing and external review; they do not establish cryptographic security. +## 2026-05-20 Fast8x Stream Update + +The public C API now includes an explicit experimental stream variant, +`fast8x`, for continued statistical testing. It does not replace the released +baseline stream and must be requested by name. + +In the ablation-lab run, `fast8x` measured `132.655 MiB/s` on a 256 MiB stream +benchmark, compared with `69.796 MiB/s` for the baseline in the same harness. +The ablation record lists `fast8x` as clean through PractRand 1 GiB, SmokeRand +express 7/7, and TestU01 SmallCrush 15/15. Faster candidates were not promoted +because they introduced low-bit PractRand warnings or failures. + +See `tests/ablation_lab/` for the compact public ablation record. This +update is performance and statistical-screening evidence only; it does not +establish cryptographic security. + +## 2026-05-20 Probe/Screens Reproducibility Update + +The black-box development probes have been moved from result-only descriptions +into a reproducible screen suite under `tests/crypto_analysis/`. The +suite uses precise names for each screen: black-box differential diffusion +probe, black-box rotational relation probe, small black-box algebraic degree +screen, collision/birthday sanity check, near-collision sanity check, +overlap/fork stream uniqueness screen, black-box state-recovery/predictability +screen, and low-bit diagnostic screen. + +The latest quick profile compared `baseline` and `fast8x` with the same +stream-output interface and produced `78 PASS` rows and `10 BLOCKED` external +battery rows. The blocked rows are missing-tool markers for PractRand, +SmokeRand, TestU01, Dieharder, and NIST STS in this local shell; they are not +statistical failures. The standalone 16 MiB low-bit diagnostic run produced +`2 PASS` rows, one for `baseline` and one for `fast8x`. + +A first white-box round-model analysis was also added. It models the specified +tetrahedron, edge, shell, and permutation schedule at 64-bit lane granularity. +In the 24-round local run, all tracked lanes and the first 32 output bytes +reached full 32-lane word dependency by round 2. This is useful schedule +coverage evidence, but it is not bit-level differential, rotational, algebraic, +or state-recovery cryptanalysis. + +The compact screen outputs are generated locally under +`tests/crypto_analysis/results/`. Those generated per-run folders are not +committed to the repository so it does not accumulate duplicate +machine-specific result snapshots. Reproduce them with: + +```bash +python3 tests/crypto_analysis/run_all_screens.py \ + --profile quick \ + --variants baseline,fast8x \ + --out tests/crypto_analysis/results/quick-latest + +python3 tests/crypto_analysis/screens/low_bit_diagnostics.py \ + --variants baseline,fast8x \ + --bytes 16777216 \ + --out tests/crypto_analysis/results/low-bit-latest + +python3 tests/crypto_analysis/screens/whitebox_round_model.py \ + --rounds 24 \ + --out tests/crypto_analysis/results/whitebox-latest +``` + +These screens are development gates. They can find obvious failures or warning +patterns, but they do not replace white-box cryptanalysis. + ## Environment | Item | Value | @@ -59,15 +123,20 @@ This is a WARN result. It should drive follow-up testing, not be treated as a cl | Full digest collision smoke | PASS | 0 full digest collisions and 0 prefix64 collisions over 20,000 sampled messages. | | Diffusion, 12 rounds | PASS | Mean changed bits 128.027 of 256; stdev 7.981; min 99; max 156. | | Diffusion, 16 rounds | PASS | Mean changed bits 127.819 of 256; stdev 7.950; min 95; max 152. | -| Differential probes | PASS | Tested deltas across 1, 2, 4, 8, 12, and 16 rounds; no repeated output differences observed. | -| Rotational probes | PASS | Tested rotations 1, 7, 8, 13, 16, and 32 across 1, 2, 4, 8, 12, and 16 rounds; mean rotational distances stayed near 128 bits. | -| Algebraic probe | PASS | Small black-box ANF probe reached max degree 10 for sampled 10-variable cases across tested rounds. | +| Black-box differential diffusion probe | PASS | Tested deltas across 1, 2, 4, 8, 12, and 16 rounds; no repeated output differences observed. | +| Black-box rotational relation probe | PASS | Tested rotations 1, 7, 8, 13, 16, and 32 across 1, 2, 4, 8, 12, and 16 rounds; mean rotational distances stayed near 128 bits. | +| Small black-box algebraic degree screen | PASS | Sampled ANF screen reached max degree 10 for sampled 10-variable cases across tested rounds. | | Domain/tweak separation | PASS | 5 unique digests; minimum hamming distance from default case was 121 bits. | | Bit influence spread | PASS | Mean output flip rate 0.5009918; min 0.425781; max 0.570312. | -| State-recovery screen | PASS | Next-byte prediction accuracy 0.00396061; bit accuracy 0.500095; linear-complexity ratio 0.5. | -| Related-seed overlap/fork test | PASS | 0 repeated 32-byte block overlaps across 4 streams and 262,144 tested blocks. | +| Black-box state-recovery/predictability screen | PASS | Next-byte prediction accuracy 0.00396061; bit accuracy 0.500095; linear-complexity ratio 0.5. | +| Overlap/fork stream uniqueness screen | PASS | 0 repeated 32-byte block overlaps across 4 streams and 262,144 tested blocks. | -These probes are development gates. They can find obvious problems, but they do not replace cryptanalysis. +These probes are development gates. They can find obvious problems, but they do +not replace cryptanalysis. The black-box differential diffusion probe does not +search trails or bound differential probability. The black-box rotational +relation probe does not prove resistance to rotational distinguishers. The +small black-box algebraic degree screen does not perform SAT, MILP, +Gröbner-basis, full ANF, or invariant analysis. ## Performance @@ -75,10 +144,12 @@ These probes are development gates. They can find obvious problems, but they do | Implementation / mode | Throughput | Notes | |---|---:|---| +| Experimental C `fast8x` stream variant | 132.655 MiB/s | 256 MiB ablation-lab run; explicit opt-in variant. | +| Released C baseline stream in same ablation harness | 69.796 MiB/s | Same 256 MiB benchmark run as `fast8x`. | | `tricube_tc256_xof_fast` | 80.158 MiB/s | Python harness candidate, 1 MiB stream sanity run. | | `tricube_geo256_chain_fast` | 62.255 MiB/s | Python harness candidate, 1 MiB stream sanity run. | | `tricube_tetra_block256_chain_fast` | 57.276 MiB/s | Python harness candidate, 1 MiB stream sanity run. | -| Current standalone C TriCube stream | 51.513 MiB/s | 1 MiB refresh run; local package smoke measured 53.147 MiB/s. | +| Current standalone C TriCube stream | 51.513 MiB/s | 1 MiB refresh run; package smoke measured 53.147 MiB/s. | | `sha256_counter_chain` harness control | 51.307 MiB/s | Python harness control, not an optimized C SHA comparison. | ### Hash Throughput @@ -100,15 +171,14 @@ Hash throughput is the main engineering weakness. The current C implementation i The strongest positive evidence is the TestU01 Crush pass, the Dieharder battery with no failures, the NIST STS pass, the absence of obvious failures in the structural probes, and enough C stream throughput to run longer external batteries. -The strongest negative evidence is the PractRand low-bit warning. That issue needs multi-seed, low-bit-focused, and longer-run follow-up. TriCube also still lacks independent cryptanalysis, reduced-round attacks, differential trail work, rotational analysis, algebraic analysis at larger scale, side-channel review, and competitive optimized implementations. +The strongest negative evidence is the PractRand low-bit warning. That issue needs multi-seed, low-bit-focused, and longer-run follow-up. TriCube also still lacks independent cryptanalysis, reduced-round attacks, formal differential trail work, formal rotational-distinguisher analysis, algebraic analysis at larger scale, side-channel review, and competitive optimized implementations. The correct public claim is narrow: > TriCube is an experimental geometric hash/XOF candidate with a concrete C implementation, reproducible vectors, meaningful early statistical-battery evidence, and unresolved cryptanalytic questions. -The wrong public claim is: - -> TriCube is cryptographically secure. +An unsupported public claim would be that the current evidence establishes +security. It does not. ## Required Next Tests @@ -118,8 +188,7 @@ The wrong public claim is: - SmokeRand full battery; - low-bit-focused diagnosis of the stream path; - reduced-round attack search; -- differential and rotational cryptanalysis; +- formal differential and rotational cryptanalysis; - algebraic and invariant analysis; - birthday and near-collision sweeps at larger practical scales; - optimized C throughput comparison against SHA-256, SHA3/SHAKE, BLAKE2, and BLAKE3 libraries. - diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..47941c0 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,25 @@ +# Tests and Analysis + +This directory is the public testing home for TriCube. It keeps implementation +tests, fixed vectors, stream-variant ablation evidence, and cryptanalysis +screens in one place without mixing their purposes. + +## Layout + +- `smoke/` contains small pytest checks that exercise the packaged code and C + vector agreement. +- `vectors/` contains fixed public test vectors. +- `ablation_lab/` contains the compact evidence for adding the experimental + `fast8x` stream variant while leaving rejected high-speed variants out. +- `crypto_analysis/` contains reproducible development screens, low-bit + diagnostics, the word-level schedule model, and optional external-tool setup + checks. + +The folders are separate because they answer different questions. The ablation +lab explains why one variant was kept. The crypto-analysis screens explain how +the current probes are run and what they do not prove. Neither folder +establishes cryptographic security. + +Inside `crypto_analysis/`, the `screens/` subfolder contains the individual +screen entry points. `run_all_screens.py` remains as the combined runner and +imports those same screen modules. diff --git a/tests/ablation_lab/README.md b/tests/ablation_lab/README.md new file mode 100644 index 0000000..8c8647d --- /dev/null +++ b/tests/ablation_lab/README.md @@ -0,0 +1,97 @@ +# TriCube Stream Ablation Lab + +This ablation lab tested whether the released TriCube stream path could be made +faster without accepting obvious statistical regressions. The public repository +keeps only the result that passed the current screening threshold: + +- `baseline`: the released TriCube stream path, kept as the default. +- `fast8x`: an experimental stream variant using 8 initialization rounds, 4 + per-block rounds, a 256-byte stream rate, and an additional TriCube-family + output mixer. + +The ablation lab is about speed and variant selection. The reproducible +black-box development probes for differential diffusion, rotational relations, +algebraic degree, collision/birthday behavior, overlap/fork uniqueness, +predictability, and low-bit diagnostics live separately in +[tests/crypto_analysis/](../crypto_analysis/). Keeping those two folders +separate makes the evidence easier to read: this folder explains why `fast8x` +was the only optimized variant carried forward, while `crypto_analysis` +explains how the current screens are run and what they do not prove. + +The rejected high-speed paths are documented here but are not included in the +public C API. They produced higher throughput by widening the extraction rate +or batching output, but the strongest versions also produced low-bit PractRand +warnings or failures. Those variants remain excluded until the failure mode is +understood. + +## Why fast8x was added + +`fast8x` was the best speed/safety tradeoff in the ablation pass. On the +same Apple M4 Pro machine used for the May 2026 result refresh, it +measured about `132.65 MiB/s` for a 256 MiB stream run, compared with about +`69.80 MiB/s` for the released baseline in the same harness. + +After adding `fast8x` to the repository, a smaller 64 MiB smoke benchmark +measured `130.94 MiB/s` for `fast8x` and `73.07 MiB/s` for the baseline. That +run is recorded in +[tables/fast8x_stream_smoke_bench.csv](tables/fast8x_stream_smoke_bench.csv). + +The same ablation record lists `fast8x` as clean through: + +- PractRand 1 GiB screen; +- SmokeRand express, 7/7 tests; +- TestU01 SmallCrush, 15/15 tests; +- 16 MiB sanity probes with no repeated 32-byte blocks. + +These checks are not security claims. They justify keeping `fast8x` available +for continued testing, not using TriCube in security-critical systems. + +## What changed in fast8x + +Compared with the baseline stream path: + +- the stream domain is separated as `.../STREAM/FAST8X`; +- initialization uses 8 rounds instead of 12; +- each stream block update uses 4 rounds instead of 6; +- the output rate is widened from 192 bytes to 256 bytes per state update; +- output words pass through an additional xmix layer derived from the TriCube + state and round constants. + +The hash API, digest vectors, XOF API, and default stream behavior remain +unchanged. Users must explicitly request the variant: + +```bash +c/build/tricube stream --seed 123 --bytes 1048576 --out stream.bin --variant fast8x +``` + +For source navigation, the named C entry points are in +`c/src/tricube_fast8x.c`. The shared permutation, state schedule, and `fast8x` +stream profile remain in `c/src/tricube.c`. + +## Rejected high-speed variants + +Several faster variants were tested but not promoted: + +- `fast8x_batch` reached about `311.98 MiB/s`, but failed PractRand low-bit FPF + checks at 8 KiB. +- `fast8x_wide` reached about `285.71 MiB/s`, but showed very suspicious + low-bit rows by 16 KiB. +- `fast8x512` reached about `230.74 MiB/s`, but had recurring mild low-bit + warnings. +- `fast4x512` reached about `371.32 MiB/s`, but had too much warning density. + +The public rule is conservative: speed gains that introduce repeatable low-bit +warnings do not get promoted. + +## Tables + +The compact public tables are: + +- [tables/stream_speed_variants.csv](tables/stream_speed_variants.csv) +- [tables/quality_gate_summary.csv](tables/quality_gate_summary.csv) +- [tables/external_screen_summary.csv](tables/external_screen_summary.csv) +- [tables/candidate_recommendations.csv](tables/candidate_recommendations.csv) +- [tables/fast8x_stream_smoke_bench.csv](tables/fast8x_stream_smoke_bench.csv) + +Large generated streams, full terminal transcripts, and rejected C implementations are +not included in the repository. diff --git a/tests/ablation_lab/tables/candidate_recommendations.csv b/tests/ablation_lab/tables/candidate_recommendations.csv new file mode 100644 index 0000000..9519d3d --- /dev/null +++ b/tests/ablation_lab/tables/candidate_recommendations.csv @@ -0,0 +1,6 @@ +rank,variant,speed_mib_s,classification,recommendation,reason +1,fast8x,132.65,best_speed_safety_tradeoff,include_as_experimental_variant,Clean 1 GiB PractRand screen plus SmokeRand express and SmallCrush passes; speed improved without the observed low-bit failures. +2,fast8x512,230.74,fast_with_warning,hold_for_research,Mild recurring PractRand low-bit unusual rows. +3,fast8x_wide,285.71,near_target_failed,reject_current_design,Near target but showed very suspicious low-bit PractRand rows. +4,fast8x_batch,311.98,fast_failed,reject_current_design,Met speed target but failed PractRand low-bit FPF checks at 8 KiB. +5,fast4x512,371.32,fast_failed,reject_current_design,Fastest measured path but warning density was too high. diff --git a/tests/ablation_lab/tables/external_screen_summary.csv b/tests/ablation_lab/tables/external_screen_summary.csv new file mode 100644 index 0000000..92d39e2 --- /dev/null +++ b/tests/ablation_lab/tables/external_screen_summary.csv @@ -0,0 +1,6 @@ +variant,practrand,smokerand_express,smallcrush,notes +fast8x,PASS_1G_CLEAN,PASS_7_0_0,PASS_15_15,Cleanest optimized candidate in the ablation pass. +fast8x512,WARN_1G_LOW_BIT_UNUSUAL,PASS_7_0_0,PASS_15_15,Fast but recurring unusual low-bit rows. +fast8x_wide,FAIL_256M_LOW_BIT_WARNINGS,NOT_RUN,NOT_RUN,Very suspicious NS3 at 16 KiB and later suspicious FPF rows. +fast8x_batch,FAIL_256M_LOW_BIT_FPF,NOT_VALID_AFTER_CHANGE,NOT_VALID_AFTER_CHANGE,Reached 300+ MiB/s but failed low-bit FPF checks at 8 KiB. +fast4x512,WARN_256M_MULTIPLE_LOW_BIT,PASS_7_0_0,NOT_RUN_CURRENT,Fastest measured old path but warning density was too high. diff --git a/tests/ablation_lab/tables/fast8x_stream_smoke_bench.csv b/tests/ablation_lab/tables/fast8x_stream_smoke_bench.csv new file mode 100644 index 0000000..ca01af6 --- /dev/null +++ b/tests/ablation_lab/tables/fast8x_stream_smoke_bench.csv @@ -0,0 +1,3 @@ +implementation,bytes,elapsed_s,mib_per_s,notes +tricube_c_stream_baseline,67108864,0.875903,73.067,smoke run after adding fast8x +tricube_c_stream_fast8x,67108864,0.488772,130.940,smoke run after adding fast8x diff --git a/tests/ablation_lab/tables/quality_gate_summary.csv b/tests/ablation_lab/tables/quality_gate_summary.csv new file mode 100644 index 0000000..82e9177 --- /dev/null +++ b/tests/ablation_lab/tables/quality_gate_summary.csv @@ -0,0 +1,5 @@ +variant,stream_bytes,byte_entropy,bit_ones_fraction,low_bit_ones_fraction,serial_lag1,repeated_32_byte_blocks,cross_seed_32_byte_repeats,seed_avalanche_mean_bits,seed_avalanche_stdev_bits,adjacent_seed_mean_bits,adjacent_seed_stdev_bits +baseline,16777216,7.999989052829583,0.5000787377357483,0.5000360608100891,0.00016835837702638994,0,0,128.0625,8.286613828941228,127.3203125,8.583171756544532 +fast8x,16777216,7.999991344825514,0.49999289959669113,0.5001624822616577,0.00010167126630725942,0,0,127.90625,8.32394653619904,126.9296875,7.407959817138842 +fast8x_wide,16777216,7.9999895615618755,0.5000088140368462,0.5000000596046448,0.00007921642975222916,0,0,127.359375,8.177459850673374,129.0390625,7.040510394928321 +fast8x_batch,16777216,7.999989236907166,0.500104583799839,0.5000602602958679,0.0003176545895860339,0,0,129.0,7.599342076785332,127.5625,8.895003864529796 diff --git a/tests/ablation_lab/tables/stream_speed_variants.csv b/tests/ablation_lab/tables/stream_speed_variants.csv new file mode 100644 index 0000000..4e4f448 --- /dev/null +++ b/tests/ablation_lab/tables/stream_speed_variants.csv @@ -0,0 +1,7 @@ +implementation,bytes,elapsed_s,mib_per_s,public_status +tricube_c_stream_baseline,268435456,3.6678492500213906,69.79567112757076,default_baseline +tricube_c_stream_fast8x,268435456,1.9298185000079684,132.65496211117417,added_experimental_variant +tricube_c_stream_fast8x512,268435456,1.109450875024777,230.74478173202834,not_promoted_low_bit_watch +tricube_c_stream_fast8x_wide,268435456,0.8960235409904271,285.7067792181311,not_promoted_low_bit_warning +tricube_c_stream_fast8x_batch,268435456,0.8205709999892861,311.97787882260343,not_promoted_low_bit_failure +tricube_c_stream_fast4x512,268435456,0.6894365840125829,371.31769032338985,not_promoted_low_bit_warning_density diff --git a/tests/crypto_analysis/README.md b/tests/crypto_analysis/README.md new file mode 100644 index 0000000..19ff73a --- /dev/null +++ b/tests/crypto_analysis/README.md @@ -0,0 +1,410 @@ +# TriCube Crypto-Analysis Screens + +This folder contains reproducible development screens for TriCube stream +variants. They are named probes and screens because they are engineering gates +for obvious failures and warning patterns, not formal cryptanalysis. + +## Shared Test Model + +The screens use the C CLI as the system under test. If `c/build/tricube` is not +present, the helper layer builds it with `make -C c all`. Each black-box screen +then asks the CLI for deterministic stream bytes: + +```text +c/build/tricube stream --seed SEED --bytes N --out - [--variant fast8x] +``` + +For short-output screens, the tested function is: + +```text +F_variant(seed) = first 32 bytes of TriCube stream output for that 64-bit seed +``` + +The suite compares the released baseline stream path and the experimental +`fast8x` stream variant through that same interface. This gives a clean +side-by-side test of stream initialization, stream stepping, output extraction, +and variant domain separation. It does not test the hash API directly, and it +does not inspect bit-level internal ARX propagation except in the separate +white-box schedule model. + +The quick and standard profiles use the following budgets. + +| Parameter | Quick | Standard | +|---|---:|---:| +| Differential/rotational samples per case | 64 | 256 | +| Algebraic variables | 8 | 10 | +| Algebraic output bits | 32 | 64 | +| Collision samples | 512 | 4096 | +| Near-collision sampled pairs | 256 | 2048 | +| Stream bytes per variant for stream screens | 1 MiB | 16 MiB | +| Berlekamp-Massey bytes sampled per selected bit stream | 4096 | 16384 | + +The scripts use deterministic pseudorandom sampling from the command seed +(`--seed`, default `123`) so reviewers can rerun the same cases. + +## What Runs Here + +`run_all_screens.py` runs the compact suite and writes one combined +summary. Each screen is also a separate script so reviewers can inspect and run +one method at a time. + +## Commands + +Quick profile: + +```bash +python tests/crypto_analysis/run_all_screens.py \ + --profile quick \ + --variants baseline,fast8x \ + --out tests/crypto_analysis/results/quick-latest +``` + +Standard profile: + +```bash +python tests/crypto_analysis/run_all_screens.py \ + --profile standard \ + --variants baseline,fast8x \ + --out tests/crypto_analysis/results/standard-latest +``` + +## Screen Methods + +### Black-Box Differential Diffusion Probe + +File: `screens/differential_screen.py` + +This probe asks whether selected seed differences produce output differences +that look broadly diffused in the first 256 output bits. For each variant, +delta class, and sample, the script chooses a base seed, generates +`F(seed)`, generates `F(seed xor delta)`, and analyzes the xor difference of +the two 32-byte outputs. + +The fixed delta classes are: + +- single-bit masks at positions `0, 1, 7, 8, 31, 32, 63`; +- single-byte masks in byte positions `0, 1, 3, 7`; +- all-ones 64-bit word mask; +- low-bit-per-byte mask `0x0101010101010101`; +- checkerboard masks `0xAA55...` and `0x55AA...`; +- high-bit-per-byte mask `0x8080808080808080`; +- adjacent-bit masks near byte, word, and high-bit boundaries. + +The script also adds deterministic random low-weight, medium-weight, and +full-weight deltas. It records mean/min/max output Hamming distance, per-output +bit flip counts, maximum output-bit bias away from 0.5, a chi-square score for +bit flips, repeated output differences, and the top repeated-difference count. + +This stresses seed injection, stream initialization, per-block stream update, +and output extraction. The pass threshold expects the mean 256-bit output +difference to stay in `[112, 144]`, maximum bit bias to stay at or below +`0.25`, and no repeated difference pattern beyond the tested budget. A hard +failure is recorded for very low or high diffusion (`<96` or `>160` changed +bits on average) or repeated difference concentration. + +This is not trail-based differential cryptanalysis. It does not model how +differences propagate through modular addition, tetrahedral mixing, edge +coupling, shell lanes, or lane permutation. + +### Black-Box Rotational Relation Probe + +File: `screens/rotational_screen.py` + +This probe checks for obvious rotational symmetry between related seeds and +outputs. For each rotation in: + +```text +1, 2, 3, 4, 5, 7, 8, 13, 16, 17, 31, 32, 33, 47, 63 +``` + +the script generates `F(seed)`, rotates the 64-bit seed left by that amount, +generates `F(rotl64(seed, r))`, rotates each 64-bit output word of `F(seed)` by +the same amount, and compares that rotated output to the related-seed output. + +The metrics are exact rotational relation count, mean/min/max rotational +distance, maximum output-bit bias in the relation difference, and a z-score for +the mean distance around the 128-bit expectation. The screen fails if any exact +rotational relation appears. It warns when mean distance leaves `[112, 144]` or +maximum bit bias exceeds `0.25`. + +This stresses whether the seed-to-stream map preserves simple word rotations +through the output path. It does not prove resistance to rotational +distinguishers, and it does not model rotational trails inside the ARX round +function. + +### Small Black-Box Algebraic Degree Screen + +File: `screens/algebraic_degree_screen.py` + +This screen estimates whether small slices of the seed-to-output map have +obviously low Boolean degree. It selects seed bit positions: + +```text +position_i = (13 * i) mod 64 +``` + +for `i = 0 .. variables-1`. It enumerates all `2^variables` assignments, +xors the selected seed bits into the base seed, evaluates `F(seed)`, and builds +truth tables for the requested output bits. It then applies a Möbius transform +to each truth table and reports the algebraic normal form (ANF) degree. + +The quick profile uses 8 variables and 32 output bits. The standard profile +uses 10 variables and 64 output bits. The script refuses more than 12 variables +because this black-box method is exponential. It reports min/mean/max degree +and counts output bits whose degree is at least two below the variable count. +A warning is recorded if any sampled output bit is low-degree or if the maximum +degree is below `variables - 1`. + +This stresses whether selected seed bits reach sampled output bits through a +high-degree-looking black-box map. It is not full algebraic cryptanalysis. It +does not construct the Boolean equations of the round function, search +invariants, run SAT/SMT/MILP, or compute useful full-state ANF degree. + +### Collision, Birthday, and Near-Collision Screens + +File: `screens/collision_screen.py` + +This script samples `F(seed)` for deterministic random 64-bit seeds. It first +counts exact repeated 256-bit outputs. It then truncates those outputs to +prefix lengths: + +```text +16, 24, 32, 40, 48, 64 bits +``` + +and compares observed prefix collision pairs with the birthday expectation: + +```text +samples * (samples - 1) / (2 * 2^prefix_bits) +``` + +For prefix tests with expected count at least 10, the screen warns when the +observed count is more than `max(5 sigma, 10)` from expectation. For very small +expected counts, it warns only on large excess collision counts. The same +script also samples output pairs and reports min/mean/max Hamming distance as a +near-collision sanity check; it warns if the minimum sampled distance drops +below 80 bits. + +This stresses gross collision defects, bad prefix distribution, and unusually +close sampled output pairs. The sample sizes are intentionally practical and +are far below cryptographic collision-resistance validation. + +### Overlap/Fork Stream Uniqueness Screen + +File: `screens/overlap_fork_screen.py` + +This screen checks whether related stream seeds produce repeated blocks within +one stream or overlapping blocks across streams. For each variant, it generates +streams for: + +```text +seed +seed + 1 +seed xor 1 +seed xor 2^63 +``` + +It tests block sizes of 16, 32, and 64 bytes. For each block size it reports: + +- repeated blocks within each stream; +- blocks that appear in more than one stream; +- same-position equality between `seed` and `seed + 1`; +- Hamming distance between the first 1024 bytes of adjacent-seed streams. + +Any repeated, overlapping, or same-position equal block is a failure. This +stresses seed separation, fork behavior, and obvious stream overlap. It does +not prove stream independence. + +### Black-Box State-Recovery/Predictability Screen + +File: `screens/state_recovery_screen.py` + +This screen treats stream output as a sequence to be predicted without internal +state access. It generates one stream, splits it into train and test halves, and +evaluates deliberately simple predictors: + +- most-common-byte predictor from the training half; +- one-byte-context next-byte predictor from training transitions; +- global majority-bit predictor; +- Berlekamp-Massey binary linear complexity for bit positions `0`, `1`, and + `7` over the first `--bm-bits` bytes. + +The `--bm-bits` flag name is historical; the current implementation slices +that many bytes from the stream and then extracts one bit position from each +byte. + +The reported baselines include the random next-byte rate `1/256` and expected +bit accuracy near `0.5`. The screen warns if next-byte or one-byte-context +accuracy exceeds `0.02`, or if bit accuracy differs from `0.5` by more than +`0.02`. + +This stresses obvious black-box predictability and low-complexity bit streams. +It is not a state-recovery attack. A real state-recovery attack would model the +internal state transition or output function. + +### Low-Bit Diagnostic Screen + +File: `screens/low_bit_diagnostics.py` + +This screen exists because low-bit warnings have been the most important +external-battery concern. It generates stream bytes and separately measures: + +- lowest bit of every byte; +- lowest bit of every 32-bit little-endian word; +- lowest bit of every 64-bit little-endian word; +- low-nibble distribution over 16 values; +- bit-position counts across 64-bit output words; +- low-bit transition counts `00`, `01`, `10`, `11`; +- lag-1 correlation of byte low bits; +- lag-1 correlation of byte high bits for comparison. + +The screen uses z-scores for one-count balance, chi-square for low nibbles, and +Pearson lag-1 correlation for bit transitions. It warns when the maximum +balance z-score exceeds `6.0`, low-nibble chi-square exceeds `45.0`, or +absolute low-bit lag-1 correlation exceeds `0.01`. + +This stresses exactly the part of the output where PractRand warnings appeared. +It is weaker than PractRand and can miss patterns that PractRand catches, but +it gives a fast local diagnostic while developing variants. + +### White-Box Word-Dependency Model + +Files: + +- `screens/whitebox_round_model.py` +- `models/tricube_schedule.py` + +This is the one screen that is not purely black-box. It imports a TriCube +schedule model that generates the 27 vertex lanes, 5 shell lanes, 8 cube cells, +48 tetrahedra, 54 positive-axis grid edges, shell targets, and 32-lane +permutation. The model validates those counts before analysis. + +The dependency model starts with each state lane depending only on itself. For +each modeled round it applies the same schedule shape as the specification: + +1. each tetrahedron merges the dependency sets of its four lanes; +2. each grid edge merges the dependency sets of its two endpoint lanes; +3. each shell lane merges with a scheduled vertex lane, and a scheduled + opposite vertex receives that shell/vertex dependency; +4. the lane permutation moves dependency sets to their destination lanes. + +The output model then checks the lanes read for the first four output words +(the first 32 output bytes). The report records per-round minimum, mean, and +maximum dependency size, how many lanes depend on all 32 initial lanes, and +when the first 32 output bytes reach full word-lane dependency. + +This screen tests schedule reachability and coverage: whether every lane can +influence every other lane at 64-bit word granularity under the current +tetrahedron/edge/shell/permutation schedule. It does not model bit-level ARX +probabilities, modular-addition carries, rotational trails, algebraic degree, +or attack cost. + +### External Battery Availability Screen + +File: `screens/external_batteries.py` + +The all-in-one quick run does not launch long external batteries. Instead, this +screen checks whether expected binaries are on `PATH`: + +- PractRand `RNG_test`; +- SmokeRand `smokerand`; +- TestU01 stdin adapter `testu01_stdin32`; +- Dieharder `dieharder`; +- NIST STS `assess`. + +If a tool is detected, the row is `NOT_RUN` because the quick suite did not +launch it. If the tool is absent, the row is `BLOCKED`. This avoids confusing a +missing local battery install with a statistical pass or failure. + +## Individual Commands + +The all-in-one runner calls the same screen modules listed below. Reviewers can +run any screen by itself when they want to inspect one method, one output table, +or one threshold policy. + +| Screen | Command | +|---|---| +| Differential diffusion | `python tests/crypto_analysis/screens/differential_screen.py --variants baseline,fast8x --samples 64 --out tests/crypto_analysis/results/differential-latest` | +| Rotational relation | `python tests/crypto_analysis/screens/rotational_screen.py --variants baseline,fast8x --samples 64 --out tests/crypto_analysis/results/rotational-latest` | +| Algebraic degree | `python tests/crypto_analysis/screens/algebraic_degree_screen.py --variants baseline,fast8x --variables 8 --output-bits 32 --out tests/crypto_analysis/results/algebraic-latest` | +| Collision and birthday | `python tests/crypto_analysis/screens/collision_screen.py --variants baseline,fast8x --samples 512 --near-pairs 256 --out tests/crypto_analysis/results/collision-latest` | +| Overlap/fork uniqueness | `python tests/crypto_analysis/screens/overlap_fork_screen.py --variants baseline,fast8x --bytes 1048576 --out tests/crypto_analysis/results/overlap-latest` | +| State-recovery/predictability | `python tests/crypto_analysis/screens/state_recovery_screen.py --variants baseline,fast8x --bytes 1048576 --bm-bits 4096 --out tests/crypto_analysis/results/state-recovery-latest` | +| Low-bit diagnostics | `python tests/crypto_analysis/screens/low_bit_diagnostics.py --variants baseline,fast8x --bytes 16777216 --out tests/crypto_analysis/results/low-bit-latest` | +| White-box word-dependency model | `python tests/crypto_analysis/screens/whitebox_round_model.py --rounds 24 --out tests/crypto_analysis/results/whitebox-latest` | +| External battery availability | `python tests/crypto_analysis/screens/external_batteries.py --variants baseline,fast8x --out tests/crypto_analysis/results/external-latest` | + +Low-bit diagnostics at 16 MiB per variant: + +```bash +python tests/crypto_analysis/screens/low_bit_diagnostics.py \ + --variants baseline,fast8x \ + --bytes 16777216 \ + --out tests/crypto_analysis/results/low-bit-latest +``` + +White-box word-dependency model: + +```bash +python tests/crypto_analysis/screens/whitebox_round_model.py \ + --rounds 24 \ + --out tests/crypto_analysis/results/whitebox-latest +``` + +Optional tool availability: + +```bash +python tests/crypto_analysis/tooling/check_tools.py +``` + +## Output Format + +Each run writes: + +- `summary.json` with metadata, exact command, branch, commit, machine, Python + version, profile, seed, variants, and status counts; +- `summary.md` with compact interpreted tables; +- one CSV and one Markdown summary for each screen; +- `all_screens.csv` with every row in one compact table. + +Large raw streams and external battery logs are not stored here. External tools +should write their summaries under `results/` or another documented results +location. + +## Status Labels + +- `PASS`: no issue detected under this test budget. +- `WARN`: suspicious pattern or threshold breach that is not conclusive. +- `FAIL`: clear failure under the screen criteria. +- `BLOCKED`: missing tool, missing binary, or harness/runtime blocker. +- `NOT_RUN`: available test was not attempted in this run. + +Do not treat `PASS` as a security claim. Do not treat EOF, SIGPIPE, timeout, or +input exhaustion as a statistical pass. + +## Limits + +The current screens are black-box tests over output bytes. They do not model +TriCube's tetrahedral ARX layer, modular-addition difference propagation, lane +schedules, constants, or output extraction symbolically. Formal work still +requires differential trail search, rotational propagation analysis, +SAT/SMT/MILP or Gröbner-style algebraic modeling, reduced-round attack +experiments, and independent review. + +The white-box round model is a narrow exception: it does inspect the specified +lane schedule, but only at word-dependency granularity. It is useful for +checking coverage and schedule reachability. It is still not differential, +rotational, algebraic, or state-recovery cryptanalysis. + +## Tooling and Models + +The reusable TriCube schedule model is in `models/tricube_schedule.py`. It +generates the lane mapping, tetrahedra, edges, shell schedule, and lane +permutation used by the current specification. Solver-specific work should +build on that model rather than duplicating schedule rules in separate +scripts. + +The `tooling/` folder contains optional setup checks for Z3, SageMath, +CryptoMiniSat, PractRand, Dieharder, TestU01 adapters, SmokeRand, and NIST STS. +Those checks do not vendor or run full external tools; they only report whether +the local machine is ready for deeper analysis. diff --git a/tests/crypto_analysis/common.py b/tests/crypto_analysis/common.py new file mode 100644 index 0000000..ec2ba3e --- /dev/null +++ b/tests/crypto_analysis/common.py @@ -0,0 +1,278 @@ +"""Shared helpers for TriCube black-box development screens. + +These helpers intentionally treat TriCube as a black box. They are useful for +finding obvious warning patterns in stream output. They are not formal +cryptanalysis and do not model the internal round function. +""" + +from __future__ import annotations + +from collections import Counter, defaultdict +from dataclasses import dataclass +from datetime import datetime, timezone +import csv +import json +import math +import os +from pathlib import Path +import platform +import random +import shutil +import subprocess +import sys +from functools import lru_cache +from typing import Any + + +STATUS_PASS = "PASS" +STATUS_WARN = "WARN" +STATUS_FAIL = "FAIL" +STATUS_BLOCKED = "BLOCKED" +STATUS_NOT_RUN = "NOT_RUN" + +REPO_ROOT = Path(__file__).resolve().parents[2] +C_CLI = REPO_ROOT / "c" / "build" / "tricube" + + +@dataclass(frozen=True) +class Variant: + name: str + cli_name: str + + +VARIANTS = { + "baseline": Variant("baseline", "baseline"), + "fast8x": Variant("fast8x", "fast8x"), +} + + +def run_text(args: list[str], *, cwd: Path = REPO_ROOT, timeout: int = 30) -> str: + try: + return subprocess.check_output(args, cwd=cwd, text=True, stderr=subprocess.DEVNULL, timeout=timeout).strip() + except Exception: + return "" + + +def git_branch() -> str: + return run_text(["git", "rev-parse", "--abbrev-ref", "HEAD"]) or "UNKNOWN" + + +def git_commit() -> str: + return run_text(["git", "rev-parse", "HEAD"]) or "UNKNOWN" + + +def environment(command: list[str] | None = None) -> dict[str, Any]: + return { + "date_utc": datetime.now(timezone.utc).isoformat(), + "git_branch": git_branch(), + "git_commit": git_commit(), + "machine": platform.node(), + "platform": platform.platform(), + "processor": platform.processor(), + "python_version": sys.version.replace("\n", " "), + "command": command or sys.argv, + } + + +def ensure_cli() -> None: + if C_CLI.exists(): + return + subprocess.check_call(["make", "-C", "c", "all"], cwd=REPO_ROOT) + + +@lru_cache(maxsize=8192) +def stream_bytes(variant: str, seed: int, n_bytes: int) -> bytes: + ensure_cli() + if variant not in VARIANTS: + raise ValueError(f"unknown variant: {variant}") + cmd = [ + str(C_CLI), + "stream", + "--seed", + str(seed & 0xFFFFFFFFFFFFFFFF), + "--bytes", + str(n_bytes), + "--out", + "-", + ] + if variant != "baseline": + cmd.extend(["--variant", VARIANTS[variant].cli_name]) + return subprocess.check_output(cmd, cwd=REPO_ROOT) + + +def digest32(variant: str, seed: int) -> bytes: + """Return the first 32 bytes of deterministic stream output. + + This is a stream-output screen, not a hash-mode screen. It exists so the + baseline and fast8x stream variants can be compared under the same black-box + input/output interface. + """ + + return stream_bytes(variant, seed, 32) + + +def hamming_bytes(a: bytes, b: bytes) -> int: + return sum((x ^ y).bit_count() for x, y in zip(a, b)) + + +def xor_bytes(a: bytes, b: bytes) -> bytes: + return bytes(x ^ y for x, y in zip(a, b)) + + +def rotl64(x: int, r: int) -> int: + r &= 63 + return ((x << r) | (x >> (64 - r))) & 0xFFFFFFFFFFFFFFFF + + +def rotate_words64(data: bytes, r: int) -> bytes: + out = bytearray() + for off in range(0, len(data), 8): + word = int.from_bytes(data[off : off + 8].ljust(8, b"\0"), "little") + out.extend(rotl64(word, r).to_bytes(8, "little")) + return bytes(out[: len(data)]) + + +def bytes_to_bits(data: bytes) -> list[int]: + return [(byte >> bit) & 1 for byte in data for bit in range(8)] + + +def berlekamp_massey_binary(bits: list[int]) -> int: + """Return binary linear complexity using the Berlekamp-Massey algorithm.""" + + n = len(bits) + c = [0] * n + b = [0] * n + c[0] = 1 + b[0] = 1 + length = 0 + m = -1 + for idx in range(n): + discrepancy = bits[idx] + for j in range(1, length + 1): + discrepancy ^= c[j] & bits[idx - j] + if discrepancy: + t = c[:] + shift = idx - m + for j in range(0, n - shift): + c[j + shift] ^= b[j] + if 2 * length <= idx: + length = idx + 1 - length + m = idx + b = t + return length + + +def anf_degree(truth: list[int]) -> int: + """Compute ANF degree from a truth table whose length is a power of two.""" + + coeff = truth[:] + n = len(coeff).bit_length() - 1 + for i in range(n): + bit = 1 << i + for mask in range(len(coeff)): + if mask & bit: + coeff[mask] ^= coeff[mask ^ bit] + degree = 0 + for mask, value in enumerate(coeff): + if value: + degree = max(degree, mask.bit_count()) + return degree + + +def mean(values: list[float]) -> float: + return sum(values) / len(values) if values else 0.0 + + +def stdev(values: list[float]) -> float: + if len(values) < 2: + return 0.0 + mu = mean(values) + return math.sqrt(sum((x - mu) ** 2 for x in values) / (len(values) - 1)) + + +def choose2(n: int) -> int: + return n * (n - 1) // 2 + + +def write_json(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def write_csv(path: Path, rows: list[dict[str, Any]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + if not rows: + path.write_text("", encoding="utf-8") + return + fields: list[str] = [] + seen = set() + for row in rows: + for key in row: + if key not in seen: + fields.append(key) + seen.add(key) + with path.open("w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + + +def markdown_table(rows: list[dict[str, Any]], fields: list[str] | None = None) -> str: + if not rows: + return "_No rows._\n" + fields = fields or list(rows[0].keys()) + lines = ["| " + " | ".join(fields) + " |", "| " + " | ".join(["---"] * len(fields)) + " |"] + for row in rows: + lines.append("| " + " | ".join(str(row.get(f, "")) for f in fields) + " |") + return "\n".join(lines) + "\n" + + +def write_summary_md(path: Path, title: str, intro: str, rows: list[dict[str, Any]], fields: list[str] | None = None) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + text = f"# {title}\n\n{intro.strip()}\n\n{markdown_table(rows, fields)}" + path.write_text(text, encoding="utf-8") + + +def parse_variants(value: str) -> list[str]: + variants = [v.strip() for v in value.split(",") if v.strip()] + for variant in variants: + if variant not in VARIANTS: + raise ValueError(f"unknown variant {variant!r}; expected one of {sorted(VARIANTS)}") + return variants + + +def rng_for(seed: int) -> random.Random: + return random.Random(seed & 0xFFFFFFFFFFFFFFFF) + + +def command_exists(name: str) -> bool: + return shutil.which(name) is not None + + +def count_repeated_blocks(data: bytes, block_size: int) -> int: + counts = Counter(data[i : i + block_size] for i in range(0, len(data) - block_size + 1, block_size)) + return sum(count - 1 for count in counts.values() if count > 1) + + +def bit_position_counts64(data: bytes) -> list[int]: + counts = [0] * 64 + words = len(data) // 8 + for i in range(words): + word = int.from_bytes(data[i * 8 : i * 8 + 8], "little") + for bit in range(64): + counts[bit] += (word >> bit) & 1 + return counts + + +def zscore_ones(ones: int, n: int) -> float: + if n <= 0: + return 0.0 + return (ones - n / 2.0) / math.sqrt(n / 4.0) + + +def group_by_status(rows: list[dict[str, Any]]) -> dict[str, int]: + counts: dict[str, int] = defaultdict(int) + for row in rows: + counts[str(row.get("status", "UNKNOWN"))] += 1 + return dict(sorted(counts.items())) + diff --git a/tests/crypto_analysis/models/__init__.py b/tests/crypto_analysis/models/__init__.py new file mode 100644 index 0000000..8d497a1 --- /dev/null +++ b/tests/crypto_analysis/models/__init__.py @@ -0,0 +1 @@ +"""TriCube-specific modeling helpers for analysis scripts.""" diff --git a/tests/crypto_analysis/models/tricube_schedule.py b/tests/crypto_analysis/models/tricube_schedule.py new file mode 100644 index 0000000..3355fd7 --- /dev/null +++ b/tests/crypto_analysis/models/tricube_schedule.py @@ -0,0 +1,189 @@ +"""TriCube lane schedule model used by analysis scripts. + +This module describes the public TriCube lane topology and schedule at the +word-lane level. It does not model bit-level ARX probabilities, SAT/SMT +constraints, algebraic degree, or attack cost. The purpose is to provide one +tested place where later solver models can get the same lane mapping, +tetrahedra, edge set, shell schedule, and permutation used by the +specification. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +STATE_WORDS = 32 +VERTEX_LANES = 27 +SHELL_LANES = tuple(range(27, 32)) +SCHEDULE_PERIOD = 24 +LOCAL_TETRAHEDRA = ( + (0, 1, 3, 7), + (0, 3, 2, 7), + (0, 2, 6, 7), + (0, 6, 4, 7), + (0, 4, 5, 7), + (0, 5, 1, 7), +) +TETRA_ROTATION_SETS = ( + (17, 29, 41, 53), + (23, 31, 47, 59), + (19, 37, 43, 61), + (13, 27, 39, 55), +) + + +@dataclass(frozen=True) +class ScheduleCounts: + vertex_lanes: int + shell_lanes: int + cube_cells: int + tetrahedra: int + edges: int + permutation_lanes: int + + +def vindex3(x: int, y: int, z: int) -> int: + """Return the vertex lane for coordinate ``(x, y, z)`` in the 3x3x3 grid.""" + + if not (0 <= x < 3 and 0 <= y < 3 and 0 <= z < 3): + raise ValueError("vertex coordinates must be in 0..2") + return x + 3 * (y + 3 * z) + + +def vertex_coordinates() -> list[tuple[int, int, int, int]]: + """Return ``(lane, x, y, z)`` rows for the 27 vertex lanes.""" + + rows: list[tuple[int, int, int, int]] = [] + for z in range(3): + for y in range(3): + for x in range(3): + rows.append((vindex3(x, y, z), x, y, z)) + return rows + + +def cube_vertices(cx: int, cy: int, cz: int) -> tuple[int, int, int, int, int, int, int, int]: + """Return the eight vertex lanes for cube cell ``(cx, cy, cz)``.""" + + if not (0 <= cx < 2 and 0 <= cy < 2 and 0 <= cz < 2): + raise ValueError("cube coordinates must be in 0..1") + return ( + vindex3(cx, cy, cz), + vindex3(cx + 1, cy, cz), + vindex3(cx, cy + 1, cz), + vindex3(cx + 1, cy + 1, cz), + vindex3(cx, cy, cz + 1), + vindex3(cx + 1, cy, cz + 1), + vindex3(cx, cy + 1, cz + 1), + vindex3(cx + 1, cy + 1, cz + 1), + ) + + +def cube_cells() -> list[tuple[int, int, int]]: + """Return cube cells in the normative iteration order.""" + + return [(cx, cy, cz) for cz in range(2) for cy in range(2) for cx in range(2)] + + +def build_tetrahedra() -> list[tuple[int, int, int, int]]: + """Return the 48 global tetrahedral neighborhoods.""" + + out: list[tuple[int, int, int, int]] = [] + for cx, cy, cz in cube_cells(): + verts = cube_vertices(cx, cy, cz) + out.extend(tuple(verts[i] for i in tet) for tet in LOCAL_TETRAHEDRA) + return out + + +def build_edges() -> list[tuple[int, int]]: + """Return all positive-axis grid-adjacent vertex edges.""" + + out: list[tuple[int, int]] = [] + for z in range(3): + for y in range(3): + for x in range(3): + i = vindex3(x, y, z) + if x + 1 < 3: + out.append((i, vindex3(x + 1, y, z))) + if y + 1 < 3: + out.append((i, vindex3(x, y + 1, z))) + if z + 1 < 3: + out.append((i, vindex3(x, y, z + 1))) + return out + + +def oriented_tetrahedron( + tet: tuple[int, int, int, int], round_index: int, tetrahedron_index: int +) -> tuple[int, int, int, int]: + """Return the oriented tetrahedron used in one scheduled round.""" + + a, b, c, d = tet + if (round_index + tetrahedron_index) & 1: + b, d = d, b + if (round_index + tetrahedron_index) & 2: + a, c = c, a + return a, b, c, d + + +def tetra_rotation_set(round_index: int, tetrahedron_index: int) -> tuple[int, int, int, int]: + """Return the four tetrahedral rotation constants for this schedule point.""" + + return TETRA_ROTATION_SETS[(round_index + tetrahedron_index) & 3] + + +def edge_rotations(round_index: int, edge_index: int) -> tuple[int, int]: + """Return the two edge-coupling rotation counts for this schedule point.""" + + sr = round_index % SCHEDULE_PERIOD + return ((edge_index + sr * 3) % 61 + 1, (edge_index * 7 + sr) % 61 + 1) + + +def shell_targets(round_index: int) -> list[tuple[int, int, int]]: + """Return ``(shell_lane, vertex_lane, opposite_lane)`` for one round.""" + + sr = round_index % SCHEDULE_PERIOD + rows: list[tuple[int, int, int]] = [] + for j, lane in enumerate(SHELL_LANES): + vertex = (sr * 5 + j * 7) % VERTEX_LANES + opposite = (vertex * 11 + 3) % VERTEX_LANES + rows.append((lane, vertex, opposite)) + return rows + + +def lane_permutation() -> list[int]: + """Return the source lane for each destination lane after permutation.""" + + return [(9 * i + 5) & 31 for i in range(STATE_WORDS)] + + +def permutation_rotation(round_index: int, lane_index: int) -> int: + """Return the constant-injection rotation for one lane after permutation.""" + + return ((5 * lane_index + (round_index % SCHEDULE_PERIOD)) % 61) + 1 + + +def schedule_counts() -> ScheduleCounts: + """Return counts that should remain stable for the current public baseline.""" + + return ScheduleCounts( + vertex_lanes=VERTEX_LANES, + shell_lanes=len(SHELL_LANES), + cube_cells=len(cube_cells()), + tetrahedra=len(build_tetrahedra()), + edges=len(build_edges()), + permutation_lanes=len(lane_permutation()), + ) + + +def validate_schedule() -> None: + """Raise ``AssertionError`` if the public baseline schedule shape changes.""" + + counts = schedule_counts() + assert counts.vertex_lanes == 27 + assert counts.shell_lanes == 5 + assert counts.cube_cells == 8 + assert counts.tetrahedra == 48 + assert counts.edges == 54 + assert counts.permutation_lanes == 32 + assert sorted(lane_permutation()) == list(range(STATE_WORDS)) + diff --git a/tests/crypto_analysis/run_all_screens.py b/tests/crypto_analysis/run_all_screens.py new file mode 100644 index 0000000..cf80e81 --- /dev/null +++ b/tests/crypto_analysis/run_all_screens.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Run all TriCube crypto-analysis development screens. + +The individual screens live in ``tests/crypto_analysis/screens/`` and can be +run separately for review. This script is only the orchestration layer that +uses the same screen implementations and writes one combined summary. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import sys +from typing import Any + +SCREEN_DIR = Path(__file__).resolve().parent / "screens" +if str(SCREEN_DIR) not in sys.path: + sys.path.insert(0, str(SCREEN_DIR)) + +from common import STATUS_FAIL, environment, group_by_status, markdown_table, parse_variants, write_csv, write_json, write_summary_md +from screens.algebraic_degree_screen import run_screen as run_algebraic_degree_screen +from screens.collision_screen import run_screen as run_collision_screen +from screens.config import PROFILE_DEFAULTS +from screens.differential_screen import run_screen as run_differential_screen +from screens.external_batteries import run_screen as run_external_batteries +from screens.low_bit_diagnostics import run_screen as run_low_bit_diagnostics +from screens.overlap_fork_screen import run_screen as run_overlap_fork_screen +from screens.rotational_screen import run_screen as run_rotational_screen +from screens.state_recovery_screen import run_screen as run_state_recovery_screen + + +def write_outputs(out_dir: Path, rows_by_name: dict[str, list[dict[str, Any]]], metadata: dict[str, Any]) -> None: + out_dir.mkdir(parents=True, exist_ok=True) + all_rows = [row for rows in rows_by_name.values() for row in rows] + write_json(out_dir / "summary.json", {"metadata": metadata, "status_counts": group_by_status(all_rows), "screens": rows_by_name}) + for name, rows in rows_by_name.items(): + write_csv(out_dir / f"{name}.csv", rows) + write_summary_md( + out_dir / f"{name}.md", + title=name.replace("_", " ").title(), + intro=( + "Generated by TriCube development screens. PASS means no issue " + "was detected under this budget; it is not a security proof." + ), + rows=rows, + ) + write_csv(out_dir / "all_screens.csv", all_rows) + md = [ + "# TriCube Crypto-Analysis Screen Summary", + "", + "These tests are development gates. They can find obvious failures or warning patterns, but they do not replace white-box cryptanalysis.", + "", + "## Metadata", + "", + markdown_table([{k: v for k, v in metadata.items() if k != "command"}]), + "## Status Counts", + "", + markdown_table([group_by_status(all_rows)]), + ] + for name, rows in rows_by_name.items(): + md.extend([f"## {name.replace('_', ' ').title()}", "", markdown_table(rows)]) + (out_dir / "summary.md").write_text("\n".join(md), encoding="utf-8") + + +def run(profile: str, variants: list[str], out_dir: Path, seed: int) -> dict[str, list[dict[str, Any]]]: + defaults = PROFILE_DEFAULTS[profile] + rows_by_name = { + "differential_screen": run_differential_screen(variants, defaults["samples"], seed), + "rotational_screen": run_rotational_screen(variants, defaults["samples"], seed), + "algebraic_degree_screen": run_algebraic_degree_screen( + variants, defaults["algebraic_vars"], defaults["algebraic_bits"], seed + ), + "collision_screen": run_collision_screen(variants, defaults["collision_samples"], defaults["near_pairs"], seed), + "overlap_fork_screen": run_overlap_fork_screen(variants, defaults["stream_bytes"], seed), + "state_recovery_screen": run_state_recovery_screen(variants, defaults["stream_bytes"], defaults["bm_bits"], seed), + "low_bit_diagnostics": run_low_bit_diagnostics(variants, defaults["stream_bytes"], seed), + "external_batteries": run_external_batteries(variants), + } + metadata = environment() + metadata.update({"profile": profile, "variants": variants, "seed": seed, **defaults}) + write_outputs(out_dir, rows_by_name, metadata) + return rows_by_name + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Run all TriCube crypto-analysis development screens.") + parser.add_argument("--profile", choices=sorted(PROFILE_DEFAULTS), default="quick") + parser.add_argument("--variants", default="baseline,fast8x") + parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args(argv) + + variants = parse_variants(args.variants) + rows = run(args.profile, variants, args.out, args.seed) + counts = group_by_status([row for screen_rows in rows.values() for row in screen_rows]) + print(f"wrote {args.out}") + print(counts) + return 0 if counts.get(STATUS_FAIL, 0) == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/crypto_analysis/screens/__init__.py b/tests/crypto_analysis/screens/__init__.py new file mode 100644 index 0000000..b34687a --- /dev/null +++ b/tests/crypto_analysis/screens/__init__.py @@ -0,0 +1,2 @@ +"""Individually runnable TriCube development screens.""" + diff --git a/tests/crypto_analysis/screens/_bootstrap.py b/tests/crypto_analysis/screens/_bootstrap.py new file mode 100644 index 0000000..e64e265 --- /dev/null +++ b/tests/crypto_analysis/screens/_bootstrap.py @@ -0,0 +1,16 @@ +"""Import-path setup for directly executed screen scripts.""" + +from __future__ import annotations + +from pathlib import Path +import sys + + +CRYPTO_ANALYSIS_DIR = Path(__file__).resolve().parents[1] + + +def ensure_import_path() -> None: + path = str(CRYPTO_ANALYSIS_DIR) + if path not in sys.path: + sys.path.insert(0, path) + diff --git a/tests/crypto_analysis/screens/_output.py b/tests/crypto_analysis/screens/_output.py new file mode 100644 index 0000000..4c311a8 --- /dev/null +++ b/tests/crypto_analysis/screens/_output.py @@ -0,0 +1,42 @@ +"""Output helpers for individual TriCube screen scripts.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import environment, group_by_status, write_csv, write_json, write_summary_md # noqa: E402 + + +def write_single_screen(out_dir: Path, name: str, rows: list[dict[str, Any]], metadata: dict[str, Any]) -> None: + out_dir.mkdir(parents=True, exist_ok=True) + write_json(out_dir / "summary.json", {"metadata": metadata, "status_counts": group_by_status(rows), "rows": rows}) + write_csv(out_dir / f"{name}.csv", rows) + intro = ( + "Generated by one TriCube development screen. PASS means no issue " + "was detected under this budget; it is not a security proof." + ) + write_summary_md( + out_dir / f"{name}.md", + title=name.replace("_", " ").title(), + intro=intro, + rows=rows, + ) + write_summary_md( + out_dir / "summary.md", + title=name.replace("_", " ").title(), + intro=intro, + rows=rows, + ) + print(f"wrote {out_dir}") + print(group_by_status(rows)) + + +def screen_metadata(**values: Any) -> dict[str, Any]: + metadata = environment() + metadata.update(values) + return metadata diff --git a/tests/crypto_analysis/screens/algebraic_degree_screen.py b/tests/crypto_analysis/screens/algebraic_degree_screen.py new file mode 100644 index 0000000..eca340d --- /dev/null +++ b/tests/crypto_analysis/screens/algebraic_degree_screen.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Small black-box algebraic degree screen for TriCube stream variants.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import STATUS_PASS, STATUS_WARN, anf_degree, digest32, mean, parse_variants # noqa: E402 +from _output import screen_metadata, write_single_screen # noqa: E402 + + +def run_screen(variants: list[str], variable_count: int, output_bits: int, seed: int) -> list[dict[str, Any]]: + if variable_count > 12: + raise ValueError("variable_count above 12 is intentionally blocked for this black-box screen") + rows: list[dict[str, Any]] = [] + positions = [((i * 13) % 64) for i in range(variable_count)] + assignments = 1 << variable_count + for variant in variants: + outputs = [] + for mask in range(assignments): + s = seed + for i, pos in enumerate(positions): + if (mask >> i) & 1: + s ^= 1 << pos + outputs.append(digest32(variant, s)) + degrees: list[int] = [] + for bit_idx in range(output_bits): + table = [] + byte_idx = bit_idx // 8 + bit_in_byte = bit_idx % 8 + for out in outputs: + table.append((out[byte_idx] >> bit_in_byte) & 1) + degrees.append(anf_degree(table)) + low = sum(1 for d in degrees if d <= max(0, variable_count - 2)) + max_degree = max(degrees) if degrees else 0 + status = STATUS_WARN if low > 0 or max_degree < variable_count - 1 else STATUS_PASS + rows.append( + { + "screen": "small black-box algebraic degree screen", + "variant": variant, + "variables": variable_count, + "assignments": assignments, + "output_bits": output_bits, + "variable_positions": ",".join(str(p) for p in positions), + "min_degree": min(degrees) if degrees else 0, + "mean_degree": round(mean([float(x) for x in degrees]), 4), + "max_degree": max_degree, + "low_degree_outputs": low, + "status": status, + } + ) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variants", default="baseline,fast8x") + parser.add_argument("--variables", type=int, default=8) + parser.add_argument("--output-bits", type=int, default=32) + parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + variants = parse_variants(args.variants) + rows = run_screen(variants, args.variables, args.output_bits, args.seed) + write_single_screen( + args.out, + "algebraic_degree_screen", + rows, + screen_metadata(variants=variants, variables=args.variables, output_bits=args.output_bits, seed=args.seed), + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tests/crypto_analysis/screens/collision_screen.py b/tests/crypto_analysis/screens/collision_screen.py new file mode 100644 index 0000000..04b0438 --- /dev/null +++ b/tests/crypto_analysis/screens/collision_screen.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Collision, birthday, and near-collision sanity checks for TriCube.""" + +from __future__ import annotations + +import argparse +from collections import Counter +import math +from pathlib import Path +from typing import Any + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import STATUS_FAIL, STATUS_PASS, STATUS_WARN, choose2, digest32, hamming_bytes, mean, parse_variants, rng_for # noqa: E402 +from config import PREFIX_BITS # noqa: E402 +from _output import screen_metadata, write_single_screen # noqa: E402 + + +def run_screen(variants: list[str], samples: int, near_pairs: int, seed: int) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + rng = rng_for(seed ^ 0xC0111510) + for variant in variants: + outputs = [digest32(variant, rng.getrandbits(64)) for _ in range(samples)] + full_counts = Counter(outputs) + full_collisions = sum(choose2(c) for c in full_counts.values() if c > 1) + rows.append( + { + "screen": "collision/birthday sanity check", + "variant": variant, + "kind": "full_digest", + "samples": samples, + "prefix_bits": 256, + "observed_collision_pairs": full_collisions, + "expected_collision_pairs": "near 0", + "observed_expected_ratio": "", + "status": STATUS_FAIL if full_collisions else STATUS_PASS, + } + ) + for bits in PREFIX_BITS: + prefix_bytes = (bits + 7) // 8 + shift = prefix_bytes * 8 - bits + prefixes = [] + for out in outputs: + value = int.from_bytes(out[:prefix_bytes], "big") + prefixes.append(value >> shift if shift else value) + counts = Counter(prefixes) + observed = sum(choose2(c) for c in counts.values() if c > 1) + expected = samples * (samples - 1) / (2 * (2**bits)) + ratio = observed / expected if expected else 0.0 + if expected >= 10: + sigma = math.sqrt(expected) + status = STATUS_WARN if abs(observed - expected) > max(5.0 * sigma, 10.0) else STATUS_PASS + else: + status = STATUS_WARN if observed > max(10, expected * 10.0) else STATUS_PASS + rows.append( + { + "screen": "collision/birthday sanity check", + "variant": variant, + "kind": "prefix", + "samples": samples, + "prefix_bits": bits, + "observed_collision_pairs": observed, + "expected_collision_pairs": round(expected, 4), + "observed_expected_ratio": round(ratio, 4) if expected else "", + "status": status, + } + ) + distances = [] + for _ in range(near_pairs): + a, b = rng.sample(outputs, 2) + distances.append(hamming_bytes(a, b)) + rows.append( + { + "screen": "near-collision sanity check", + "variant": variant, + "kind": "sampled_pair_hamming", + "samples": near_pairs, + "prefix_bits": "", + "observed_collision_pairs": "", + "expected_collision_pairs": "", + "observed_expected_ratio": "", + "min_hamming": min(distances), + "mean_hamming": round(mean([float(x) for x in distances]), 4), + "max_hamming": max(distances), + "status": STATUS_WARN if min(distances) < 80 else STATUS_PASS, + } + ) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variants", default="baseline,fast8x") + parser.add_argument("--samples", type=int, default=512) + parser.add_argument("--near-pairs", type=int, default=256) + parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + variants = parse_variants(args.variants) + rows = run_screen(variants, args.samples, args.near_pairs, args.seed) + write_single_screen( + args.out, + "collision_screen", + rows, + screen_metadata(variants=variants, samples=args.samples, near_pairs=args.near_pairs, seed=args.seed), + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tests/crypto_analysis/screens/config.py b/tests/crypto_analysis/screens/config.py new file mode 100644 index 0000000..5ec4e9c --- /dev/null +++ b/tests/crypto_analysis/screens/config.py @@ -0,0 +1,39 @@ +"""Shared screen configuration.""" + +from __future__ import annotations + + +PROFILE_DEFAULTS = { + "quick": { + "samples": 64, + "algebraic_vars": 8, + "algebraic_bits": 32, + "collision_samples": 512, + "near_pairs": 256, + "stream_bytes": 1 << 20, + "bm_bits": 4096, + }, + "standard": { + "samples": 256, + "algebraic_vars": 10, + "algebraic_bits": 64, + "collision_samples": 4096, + "near_pairs": 2048, + "stream_bytes": 16 << 20, + "bm_bits": 16384, + }, +} + +DELTA_CLASSES = { + "single_bit": [1 << b for b in (0, 1, 7, 8, 31, 32, 63)], + "single_byte": [0xFF << (8 * b) for b in (0, 1, 3, 7)], + "single_64_word": [0xFFFFFFFFFFFFFFFF], + "all_low_bit_mask": [0x0101010101010101], + "checkerboard_mask": [0xAA55AA55AA55AA55, 0x55AA55AA55AA55AA], + "high_bit_mask": [0x8080808080808080], + "adjacent_bit_mask": [(1 << b) | (1 << (b + 1)) for b in (0, 1, 7, 15, 31, 47, 62)], +} + +ROTATIONS = [1, 2, 3, 4, 5, 7, 8, 13, 16, 17, 31, 32, 33, 47, 63] +PREFIX_BITS = [16, 24, 32, 40, 48, 64] + diff --git a/tests/crypto_analysis/screens/differential_screen.py b/tests/crypto_analysis/screens/differential_screen.py new file mode 100644 index 0000000..15f58fc --- /dev/null +++ b/tests/crypto_analysis/screens/differential_screen.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Black-box differential diffusion probe for TriCube stream variants.""" + +from __future__ import annotations + +import argparse +from collections import Counter +from pathlib import Path +from typing import Any + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import ( # noqa: E402 + STATUS_FAIL, + STATUS_PASS, + STATUS_WARN, + bytes_to_bits, + digest32, + hamming_bytes, + mean, + parse_variants, + rng_for, + xor_bytes, +) +from config import DELTA_CLASSES # noqa: E402 +from _output import screen_metadata, write_single_screen # noqa: E402 + + +def status_for_diff(mean_bits: float, max_bias: float, repeated_top: int) -> str: + if mean_bits < 96 or mean_bits > 160 or repeated_top > 4: + return STATUS_FAIL + if mean_bits < 112 or mean_bits > 144 or max_bias > 0.25 or repeated_top > 1: + return STATUS_WARN + return STATUS_PASS + + +def run_screen(variants: list[str], samples: int, seed: int) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + rng = rng_for(seed) + random_deltas = { + "random_low_weight": [sum(1 << rng.randrange(64) for _ in range(3)) for _ in range(4)], + "random_medium_weight": [rng.getrandbits(64) & rng.getrandbits(64) for _ in range(4)], + "random_full_weight": [rng.getrandbits(64) or 1 for _ in range(4)], + } + all_classes = {**DELTA_CLASSES, **random_deltas} + for variant in variants: + for name, deltas in all_classes.items(): + distances: list[int] = [] + repeated = Counter() + bit_flips = [0] * 256 + for idx in range(samples): + base_seed = rng.getrandbits(64) + delta = deltas[idx % len(deltas)] & 0xFFFFFFFFFFFFFFFF + a = digest32(variant, base_seed) + b = digest32(variant, base_seed ^ delta) + diff = xor_bytes(a, b) + distances.append(hamming_bytes(a, b)) + repeated[diff] += 1 + for bit_idx, bit in enumerate(bytes_to_bits(diff)): + bit_flips[bit_idx] += bit + max_bias = max(abs(count / samples - 0.5) for count in bit_flips) + chi2 = sum(((count - samples / 2) ** 2) / (samples / 2) for count in bit_flips) + top = repeated.most_common(1)[0][1] if repeated else 0 + m = mean([float(x) for x in distances]) + rows.append( + { + "screen": "black-box differential diffusion probe", + "variant": variant, + "delta_class": name, + "samples": samples, + "mean_hamming": round(m, 4), + "min_hamming": min(distances), + "max_hamming": max(distances), + "max_bit_bias": round(max_bias, 6), + "chi_square_bit_flips": round(chi2, 3), + "repeated_output_differences": sum(v - 1 for v in repeated.values() if v > 1), + "top_repeated_difference_count": top, + "status": status_for_diff(m, max_bias, top), + } + ) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variants", default="baseline,fast8x") + parser.add_argument("--samples", type=int, default=64) + parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + variants = parse_variants(args.variants) + rows = run_screen(variants, args.samples, args.seed) + write_single_screen(args.out, "differential_screen", rows, screen_metadata(variants=variants, samples=args.samples, seed=args.seed)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tests/crypto_analysis/screens/external_batteries.py b/tests/crypto_analysis/screens/external_batteries.py new file mode 100644 index 0000000..b32ac27 --- /dev/null +++ b/tests/crypto_analysis/screens/external_batteries.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Availability screen for optional external statistical batteries.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import STATUS_BLOCKED, STATUS_NOT_RUN, command_exists, parse_variants # noqa: E402 +from _output import screen_metadata, write_single_screen # noqa: E402 + + +TOOLS = [ + ("PractRand", "RNG_test", "PractRand 256 MiB quick screen"), + ("SmokeRand", "smokerand", "SmokeRand express"), + ("TestU01 SmallCrush", "testu01_stdin32", "SmallCrush stdin32 wrapper"), + ("Dieharder", "dieharder", "Dieharder full battery"), + ("NIST STS", "assess", "NIST STS assess binary"), +] + + +def run_screen(variants: list[str]) -> list[dict[str, Any]]: + rows = [] + for variant in variants: + for name, binary, purpose in TOOLS: + exists = command_exists(binary) + rows.append( + { + "screen": "external statistical battery", + "variant": variant, + "tool": name, + "binary": binary, + "purpose": purpose, + "status": STATUS_NOT_RUN if exists else STATUS_BLOCKED, + "notes": "tool detected but not launched by this quick run" if exists else "external tool not installed or not on PATH", + } + ) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variants", default="baseline,fast8x") + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + variants = parse_variants(args.variants) + rows = run_screen(variants) + write_single_screen(args.out, "external_batteries", rows, screen_metadata(variants=variants)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/crypto_analysis/screens/low_bit_diagnostics.py b/tests/crypto_analysis/screens/low_bit_diagnostics.py new file mode 100644 index 0000000..a8fde1c --- /dev/null +++ b/tests/crypto_analysis/screens/low_bit_diagnostics.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Low-bit diagnostic screen for TriCube stream variants.""" + +from __future__ import annotations + +import argparse +from collections import Counter +import math +from pathlib import Path +from typing import Any + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import ( # noqa: E402 + STATUS_PASS, + STATUS_WARN, + bit_position_counts64, + mean, + parse_variants, + stream_bytes, + zscore_ones, +) +from _output import screen_metadata, write_single_screen # noqa: E402 + + +def lag_correlation(bits: list[int]) -> float: + if len(bits) < 2: + return 0.0 + xs = bits[:-1] + ys = bits[1:] + mx = mean([float(x) for x in xs]) + my = mean([float(y) for y in ys]) + num = sum((x - mx) * (y - my) for x, y in zip(xs, ys)) + denx = math.sqrt(sum((x - mx) ** 2 for x in xs)) + deny = math.sqrt(sum((y - my) ** 2 for y in ys)) + return num / (denx * deny) if denx and deny else 0.0 + + +def run_screen(variants: list[str], stream_size: int, seed: int) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for variant in variants: + data = stream_bytes(variant, seed, stream_size) + n_bytes = len(data) + byte_lsb = sum(byte & 1 for byte in data) + word32_count = n_bytes // 4 + word64_count = n_bytes // 8 + word32_lsb = sum(int.from_bytes(data[i * 4 : i * 4 + 4], "little") & 1 for i in range(word32_count)) + word64_lsb = sum(int.from_bytes(data[i * 8 : i * 8 + 8], "little") & 1 for i in range(word64_count)) + low_nibbles = Counter(byte & 0x0F for byte in data) + expected_nibble = n_bytes / 16 + nibble_chi2 = sum(((low_nibbles[i] - expected_nibble) ** 2) / expected_nibble for i in range(16)) + bit_counts = bit_position_counts64(data[: word64_count * 8]) + bit_z = [abs(zscore_ones(count, word64_count)) for count in bit_counts] + low_bits = [(byte & 1) for byte in data] + transitions = Counter((low_bits[i], low_bits[i + 1]) for i in range(len(low_bits) - 1)) + high_bits = [(byte >> 7) & 1 for byte in data] + low_lag1 = lag_correlation(low_bits) + high_lag1 = lag_correlation(high_bits) + max_z = max( + abs(zscore_ones(byte_lsb, n_bytes)), + abs(zscore_ones(word32_lsb, word32_count)), + abs(zscore_ones(word64_lsb, word64_count)), + max(bit_z) if bit_z else 0.0, + ) + status = STATUS_WARN if max_z > 6.0 or nibble_chi2 > 45.0 or abs(low_lag1) > 0.01 else STATUS_PASS + rows.append( + { + "screen": "low-bit diagnostic screen", + "variant": variant, + "bytes": n_bytes, + "byte_lsb_fraction": round(byte_lsb / n_bytes, 8), + "byte_lsb_z": round(zscore_ones(byte_lsb, n_bytes), 4), + "word32_lsb_fraction": round(word32_lsb / word32_count, 8), + "word32_lsb_z": round(zscore_ones(word32_lsb, word32_count), 4), + "word64_lsb_fraction": round(word64_lsb / word64_count, 8), + "word64_lsb_z": round(zscore_ones(word64_lsb, word64_count), 4), + "low_nibble_chi_square": round(nibble_chi2, 4), + "max_bit_position_z64": round(max(bit_z), 4), + "low_bit_transition_00": transitions[(0, 0)], + "low_bit_transition_01": transitions[(0, 1)], + "low_bit_transition_10": transitions[(1, 0)], + "low_bit_transition_11": transitions[(1, 1)], + "low_bit_lag1_corr": round(low_lag1, 8), + "high_bit_lag1_corr": round(high_lag1, 8), + "status": status, + } + ) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variants", default="baseline,fast8x") + parser.add_argument("--bytes", type=int, default=16_777_216) + parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + + variants = parse_variants(args.variants) + rows = run_screen(variants, args.bytes, args.seed) + write_single_screen(args.out, "low_bit_diagnostics", rows, screen_metadata(variants=variants, seed=args.seed, bytes=args.bytes)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/crypto_analysis/screens/overlap_fork_screen.py b/tests/crypto_analysis/screens/overlap_fork_screen.py new file mode 100644 index 0000000..dccc1c2 --- /dev/null +++ b/tests/crypto_analysis/screens/overlap_fork_screen.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Overlap/fork stream uniqueness screen for TriCube stream variants.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import STATUS_FAIL, STATUS_PASS, count_repeated_blocks, hamming_bytes, parse_variants, stream_bytes # noqa: E402 +from _output import screen_metadata, write_single_screen # noqa: E402 + + +def run_screen(variants: list[str], stream_size: int, seed: int) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + seed_set = [seed, seed + 1, seed ^ 1, seed ^ (1 << 63)] + for variant in variants: + streams = {s: stream_bytes(variant, s, stream_size) for s in seed_set} + for block_size in (16, 32, 64): + within = sum(count_repeated_blocks(data, block_size) for data in streams.values()) + cross = 0 + same_position = 0 + seen_by_seed: dict[bytes, int] = {} + for s, data in streams.items(): + for pos in range(0, len(data) - block_size + 1, block_size): + block = data[pos : pos + block_size] + if block in seen_by_seed and seen_by_seed[block] != s: + cross += 1 + else: + seen_by_seed[block] = s + first = streams[seed] + second = streams[seed + 1] + for pos in range(0, min(len(first), len(second)) - block_size + 1, block_size): + if first[pos : pos + block_size] == second[pos : pos + block_size]: + same_position += 1 + prefix_hamming = hamming_bytes(first[:1024], second[:1024]) + status = STATUS_FAIL if within or cross or same_position else STATUS_PASS + rows.append( + { + "screen": "overlap/fork stream uniqueness screen", + "variant": variant, + "stream_bytes_per_seed": stream_size, + "seeds": ",".join(str(s) for s in seed_set), + "block_size": block_size, + "within_stream_repeated_blocks": within, + "cross_stream_overlaps": cross, + "same_position_equal_blocks": same_position, + "adjacent_seed_prefix_hamming_1024B": prefix_hamming, + "status": status, + } + ) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variants", default="baseline,fast8x") + parser.add_argument("--bytes", type=int, default=1 << 20) + parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + variants = parse_variants(args.variants) + rows = run_screen(variants, args.bytes, args.seed) + write_single_screen(args.out, "overlap_fork_screen", rows, screen_metadata(variants=variants, bytes=args.bytes, seed=args.seed)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tests/crypto_analysis/screens/rotational_screen.py b/tests/crypto_analysis/screens/rotational_screen.py new file mode 100644 index 0000000..3c32b2a --- /dev/null +++ b/tests/crypto_analysis/screens/rotational_screen.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Black-box rotational relation probe for TriCube stream variants.""" + +from __future__ import annotations + +import argparse +import math +from pathlib import Path +from typing import Any + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import ( # noqa: E402 + STATUS_FAIL, + STATUS_PASS, + STATUS_WARN, + bytes_to_bits, + digest32, + hamming_bytes, + mean, + parse_variants, + rng_for, + rotate_words64, + rotl64, + xor_bytes, +) +from config import ROTATIONS # noqa: E402 +from _output import screen_metadata, write_single_screen # noqa: E402 + + +def run_screen(variants: list[str], samples: int, seed: int) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + rng = rng_for(seed ^ 0xA55A) + for variant in variants: + for rotation in ROTATIONS: + distances: list[int] = [] + exact = 0 + bit_flips = [0] * 256 + for _ in range(samples): + base_seed = rng.getrandbits(64) + a = digest32(variant, base_seed) + b = digest32(variant, rotl64(base_seed, rotation)) + expected = rotate_words64(a, rotation) + diff = xor_bytes(expected, b) + if diff == b"\0" * len(diff): + exact += 1 + distances.append(hamming_bytes(expected, b)) + for bit_idx, bit in enumerate(bytes_to_bits(diff)): + bit_flips[bit_idx] += bit + m = mean([float(x) for x in distances]) + max_bias = max(abs(count / samples - 0.5) for count in bit_flips) + status = STATUS_FAIL if exact else (STATUS_WARN if m < 112 or m > 144 or max_bias > 0.25 else STATUS_PASS) + rows.append( + { + "screen": "black-box rotational relation probe", + "variant": variant, + "rotation": rotation, + "samples": samples, + "mean_rotational_distance": round(m, 4), + "min_distance": min(distances), + "max_distance": max(distances), + "max_bit_bias": round(max_bias, 6), + "exact_rotational_relations": exact, + "z_score_mean_distance": round((m - 128.0) / math.sqrt(256 * 0.25 / samples), 4), + "status": status, + } + ) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variants", default="baseline,fast8x") + parser.add_argument("--samples", type=int, default=64) + parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + variants = parse_variants(args.variants) + rows = run_screen(variants, args.samples, args.seed) + write_single_screen(args.out, "rotational_screen", rows, screen_metadata(variants=variants, samples=args.samples, seed=args.seed)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tests/crypto_analysis/screens/state_recovery_screen.py b/tests/crypto_analysis/screens/state_recovery_screen.py new file mode 100644 index 0000000..4ff4893 --- /dev/null +++ b/tests/crypto_analysis/screens/state_recovery_screen.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Black-box state-recovery and predictability screen for TriCube streams.""" + +from __future__ import annotations + +import argparse +from collections import Counter, defaultdict +from pathlib import Path +from typing import Any + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import ( # noqa: E402 + STATUS_PASS, + STATUS_WARN, + berlekamp_massey_binary, + bytes_to_bits, + mean, + parse_variants, + stream_bytes, +) +from _output import screen_metadata, write_single_screen # noqa: E402 + + +def run_screen(variants: list[str], stream_size: int, bm_bits: int, seed: int) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for variant in variants: + data = stream_bytes(variant, seed, stream_size) + half = len(data) // 2 + train = data[:half] + test = data[half:] + most_common_byte = Counter(train).most_common(1)[0][0] + next_byte_acc = sum(1 for b in test if b == most_common_byte) / len(test) + transitions: dict[int, Counter[int]] = defaultdict(Counter) + for a, b in zip(train, train[1:]): + transitions[a][b] += 1 + hits = 0 + total = 0 + for a, b in zip(test, test[1:]): + pred = transitions.get(a) + if pred: + hits += pred.most_common(1)[0][0] == b + total += 1 + ngram_acc = hits / total if total else 0.0 + train_bits = bytes_to_bits(train) + test_bits = bytes_to_bits(test) + majority = 1 if sum(train_bits) >= len(train_bits) / 2 else 0 + bit_acc = sum(bit == majority for bit in test_bits) / len(test_bits) if test_bits else 0.0 + lc_ratios = [] + for bit_pos in (0, 1, 7): + seq = [((byte >> bit_pos) & 1) for byte in data[:bm_bits]] + lc_ratios.append(berlekamp_massey_binary(seq) / len(seq)) + status = STATUS_WARN if next_byte_acc > 0.02 or ngram_acc > 0.02 or abs(bit_acc - 0.5) > 0.02 else STATUS_PASS + rows.append( + { + "screen": "black-box state-recovery/predictability screen", + "variant": variant, + "train_bytes": len(train), + "test_bytes": len(test), + "next_byte_accuracy": round(next_byte_acc, 6), + "ngram1_accuracy": round(ngram_acc, 6), + "random_next_byte_baseline": round(1 / 256, 6), + "bit_accuracy": round(bit_acc, 6), + "berlekamp_massey_lc_ratio_min": round(min(lc_ratios), 6), + "berlekamp_massey_lc_ratio_mean": round(mean(lc_ratios), 6), + "berlekamp_massey_lc_ratio_max": round(max(lc_ratios), 6), + "status": status, + } + ) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variants", default="baseline,fast8x") + parser.add_argument("--bytes", type=int, default=1 << 20) + parser.add_argument("--bm-bits", type=int, default=4096) + parser.add_argument("--seed", type=int, default=123) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + variants = parse_variants(args.variants) + rows = run_screen(variants, args.bytes, args.bm_bits, args.seed) + write_single_screen( + args.out, + "state_recovery_screen", + rows, + screen_metadata(variants=variants, bytes=args.bytes, bm_bits=args.bm_bits, seed=args.seed), + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tests/crypto_analysis/screens/whitebox_round_model.py b/tests/crypto_analysis/screens/whitebox_round_model.py new file mode 100644 index 0000000..58d9ced --- /dev/null +++ b/tests/crypto_analysis/screens/whitebox_round_model.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +"""White-box schedule and word-dependency analysis for TriCube. + +This is structural analysis, not cryptanalysis. It models which original +64-bit lanes can influence which later 64-bit lanes under the specified +round schedule. It does not model bit-level modular-addition differentials, +rotational probabilities, algebraic degree, or attack cost. +""" + +from __future__ import annotations + +import argparse +import json +import statistics +from pathlib import Path + +from _bootstrap import ensure_import_path + +ensure_import_path() + +from common import environment, markdown_table, write_csv, write_json +from models.tricube_schedule import ( + STATE_WORDS, + SHELL_LANES, + TETRA_ROTATION_SETS, + build_edges, + build_tetrahedra, + edge_rotations, + lane_permutation, + oriented_tetrahedron, + permutation_rotation, + shell_targets, + validate_schedule, +) + + +CHECK_ROUNDS = [0, 1, 2, 3, 4, 6, 8, 12, 16, 24] + + +def apply_round(deps: list[set[int]], rnd: int, tetrahedra: list[tuple[int, int, int, int]], edges: list[tuple[int, int]]) -> list[set[int]]: + deps = [set(d) for d in deps] + sr = rnd % 24 + + for t, tet in enumerate(tetrahedra): + a, b, c, d = oriented_tetrahedron(tet, sr, t) + merged = deps[a] | deps[b] | deps[c] | deps[d] + deps[a] = set(merged) + deps[b] = set(merged) + deps[c] = set(merged) + deps[d] = set(merged) + + for a, b in edges: + merged = deps[a] | deps[b] + deps[a] = set(merged) + deps[b] = set(merged) + + for lane, vertex, opposite in shell_targets(sr): + lane_dep = deps[lane] | deps[vertex] + deps[lane] = set(lane_dep) + deps[opposite] = deps[opposite] | lane_dep | deps[vertex] + + return [set(deps[source]) for source in lane_permutation()] + + +def dependency_row(round_number: int, deps: list[set[int]]) -> dict[str, object]: + sizes = [len(d) for d in deps] + influence = [sum(1 for d in deps if src in d) for src in range(STATE_WORDS)] + return { + "rounds": round_number, + "min_lane_dependency": min(sizes), + "mean_lane_dependency": round(statistics.mean(sizes), 4), + "max_lane_dependency": max(sizes), + "fully_mixed_lanes": sum(1 for size in sizes if size == STATE_WORDS), + "min_source_influence_lanes": min(influence), + "mean_source_influence_lanes": round(statistics.mean(influence), 4), + "max_source_influence_lanes": max(influence), + } + + +def output_dependency_rows(round_number: int, deps: list[set[int]]) -> list[dict[str, object]]: + rows: list[dict[str, object]] = [] + for j in range(4): + lanes = [ + (j * 5) & 31, + (j * 11 + 7) & 31, + (j * 17 + 13) & 31, + (j * 23 + 19) & 31, + ] + merged: set[int] = set() + for lane in lanes: + merged |= deps[lane] + rows.append( + { + "rounds": round_number, + "output_word": j, + "state_lanes_read": ",".join(str(x) for x in lanes), + "dependency_size": len(merged), + "depends_on_all_initial_lanes": len(merged) == STATE_WORDS, + } + ) + return rows + + +def schedule_coverage_rows(tetrahedra: list[tuple[int, int, int, int]], edges: list[tuple[int, int]], rounds: int) -> list[dict[str, object]]: + from collections import Counter + + tet_counts = Counter(lane for tet in tetrahedra for lane in tet) + edge_counts = Counter(lane for edge in edges for lane in edge) + shell_vertex_counts = Counter() + shell_opposite_counts = Counter() + tetra_rotations = Counter() + observed_edge_rotations = Counter() + perm_rotations = Counter() + for rnd in range(rounds): + sr = rnd % 24 + for t in range(len(tetrahedra)): + tetra_rotations.update(TETRA_ROTATION_SETS[(sr + t) & 3]) + for e in range(len(edges)): + observed_edge_rotations.update(edge_rotations(sr, e)) + for i in range(STATE_WORDS): + perm_rotations[permutation_rotation(sr, i)] += 1 + for _lane, vertex, opposite in shell_targets(sr): + shell_vertex_counts[vertex] += 1 + shell_opposite_counts[opposite] += 1 + + return [ + { + "metric": "tetrahedra_per_round", + "value": len(tetrahedra), + "notes": "six tetrahedra for each of eight cube cells", + }, + { + "metric": "edges_per_round", + "value": len(edges), + "notes": "all positive-axis grid adjacencies in the 3x3x3 vertex grid", + }, + { + "metric": "min_tetra_incidence_per_vertex", + "value": min(tet_counts.values()), + "notes": "per round, vertex lanes only", + }, + { + "metric": "max_tetra_incidence_per_vertex", + "value": max(tet_counts.values()), + "notes": "per round, vertex lanes only", + }, + { + "metric": "min_edge_degree_per_vertex", + "value": min(edge_counts.values()), + "notes": "corners are lower degree, center is highest degree", + }, + { + "metric": "max_edge_degree_per_vertex", + "value": max(edge_counts.values()), + "notes": "axis-adjacent grid graph degree", + }, + { + "metric": "shell_vertex_lanes_touched", + "value": len(shell_vertex_counts), + "notes": f"over {rounds} modeled rounds", + }, + { + "metric": "shell_opposite_lanes_touched", + "value": len(shell_opposite_counts), + "notes": f"over {rounds} modeled rounds", + }, + { + "metric": "distinct_tetra_rotation_constants", + "value": len(tetra_rotations), + "notes": ",".join(str(x) for x in sorted(tetra_rotations)), + }, + { + "metric": "distinct_edge_rotation_constants", + "value": len(observed_edge_rotations), + "notes": "edge coupling uses rotations 1..61 over a 24-round period", + }, + { + "metric": "distinct_permutation_rotation_constants", + "value": len(perm_rotations), + "notes": "constant-injection rotations used after lane permutation", + }, + ] + + +def run(rounds: int) -> dict[str, object]: + validate_schedule() + tetrahedra = build_tetrahedra() + edges = build_edges() + deps = [{i} for i in range(STATE_WORDS)] + dependency_rows: list[dict[str, object]] = [] + output_rows: list[dict[str, object]] = [] + for r in range(rounds + 1): + if r in CHECK_ROUNDS or r == rounds: + dependency_rows.append(dependency_row(r, deps)) + output_rows.extend(output_dependency_rows(r, deps)) + if r < rounds: + deps = apply_round(deps, r, tetrahedra, edges) + coverage_rows = schedule_coverage_rows(tetrahedra, edges, rounds) + first_full = next((row["rounds"] for row in dependency_rows if row["fully_mixed_lanes"] == STATE_WORDS), None) + first_output_full = next( + ( + row["rounds"] + for row in output_rows + if row["rounds"] > 0 + and all( + out_row["depends_on_all_initial_lanes"] + for out_row in output_rows + if out_row["rounds"] == row["rounds"] + ) + ), + None, + ) + return { + "metadata": {}, + "summary": { + "rounds_modeled": rounds, + "tetrahedra_per_round": len(tetrahedra), + "edges_per_round": len(edges), + "first_round_in_table_all_state_lanes_full_dependency": first_full, + "first_round_in_table_first_32_output_bytes_full_dependency": first_output_full, + "interpretation": "word-level dependency reaches all modeled source lanes quickly, but this is not a differential, rotational, or algebraic security bound", + }, + "dependency_by_round": dependency_rows, + "output_dependency": output_rows, + "schedule_coverage": coverage_rows, + } + + +def write_markdown(path: Path, data: dict[str, object]) -> None: + summary = data["summary"] + dependency_rows = data["dependency_by_round"] + output_rows = data["output_dependency"] + coverage_rows = data["schedule_coverage"] + text = [ + "# TriCube White-Box Round-Model Summary", + "", + "This report analyzes the specified round schedule at word-lane granularity. It uses the TriCube tetrahedron, edge, shell, and permutation rules directly instead of treating the primitive as a black box.", + "", + "This is not cryptanalysis. It does not model modular-addition differential probabilities, rotational trails, SAT/SMT/MILP constraints, algebraic invariants, or attack complexity. It answers a narrower question: which original 64-bit lanes can influence which later 64-bit lanes under the current schedule.", + "", + "## Summary", + "", + markdown_table([summary]), + "## State Dependency by Round", + "", + markdown_table(dependency_rows), + "## First 32 Output Bytes Dependency", + "", + markdown_table(output_rows), + "## Schedule Coverage", + "", + markdown_table(coverage_rows), + ] + path.write_text("\n".join(text), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run TriCube white-box schedule dependency analysis.") + parser.add_argument("--rounds", type=int, default=24) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + args.out.mkdir(parents=True, exist_ok=True) + data = run(args.rounds) + data["metadata"] = environment() + data["metadata"]["rounds"] = args.rounds + write_json(args.out / "summary.json", data) + write_csv(args.out / "dependency_by_round.csv", data["dependency_by_round"]) + write_csv(args.out / "output_dependency.csv", data["output_dependency"]) + write_csv(args.out / "schedule_coverage.csv", data["schedule_coverage"]) + write_markdown(args.out / "summary.md", data) + print(f"wrote {args.out}") + print(json.dumps(data["summary"], indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/crypto_analysis/tooling/README.md b/tests/crypto_analysis/tooling/README.md new file mode 100644 index 0000000..2a8ff5e --- /dev/null +++ b/tests/crypto_analysis/tooling/README.md @@ -0,0 +1,45 @@ +# Optional Analysis Tooling + +This folder contains small checks for optional external cryptanalysis and +statistical-testing tools. It does not vendor those tools and it does not +implement formal cryptanalysis. + +The scripts are deliberately small: + +- `check_tools.py` reports which optional tools are installed. +- `z3_smoke.py` verifies that the Python Z3 binding can solve a tiny + bit-vector equation. +- `sage_smoke.py` verifies that SageMath can construct a small Boolean + polynomial ring. Run it with Sage's Python. +- `solver_model_plan.md` lists the TriCube-specific modeling work still needed + before solver results would mean anything. + +## Commands + +Check tool availability: + +```bash +python tests/crypto_analysis/tooling/check_tools.py +``` + +Run the Z3 smoke test if `z3-solver` is installed: + +```bash +python tests/crypto_analysis/tooling/z3_smoke.py +``` + +Run the SageMath smoke test if `sage` is installed: + +```bash +sage -python tests/crypto_analysis/tooling/sage_smoke.py +``` + +Missing tools are reported as `NOT_INSTALLED`, not as repository failures. + +## Boundary + +These scripts only check plumbing. A real reduced-round analysis still needs a +verified TriCube model for the 32-lane state, tetrahedral schedule, edge +coupling, shell coupling, lane permutation, constants, absorb/finalize rules, +and output extraction. + diff --git a/tests/crypto_analysis/tooling/check_tools.py b/tests/crypto_analysis/tooling/check_tools.py new file mode 100644 index 0000000..278f6e4 --- /dev/null +++ b/tests/crypto_analysis/tooling/check_tools.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Check optional cryptanalysis and statistical-testing tools. + +The script reports availability only. Missing tools are expected on many +developer machines and are reported as NOT_INSTALLED rather than failure. +""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +from pathlib import Path +import shutil +import subprocess +import sys +from typing import Any + + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +def command_path(*names: str) -> str | None: + for name in names: + found = shutil.which(name) + if found: + return found + return None + + +def python_package(name: str) -> bool: + return importlib.util.find_spec(name) is not None + + +def version_for(command: str, args: list[str] | None = None) -> str: + args = args or ["--version"] + try: + proc = subprocess.run( + [command, *args], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=5, + check=False, + ) + except Exception as exc: + return f"version unavailable: {exc.__class__.__name__}" + text = " ".join(proc.stdout.strip().split()) + return text[:180] if text else "version unavailable" + + +def local_testu01_adapter() -> str | None: + candidates = [ + REPO_ROOT / "external_tools" / "bin" / "testu01_stdin32", + REPO_ROOT.parent / "external_tools" / "bin" / "testu01_stdin32", + REPO_ROOT / "external_tools" / "bin" / "testu01_stdin64", + REPO_ROOT.parent / "external_tools" / "bin" / "testu01_stdin64", + ] + for candidate in candidates: + if candidate.exists() and candidate.is_file(): + return str(candidate) + return command_path("testu01_stdin32", "testu01_stdin64", "TestU01Drv") + + +def row(area: str, tool: str, status: str, detail: str, path: str = "") -> dict[str, str]: + return { + "area": area, + "tool": tool, + "status": status, + "path": path, + "detail": detail, + } + + +def collect() -> list[dict[str, str]]: + rows: list[dict[str, str]] = [] + + if python_package("z3"): + rows.append(row("SMT", "Z3 Python package", "INSTALLED", "import z3 succeeds")) + else: + rows.append(row("SMT", "Z3 Python package", "NOT_INSTALLED", "install optional package with: python -m pip install z3-solver")) + + sage = command_path("sage") + if sage: + rows.append(row("Algebraic", "SageMath", "INSTALLED", version_for(sage, ["--version"]), sage)) + else: + rows.append(row("Algebraic", "SageMath", "NOT_INSTALLED", "install SageMath separately; it is not a Python package dependency")) + + cms = command_path("cryptominisat5", "cryptominisat", "cms") + if cms: + rows.append(row("SAT", "CryptoMiniSat", "INSTALLED", version_for(cms, ["--version"]), cms)) + else: + rows.append(row("SAT", "CryptoMiniSat", "NOT_INSTALLED", "optional SAT solver for future CNF experiments")) + + dieharder = command_path("dieharder") + if dieharder: + rows.append(row("Statistical batteries", "Dieharder", "INSTALLED", version_for(dieharder, ["--version"]), dieharder)) + else: + rows.append(row("Statistical batteries", "Dieharder", "NOT_INSTALLED", "install separately; do not vendor in this repo")) + + rng_test = command_path("RNG_test", "pracrand-RNG_test") + if rng_test: + rows.append(row("Statistical batteries", "PractRand RNG_test", "INSTALLED", version_for(rng_test, ["--version"]), rng_test)) + else: + rows.append(row("Statistical batteries", "PractRand RNG_test", "NOT_INSTALLED", "install PractRand separately")) + + testu01 = local_testu01_adapter() + if testu01: + rows.append(row("Statistical batteries", "TestU01 stdin adapter", "INSTALLED", "stdin adapter found", testu01)) + else: + rows.append(row("Statistical batteries", "TestU01 stdin adapter", "NOT_INSTALLED", "build or install a local adapter; TestU01 is not bundled")) + + smokerand = command_path("smokerand", "SmokeRand") + if smokerand: + rows.append(row("Statistical batteries", "SmokeRand", "INSTALLED", version_for(smokerand, ["--help"]), smokerand)) + else: + rows.append(row("Statistical batteries", "SmokeRand", "NOT_INSTALLED", "install from upstream if needed")) + + nist = command_path("assess") + if nist: + rows.append(row("Statistical batteries", "NIST STS assess", "INSTALLED", "assess command found", nist)) + else: + rows.append(row("Statistical batteries", "NIST STS assess", "NOT_INSTALLED", "install NIST STS separately; command is commonly named assess")) + + return rows + + +def markdown_table(rows: list[dict[str, str]]) -> str: + fields = ["area", "tool", "status", "path", "detail"] + lines = ["| " + " | ".join(fields) + " |", "| " + " | ".join(["---"] * len(fields)) + " |"] + for item in rows: + lines.append("| " + " | ".join(item.get(field, "").replace("|", "\\|") for field in fields) + " |") + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check optional TriCube analysis tools.") + parser.add_argument("--json", type=Path, help="write JSON output to this path") + args = parser.parse_args() + rows = collect() + if args.json: + args.json.parent.mkdir(parents=True, exist_ok=True) + args.json.write_text(json.dumps({"tools": rows}, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(markdown_table(rows)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tests/crypto_analysis/tooling/sage_smoke.py b/tests/crypto_analysis/tooling/sage_smoke.py new file mode 100644 index 0000000..6d4785c --- /dev/null +++ b/tests/crypto_analysis/tooling/sage_smoke.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Tiny SageMath smoke test for optional algebraic tooling. + +Run with ``sage -python tests/crypto_analysis/tooling/sage_smoke.py``. +This does not analyze TriCube. It only verifies that Sage can construct a small +Boolean polynomial ring and compute a normal form. +""" + +from __future__ import annotations + +import json + + +def main() -> int: + try: + from sage.all import BooleanPolynomialRing # type: ignore[import-not-found] + except Exception as exc: + print(json.dumps({"tool": "sage", "status": "NOT_INSTALLED", "detail": str(exc)}, indent=2)) + return 0 + + ring = BooleanPolynomialRing(3, "x") + x0, x1, x2 = ring.gens() + poly = (x0 + x1) * (x1 + x2) + x0 * x2 + print( + json.dumps( + { + "tool": "sage", + "status": "PASS", + "polynomial": str(poly), + "degree": int(poly.degree()), + "note": "Sage smoke test only; not TriCube cryptanalysis", + }, + indent=2, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tests/crypto_analysis/tooling/solver_model_plan.md b/tests/crypto_analysis/tooling/solver_model_plan.md new file mode 100644 index 0000000..2d28450 --- /dev/null +++ b/tests/crypto_analysis/tooling/solver_model_plan.md @@ -0,0 +1,60 @@ +# Solver Model Work Plan + +This note is a short checklist for building real TriCube solver models. It is +not a result report and it is not evidence of security. + +The custom work is the model. Z3, SageMath, CryptoMiniSat, CLAASP, CryptoSMT, +and related tools do not know TriCube's tetrahedral schedule, shell lanes, +domain separation, or output extraction. A solver result is only useful after +the TriCube model has been checked against the specification and fixed vectors. + +## Model Foundations + +1. Encode the 32-lane state, including 27 vertex lanes and 5 shell/global + lanes. +2. Encode the 3 x 3 x 3 vertex mapping, 8 cube cells, 48 tetrahedral + neighborhoods, 54 grid edges, shell coupling schedule, lane permutation, and + round constants. +3. Add reduced-round hooks for 1, 2, 3, 4, 6, 8, and full baseline rounds. +4. Validate every reduced-round implementation against a small executable + reference before using it for search. +5. Record whether the model is bit-exact, word-level, truncated-difference, or + dependency-only. Never mix those labels. + +## Differential Model + +The first useful differential model should be reduced-round and explicitly +limited. It should model modular addition, XOR, rotations, constants, and the +in-place tetrahedral update order. The goal is to search for low-weight or +high-probability reduced-round trails, not to claim full-round resistance. + +## Rotational Model + +The rotational model must track how word rotations interact with constants, +modular addition, lane permutation, shell coupling, and output extraction. +Black-box rotational-distance tests are not enough for this. + +## Algebraic Model + +The algebraic model should start with reduced-round components and a small +number of lanes. SageMath can help with Boolean polynomial experiments, but the +state size makes naive full ANF expansion impractical. Any low-degree or +invariant result must be independently reproduced. + +## Collision and Preimage Model + +A reduced-round collision/preimage model can be built with SMT or SAT once the +bit-exact round model is validated. The first target should be deliberately +small: short messages, reduced rounds, and truncated outputs. + +## Validation + +Incorrect models are dangerous because they create false confidence. Every +solver model needs: + +- known-answer checks against the C implementation or a clean reduced-round + reference; +- test vectors for the exact reduced-round variant under study; +- clear labels for assumptions and approximations; +- command lines, tool versions, and status labels in every generated summary. + diff --git a/tests/crypto_analysis/tooling/z3_smoke.py b/tests/crypto_analysis/tooling/z3_smoke.py new file mode 100644 index 0000000..0bdc2f9 --- /dev/null +++ b/tests/crypto_analysis/tooling/z3_smoke.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Tiny Z3 smoke test for optional solver setup. + +This is not TriCube cryptanalysis. It only verifies that the Python Z3 binding +can solve a fixed-width bit-vector equation on the local machine. +""" + +from __future__ import annotations + +import json + + +def main() -> int: + try: + import z3 # type: ignore[import-not-found] + except Exception as exc: + print(json.dumps({"tool": "z3", "status": "NOT_INSTALLED", "detail": str(exc)}, indent=2)) + return 0 + + x = z3.BitVec("x", 64) + solver = z3.Solver() + solver.add(((x + z3.BitVecVal(0x9E3779B97F4A7C15, 64)) ^ z3.RotateLeft(x, 17)) == 0x123456789ABCDEF0) + status = solver.check() + model_value = None + if status == z3.sat: + model_value = hex(solver.model()[x].as_long()) + print( + json.dumps( + { + "tool": "z3", + "status": "PASS" if status == z3.sat else "WARN", + "solver_status": str(status), + "model_x": model_value, + "note": "solver smoke test only; not TriCube cryptanalysis", + }, + indent=2, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + diff --git a/tools/run_dieharder.sh b/tools/run_dieharder.sh index 17cf738..03ca797 100755 --- a/tools/run_dieharder.sh +++ b/tools/run_dieharder.sh @@ -3,9 +3,9 @@ set -euo pipefail BYTES="${1:-1073741824}" SEED="${SEED:-123}" +VARIANT="${VARIANT:-baseline}" DIEHARDER="${DIEHARDER:-dieharder}" make -C c all >/dev/null -c/build/tricube stream --seed "$SEED" --bytes "$BYTES" --out - \ +c/build/tricube stream --seed "$SEED" --bytes "$BYTES" --out - --variant "$VARIANT" \ | "$DIEHARDER" -g 200 -a - diff --git a/tools/run_practrand.sh b/tools/run_practrand.sh index a0f841e..eb80db2 100755 --- a/tools/run_practrand.sh +++ b/tools/run_practrand.sh @@ -3,9 +3,9 @@ set -euo pipefail BYTES="${1:-1073741824}" SEED="${SEED:-123}" +VARIANT="${VARIANT:-baseline}" RNG_TEST="${RNG_TEST:-RNG_test}" make -C c all >/dev/null -c/build/tricube stream --seed "$SEED" --bytes "$BYTES" --out - \ +c/build/tricube stream --seed "$SEED" --bytes "$BYTES" --out - --variant "$VARIANT" \ | "$RNG_TEST" stdin32 -tlmin 1KB -tlmax "$BYTES" -tf 2 -te 1 - diff --git a/tools/run_smokerand.sh b/tools/run_smokerand.sh new file mode 100755 index 0000000..26f2b9a --- /dev/null +++ b/tools/run_smokerand.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -uo pipefail + +BATTERY="${1:-express}" +BYTES="${2:-536870912}" +SEED="${SEED:-123}" +VARIANT="${VARIANT:-baseline}" +SMOKERAND="${SMOKERAND:-smokerand}" +UNBOUNDED="${UNBOUNDED:-0}" + +make -C c all >/dev/null +if [[ "$UNBOUNDED" == "1" ]]; then + c/build/tricube stream --seed "$SEED" --unbounded --out - --variant "$VARIANT" \ + | "$SMOKERAND" "$BATTERY" stdin64 +else + c/build/tricube stream --seed "$SEED" --bytes "$BYTES" --out - --variant "$VARIANT" \ + | "$SMOKERAND" "$BATTERY" stdin64 +fi +exit "${PIPESTATUS[1]}" diff --git a/tools/run_stat_batteries.md b/tools/run_stat_batteries.md index 5b56861..e5dc163 100644 --- a/tools/run_stat_batteries.md +++ b/tools/run_stat_batteries.md @@ -3,32 +3,107 @@ The scripts in this directory stream bytes from the C CLI into external tools. They do not install those tools. +TriCube does not bundle PractRand, Dieharder, TestU01, SmokeRand, NIST STS, or +their binaries. Install each tool from its upstream source or your package +manager, then follow that tool's license. The repository notice table is +[THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md). + ## PractRand ```bash tools/run_practrand.sh 1073741824 +VARIANT=fast8x tools/run_practrand.sh 1073741824 ``` This runs `RNG_test stdin32` with expanded testing and extra folding. A WARN or FAIL must be investigated. A pass does not prove security. +PractRand can be run at a fixed maximum length. For a quick regression screen, +use 256 MiB. For a standard development run, use 1-10 GiB. For a long campaign, +use 100 GiB or more across multiple seeds. Record any unusual, suspicious, or +FAIL rows as WARN or FAIL rather than smoothing them into a pass. + ## Dieharder ```bash tools/run_dieharder.sh 1073741824 +VARIANT=fast8x tools/run_dieharder.sh 1073741824 ``` This uses stdin generator mode (`-g 200`) and `-a` to request the installed Dieharder battery. +Dieharder reports many p-values, so occasional WEAK rows can happen by chance. +The important questions are whether failures reproduce, whether the same test +keeps warning across seeds, and whether the input stream or stdin generator +exhausted before the battery finished. + +## SmokeRand + +```bash +tools/run_smokerand.sh express 536870912 +VARIANT=fast8x tools/run_smokerand.sh express 536870912 +UNBOUNDED=1 VARIANT=fast8x tools/run_smokerand.sh full 1099511627776 +``` + +The SmokeRand wrapper streams 64-bit stdin input. `UNBOUNDED=1` is useful for +full batteries that decide their own stopping point. + +SmokeRand supports `express`, `brief`, `default`, and `full` batteries. The +full battery is the right long-run target, but it should be recorded separately +from express results. A timeout is a harness/runtime blocker, not a statistical +failure. + ## TestU01 ```bash TESTU01_STDIN=path/to/testu01_stdin32 tools/run_testu01.sh smallcrush 1073741824 TESTU01_STDIN=path/to/testu01_stdin32 tools/run_testu01.sh crush 1073741824 TESTU01_STDIN=path/to/testu01_stdin32 tools/run_testu01.sh bigcrush 1099511627776 +VARIANT=fast8x TESTU01_STDIN=path/to/testu01_stdin32 tools/run_testu01.sh smallcrush 1073741824 ``` The `testu01_stdin32` wrapper is not part of upstream TestU01; it must be built -locally or provided by the user. +by the reviewer or provided as an existing executable. + +SmallCrush is a quick regression gate. Crush is the normal development target. +BigCrush is a long campaign and should be run with progress logging because it +can take hours depending on machine speed and input throughput. + +## NIST STS + +Follow the dedicated workflow in [run_nist_sts.md](run_nist_sts.md). The STS +`assess` binary uses its own experiment-directory format and should be treated +as an external tool, not as a bundled TriCube dependency. + +## Suggested Profiles + +Quick profile: + +```bash +python tests/crypto_analysis/run_all_screens.py --profile quick --variants baseline,fast8x --out tests/crypto_analysis/results/quick-latest +tools/run_practrand.sh 268435456 +tools/run_smokerand.sh express 536870912 +TESTU01_STDIN=path/to/testu01_stdin32 tools/run_testu01.sh smallcrush 1073741824 +``` + +Standard profile: + +```bash +tools/run_practrand.sh 1073741824 +VARIANT=fast8x tools/run_practrand.sh 10737418240 +tools/run_dieharder.sh 1099511627776 +TESTU01_STDIN=path/to/testu01_stdin32 tools/run_testu01.sh crush 1073741824 +``` + +Long profile: + +```bash +VARIANT=fast8x tools/run_practrand.sh 107374182400 +UNBOUNDED=1 VARIANT=fast8x tools/run_smokerand.sh full 1099511627776 +TESTU01_STDIN=path/to/testu01_stdin32 tools/run_testu01.sh bigcrush 1099511627776 +``` +Each run should record the variant, seed, byte count, command line, tool +version, machine, compiler, and whether the status is PASS, WARN, FAIL, +BLOCKED, or NOT_RUN. diff --git a/tools/run_testu01.sh b/tools/run_testu01.sh index 6eefce1..a0b0067 100755 --- a/tools/run_testu01.sh +++ b/tools/run_testu01.sh @@ -4,9 +4,9 @@ set -euo pipefail BATTERY="${1:-smallcrush}" BYTES="${2:-1073741824}" SEED="${SEED:-123}" +VARIANT="${VARIANT:-baseline}" TESTU01_STDIN="${TESTU01_STDIN:-testu01_stdin32}" make -C c all >/dev/null -c/build/tricube stream --seed "$SEED" --bytes "$BYTES" --out - \ +c/build/tricube stream --seed "$SEED" --bytes "$BYTES" --out - --variant "$VARIANT" \ | "$TESTU01_STDIN" "$BATTERY" -