hyf

Context-aware query service for Radroots
git clone https://radroots.dev/git/hyf.git
Log | Files | Refs | README | LICENSE

commit 35d21546630f1cda8776a3a68044a101e865b15d
parent 0f0a526afe4d2d8a86d1bdf494f5d1afec0df992
Author: triesap <tyson@radroots.org>
Date:   Wed, 23 Sep 2026 19:50:36 +0000

H005A: complete the required fail-closed hasher controls

- Add a bounded primary/fallback hasher seam on the test-only digest helpers.
- Reject a declared input set with one missing input, never a partial digest.
- Reject present valid inputs with a failing hasher stage and no fallback.
- Prove a supported fallback returns the same checked digest as the default.

Diffstat:
Mtests/measurement_process_helper.mojo | 40+++++++++++++++++++++++++++++++---------
Mtests/test_measurement_contract.mojo | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 133 insertions(+), 9 deletions(-)

diff --git a/tests/measurement_process_helper.mojo b/tests/measurement_process_helper.mojo @@ -509,7 +509,10 @@ def _path_basename(path: String) -> String: def _checked_file_digests( - var files: List[String], mut guard: CleanupGuard + var files: List[String], + mut guard: CleanupGuard, + primary_hasher: String = "shasum", + fallback_hasher: String = "sha256sum", ) raises -> List[String]: """Per-file sha256 of explicit argv paths, fail-closed. @@ -519,6 +522,12 @@ def _checked_file_digests( an unavailable hasher raises instead of yielding a valid empty digest. This replaces the former status-masking shell pipeline whose final stage could succeed on empty input and report the empty-input digest as success. + + ``primary_hasher``/``fallback_hasher`` name the two checked stages. The + defaults are the supported host hashers; the parameters exist only as a + bounded test seam so the failed-hasher-stage and fallback controls can run + against present, valid regular-file inputs without altering live tools or + host settings (ADR-0021 MP01). """ if len(files) == 0: raise Error("measurement: refusing to digest an empty input list") @@ -528,14 +537,14 @@ def _checked_file_digests( for index in range(len(files)): shasum_args.append(files[index]) var out = run_capture( - "shasum", shasum_args^, MEASUREMENT_SAMPLE_DEADLINE_MS, guard + primary_hasher, shasum_args^, MEASUREMENT_SAMPLE_DEADLINE_MS, guard ) if out.exit_code != 0: var sum_args = List[String]() for index in range(len(files)): sum_args.append(files[index]) out = run_capture( - "sha256sum", sum_args^, MEASUREMENT_SAMPLE_DEADLINE_MS, guard + fallback_hasher, sum_args^, MEASUREMENT_SAMPLE_DEADLINE_MS, guard ) if out.exit_code != 0: raise Error( @@ -584,19 +593,26 @@ def sha256_text( def sha256_file_set( - label: String, var files: List[String], mut guard: CleanupGuard + label: String, + var files: List[String], + mut guard: CleanupGuard, + primary_hasher: String = "shasum", + fallback_hasher: String = "sha256sum", ) raises -> String: """Deterministic, path-independent digest of an explicit file set. Each declared file's exact content digest is checked first, then the manifest text ``<basename> <digest>`` (in declared order) is hashed, so the result depends only on the declared files' content and names, never on the - checkout location. + checkout location. ``primary_hasher``/``fallback_hasher`` are the bounded + test seam documented on ``_checked_file_digests``. """ var names = List[String]() for index in range(len(files)): names.append(_path_basename(files[index])) - var digests = _checked_file_digests(files^, guard) + var digests = _checked_file_digests( + files^, guard, primary_hasher, fallback_hasher + ) var canonical = "" for index in range(len(names)): canonical += names[index] + " " + digests[index] + "\n" @@ -673,7 +689,10 @@ def source_dirty_status( def tooling_manifest_sha256( - source_root: String, mut guard: CleanupGuard + source_root: String, + mut guard: CleanupGuard, + primary_hasher: String = "shasum", + fallback_hasher: String = "sha256sum", ) raises -> String: """Content digest of the measurement tooling that produced the evidence. @@ -682,10 +701,13 @@ def tooling_manifest_sha256( digest ties the emitted evidence to the reviewed tooling revision and its imported helper closure without requiring the working tree to be committed at capture time. A missing input or failed hasher stage is a bounded error, - never a valid empty digest. + never a valid empty digest. The optional hasher parameters are the bounded + test seam described on ``_checked_file_digests``. """ var files = measurement_tooling_files(source_root) - return sha256_file_set("tooling manifest", files^, guard) + return sha256_file_set( + "tooling manifest", files^, guard, primary_hasher, fallback_hasher + ) def source_identity( diff --git a/tests/test_measurement_contract.mojo b/tests/test_measurement_contract.mojo @@ -173,6 +173,43 @@ def _run_sh_failure( return "" +def _failing_hasher(root: String, mut guard: CleanupGuard) raises -> String: + """Owned hasher stand-in that fails without touching live tools or host + settings (ADR-0021 MP01). The script is created inside the isolated owned + temp root and exits 7 with an explicit stage error. + """ + var path = root + "/failing-hasher.sh" + Path(path).write_text( + "#!/bin/sh\nprintf 'failing hasher stage\\n' >&2\nexit 7\n" + ) + var chmod_args = List[String]() + chmod_args.append("+x") + chmod_args.append(path) + var chmodded = run_capture("chmod", chmod_args^, 20000, guard) + assert_equal(chmodded.exit_code, 0) + return path^ + + +def _copy_tooling_files( + root: String, mut guard: CleanupGuard, start: Int = 0 +) raises: + """Copy declared tooling inputs into an isolated owned ``tests`` root. + + ``start`` > 0 deliberately omits the leading declared inputs, producing a + present-but-partial input set for the missing-one-input control. + """ + var tests_dir = root + "/tests" + _ = std.os.makedirs(tests_dir, exist_ok=True) + var sources = measurement_tooling_files(".") + assert_true(len(sources) > start + 1) + var cp_args = List[String]() + for index in range(start, len(sources)): + cp_args.append(sources[index]) + cp_args.append(tests_dir) + var copied = run_capture("cp", cp_args^, 20000, guard) + assert_equal(copied.exit_code, 0) + + # ── Positive persistent measurement ───────────────────────────────────────── @@ -1007,6 +1044,71 @@ def test_measurement_tooling_manifest_rejects_missing_inputs() raises: guard.assert_clean() +def test_measurement_digest_rejects_missing_one_input() raises: + # ADR-0021 MP01: the expressly required missing-one-input control. Seven of + # the eight declared tooling inputs are present valid regular files and one + # declared path is absent, so the checked pipeline must fail instead of + # returning a digest for the partial present set. The all-input-missing and + # failed-Git controls do not cover this case. + var guard = CleanupGuard() + with SafeTempDir() as root: + _copy_tooling_files(root, guard, 1) + var expected = tooling_manifest_sha256(".", guard) + assert_equal(expected.byte_length(), 64) + var message = "" + var reported = "" + try: + reported = tooling_manifest_sha256(root, guard) + except e: + message = String(e) + assert_true(message.find("sha256") >= 0) + assert_true(message.find("e3b0c442") < 0) + # No partial-set digest may be returned as a valid identity. + assert_true(reported != expected) + assert_equal(reported, "") + guard.assert_clean() + + +def test_measurement_digest_rejects_total_hasher_failure() raises: + # ADR-0021 MP01: the expressly required failed-hasher-command control. All + # declared inputs are present valid regular files and both checked hasher + # stages fail, so the pipeline must reject. A different missing/unreadable + # input would not substitute for this control; the cause text must show the + # real nonzero hasher exit rather than only an unavailable command. + var guard = CleanupGuard() + with SafeTempDir() as root: + _copy_tooling_files(root, guard) + var failing = _failing_hasher(root, guard) + var message = "" + var reported = "" + try: + reported = tooling_manifest_sha256(root, guard, failing, failing) + except e: + message = String(e) + assert_true(message.find("sha256 unavailable") >= 0) + assert_true(message.find("exited=7") >= 0) + assert_true(message.find("e3b0c442") < 0) + assert_equal(reported, "") + guard.assert_clean() + + +def test_measurement_digest_fallback_hasher_checked_correct() raises: + # ADR-0021 MP01: when the primary hasher stage fails but the supported + # fallback succeeds, the result must be the same checked digest as the + # default path — the fallback may never silently report a partial or empty + # identity. + var guard = CleanupGuard() + with SafeTempDir() as root: + _copy_tooling_files(root, guard) + var failing = _failing_hasher(root, guard) + var expected = tooling_manifest_sha256(root, guard) + assert_equal(expected.byte_length(), 64) + var fallback = tooling_manifest_sha256(root, guard, failing) + assert_equal(fallback.byte_length(), 64) + assert_true(fallback == expected) + guard.assert_clean() + + def test_measurement_source_manifest_rejects_missing_repository() raises: # ADR-0020 MC02: a failed git stage must be an error, not an empty digest. var guard = CleanupGuard()