diff --git a/scripts/check_rust_release_manifest.py b/scripts/check_rust_release_manifest.py index cacc09387..fb8b72d2f 100644 --- a/scripts/check_rust_release_manifest.py +++ b/scripts/check_rust_release_manifest.py @@ -2469,6 +2469,7 @@ def run_fixtures_mode() -> list[Failure]: "bytes": 6, } }, + "unsigned_members": {"parakeet-helper": "d" * 64}, "tools": fixture_native_tools("macos-arm64"), "signing_mode": "signed-verified", "signing": { @@ -2499,6 +2500,7 @@ def run_fixtures_mode() -> list[Failure]: } for name in CORE_SCRIPT_NAMES }, + "unsigned_members": {name: "f" * 64 for name in CORE_SCRIPT_NAMES}, "tools": fixture_native_tools("macos-arm64"), "signing_mode": "signed-verified", "signing": { @@ -2543,6 +2545,10 @@ def run_fixtures_mode() -> list[Failure]: "bytes": 6, }, }, + "unsigned_members": { + fixture_speakers_analyze_executable: "b" * 64, + fixture_speakers_analyze_dylib: "c" * 64, + }, "tools": fixture_native_tools("macos-arm64"), "signing_mode": "signed-verified", "signing": { diff --git a/scripts/check_wheel_contents.py b/scripts/check_wheel_contents.py index 8569d584c..83f4fcf0e 100644 --- a/scripts/check_wheel_contents.py +++ b/scripts/check_wheel_contents.py @@ -273,12 +273,22 @@ def _failure( ) -def _core_rebuild_command(platform_tuple: CorePlatform) -> str: - if platform_tuple[0] == "linux": +def _core_rebuild_command(platform_tuple: CorePlatform | None) -> str: + if platform_tuple is not None and platform_tuple[0] == "linux": return "bash scripts/release.sh --dry-run-linux" return "bash scripts/release.sh --candidate" +def _speakers_analyze_rebuild_command(platform_tuple: CorePlatform | None) -> str: + if platform_tuple is None: + return "bash scripts/release.sh --candidate" + if platform_tuple[0] == "darwin": + return "make wheel-macos" + if platform_tuple[1] == "aarch64": + return "make wheel-speakers-analyze-linux-aarch64" + return "make wheel-speakers-analyze-linux-x86_64" + + def check_base_wheel(path: Path, max_bytes: int) -> list[str]: errors: list[str] = [] platform_wheel = not path.name.endswith("-any.whl") @@ -985,11 +995,7 @@ def check_core_wheel(path: Path, max_bytes: int) -> list[str]: with zipfile.ZipFile(path) as wheel: expected_members = _core_expected_members(path) names = set(wheel.namelist()) - repair = ( - _core_rebuild_command(platform_tuple) - if platform_tuple is not None - else "bash scripts/release.sh --candidate" - ) + repair = _core_rebuild_command(platform_tuple) if names != expected_members: errors.append( _failure( @@ -1192,6 +1198,9 @@ def _check_speakers_analyze_elf_binary( def check_speakers_analyze_wheel(path: Path) -> list[str]: errors: list[str] = [] + tag = _core_wheel_tag(path) + platform_tuple = SPEAKERS_ANALYZE_TAG_PLATFORMS.get(tag) + repair = _speakers_analyze_rebuild_command(platform_tuple) size = path.stat().st_size if size > MAX_SPEAKERS_ANALYZE_WHEEL_BYTES: errors.append( @@ -1200,11 +1209,9 @@ def check_speakers_analyze_wheel(path: Path) -> list[str]: "speakers analyze wheel is too large", expected=f"<= {MAX_SPEAKERS_ANALYZE_WHEEL_BYTES} bytes", actual=str(size), - repair="make wheel-speakers-analyze-linux-x86_64", + repair=repair, ) ) - tag = _core_wheel_tag(path) - platform_tuple = SPEAKERS_ANALYZE_TAG_PLATFORMS.get(tag) if platform_tuple is None: errors.append( _failure( @@ -1214,7 +1221,7 @@ def check_speakers_analyze_wheel(path: Path) -> list[str]: sorted(SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS.values()) ), actual=tag, - repair="make wheel-speakers-analyze-linux-x86_64", + repair=repair, ) ) return errors @@ -1233,7 +1240,7 @@ def check_speakers_analyze_wheel(path: Path) -> list[str]: "speakers analyze wheel member set is wrong", expected=", ".join(sorted(expected_members)), actual=", ".join(sorted(names)) or "", - repair="make wheel-speakers-analyze-linux-x86_64", + repair=repair, ) ) provider_members = sorted( @@ -1261,7 +1268,7 @@ def check_speakers_analyze_wheel(path: Path) -> list[str]: "speakers analyze binary member count is wrong", expected=f"exactly one {binary_member}", actual=str(len(binary_infos)), - repair="make wheel-speakers-analyze-linux-x86_64", + repair=repair, ) ) else: @@ -1273,7 +1280,7 @@ def check_speakers_analyze_wheel(path: Path) -> list[str]: "speakers analyze binary is not executable", expected="executable mode bit set", actual=oct(mode), - repair="make wheel-speakers-analyze-linux-x86_64", + repair=repair, ) ) @@ -1282,7 +1289,11 @@ def check_speakers_analyze_wheel(path: Path) -> list[str]: except KeyError: library_content = b"" actual_library_sha = hashlib.sha256(library_content).hexdigest() - if actual_library_sha != spec.runtime_sha256: + # macOS signs the bundled dylib after staging, which rewrites the bytes; + # the pre-signing digest is unrecoverable from the signed Mach-O. The + # upstream ORT pin is therefore carried by + # record_macos_native_wheel.validate_macos_native_record instead. + if platform_tuple[0] == "linux" and actual_library_sha != spec.runtime_sha256: errors.append( _failure( path.name, diff --git a/scripts/record_macos_native_wheel.py b/scripts/record_macos_native_wheel.py index 3813da370..c290ff018 100644 --- a/scripts/record_macos_native_wheel.py +++ b/scripts/record_macos_native_wheel.py @@ -26,7 +26,6 @@ from scripts.check_wheel_contents import ( PARAKEET_HELPER_MEMBER, SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR, SPEAKERS_ANALYZE_SCRIPT_NAMES, - SPEAKERS_ANALYZE_TARGETS, core_wheel_script_members, ) from scripts.release_digest import file_sha256_size @@ -40,6 +39,7 @@ from scripts.release_tool_pins import ( PYTHON_MACOS_VERSION, tool_value_matches_pin, ) +from scripts.stage_speakers_analyze_runtime import TARGETS as SPEAKERS_ANALYZE_TARGETS NativeRole = Literal["root", "core", "speakers-analyze"] @@ -60,6 +60,7 @@ TOP_LEVEL_KEYS = frozenset( "wheel", "member", "members", + "unsigned_members", "tools", "signing_mode", "signing", @@ -70,7 +71,7 @@ SIGNING_KEYS = frozenset( ("signer_pinned", "team_pinned", "hardened_runtime", "trusted_timestamp") ) FACT_KEYS = SIGNING_KEYS | frozenset( - ("signed_binary_sha256", "notarization_status", "tools") + ("signed_binary_sha256", "unsigned_binary_sha256", "notarization_status", "tools") ) FACT_TOOL_KEYS = frozenset(("xcode", "swift", "codesign", "notarytool")) @@ -105,7 +106,7 @@ def _members_for_role( for info in wheel.infolist() if info.filename.endswith(f".data/scripts/{SPEAKERS_ANALYZE_SCRIPT_NAMES[0]}") ] - dylib_name = SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name + dylib_name = _speakers_analyze_dylib_name() dylib_suffix = ( f".data/{SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/{dylib_name}" ) @@ -125,7 +126,7 @@ def _expected_member_path(role: NativeRole) -> str: return PARAKEET_HELPER_MEMBER if role == "core": return ", ".join(f".data/scripts/{name}" for name in CORE_SCRIPT_NAMES) - dylib_name = SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name + dylib_name = _speakers_analyze_dylib_name() return ( f".data/scripts/{SPEAKERS_ANALYZE_SCRIPT_NAMES[0]} and " f".data/{SPEAKERS_ANALYZE_RUNTIME_INSTALL_DIR.as_posix()}/{dylib_name}" @@ -150,6 +151,10 @@ def _primary_member_name(role: NativeRole) -> str: return SPEAKERS_ANALYZE_SCRIPT_NAMES[0] +def _speakers_analyze_dylib_name() -> str: + return SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name + + def _read_members( wheel_path: Path, role: NativeRole ) -> dict[str, tuple[str, bytes]] | list[Failure]: @@ -222,16 +227,20 @@ def _validate_facts(facts: Mapping[str, Any]) -> list[Failure]: repair="python3 scripts/check_rust_release_manifest.py", ) ) - signed = facts.get("signed_binary_sha256") - if not isinstance(signed, str) or not SHA256_RE.fullmatch(signed): - failures.append( - _failure( - "macOS signed binary hash is invalid", - expected="lowercase SHA-256", - actual=str(signed), - repair="python3 scripts/check_rust_release_manifest.py", + for key, label in ( + ("signed_binary_sha256", "signed"), + ("unsigned_binary_sha256", "unsigned"), + ): + digest = facts.get(key) + if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest): + failures.append( + _failure( + f"macOS {label} binary hash is invalid", + expected="lowercase SHA-256", + actual=str(digest), + repair="python3 scripts/check_rust_release_manifest.py", + ) ) - ) for key in SIGNING_KEYS: if facts.get(key) is not True: failures.append( @@ -452,6 +461,10 @@ def build_macos_native_record( }, "member": member_payloads[primary_name], "members": {key: member_payloads[key] for key in sorted(member_payloads)}, + "unsigned_members": { + key: facts_by_member[key]["unsigned_binary_sha256"] + for key in sorted(member_payloads) + }, "tools": { "python": python_version, "xcode": tools["xcode"], @@ -614,6 +627,65 @@ def validate_macos_native_record( repair="python3 scripts/check_rust_release_manifest.py", ) ) + unsigned_members = record.get("unsigned_members") + record_members = record.get("members") + if not isinstance(unsigned_members, Mapping): + failures.append( + _failure( + "macOS native record unsigned members are invalid", + expected="unsigned_members object keyed like members", + actual=type(unsigned_members).__name__, + repair="python3 scripts/check_rust_release_manifest.py", + ) + ) + elif not isinstance(record_members, Mapping) or set(unsigned_members) != set( + record_members + ): + expected_names = ( + ", ".join(sorted(str(key) for key in record_members)) + if isinstance(record_members, Mapping) + else "" + ) + failures.append( + _failure( + "macOS native record unsigned member set is wrong", + expected=expected_names, + actual=", ".join(sorted(str(key) for key in unsigned_members)) + or "", + repair="python3 scripts/check_rust_release_manifest.py", + ) + ) + else: + invalid_unsigned = [ + str(name) + for name, digest in unsigned_members.items() + if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest) + ] + if invalid_unsigned: + failures.append( + _failure( + "macOS native record unsigned member hash is invalid", + expected="lowercase SHA-256 for every unsigned member", + actual=", ".join(sorted(invalid_unsigned)), + repair="python3 scripts/check_rust_release_manifest.py", + ) + ) + if role == "speakers-analyze": + dylib_name = _speakers_analyze_dylib_name() + expected_unsigned = SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_sha256 + actual_unsigned = unsigned_members.get(dylib_name) + if actual_unsigned != expected_unsigned: + failures.append( + _failure( + ( + "macOS speakers-analyze unsigned ONNX Runtime hash " + "does not match staged pin" + ), + expected=expected_unsigned, + actual=str(actual_unsigned), + repair="python3 scripts/stage_speakers_analyze_runtime.py --target macos-arm64", + ) + ) failures.extend(validate_public_evidence_tree("macos_native_record", record)) return failures diff --git a/scripts/sign-and-notarize-helper.sh b/scripts/sign-and-notarize-helper.sh index 950e91a89..fc92cb8e2 100755 --- a/scripts/sign-and-notarize-helper.sh +++ b/scripts/sign-and-notarize-helper.sh @@ -100,6 +100,8 @@ if [ "$NOTARYTOOL_OUTPUT" != "$NOTARYTOOL_PIN" ]; then exit 1 fi +UNSIGNED_BINARY_SHA256="$(shasum -a 256 "$BINARY" | awk '{print $1}')" + echo "==> codesigning $BINARY with repository-pinned identity" >&2 "$CODESIGN_BIN" --force --options runtime --timestamp \ --keychain "$NOTARY_KEYCHAIN" \ @@ -154,6 +156,7 @@ SIGNER_PINNED="$SIGNER_PINNED" \ TEAM_PINNED="$TEAM_PINNED" \ HARDENED_RUNTIME="$HARDENED_RUNTIME" \ TRUSTED_TIMESTAMP="$TRUSTED_TIMESTAMP" \ +UNSIGNED_BINARY_SHA256="$UNSIGNED_BINARY_SHA256" \ NOTARIZATION_STATUS="$NOTARIZATION_STATUS" \ XCODE_PIN="$XCODE_PIN" \ SWIFT_FIRST_LINE="$SWIFT_FIRST_LINE" \ @@ -165,6 +168,7 @@ import os payload = { "signed_binary_sha256": os.environ["SIGNED_BINARY_SHA256"], + "unsigned_binary_sha256": os.environ["UNSIGNED_BINARY_SHA256"], "signer_pinned": os.environ["SIGNER_PINNED"] == "true", "team_pinned": os.environ["TEAM_PINNED"] == "true", "hardened_runtime": os.environ["HARDENED_RUNTIME"] == "true", diff --git a/tests/helpers/release_candidate_fixtures.py b/tests/helpers/release_candidate_fixtures.py index 2b290c685..4fd7942b1 100644 --- a/tests/helpers/release_candidate_fixtures.py +++ b/tests/helpers/release_candidate_fixtures.py @@ -320,8 +320,10 @@ def macos_wheel_names() -> tuple[str, str, str]: def _facts(content: bytes) -> dict[str, Any]: + digest = hashlib.sha256(content).hexdigest() return { - "signed_binary_sha256": hashlib.sha256(content).hexdigest(), + "signed_binary_sha256": digest, + "unsigned_binary_sha256": digest, "signer_pinned": True, "team_pinned": True, "hardened_runtime": True, diff --git a/tests/test_check_release_preflight.py b/tests/test_check_release_preflight.py index 1aceef675..c5517105f 100644 --- a/tests/test_check_release_preflight.py +++ b/tests/test_check_release_preflight.py @@ -555,6 +555,7 @@ def test_collect_lane_tools_normalizes_macos_observations() -> None: def _native_record(role: str) -> dict[str, object]: return { "role": role, + "unsigned_members": {}, "signing_mode": pins.MACOS_SIGNING_MODE, "signing": { "signer_pinned": True, diff --git a/tests/test_check_wheel_contents.py b/tests/test_check_wheel_contents.py index 4d569463d..374b44c03 100644 --- a/tests/test_check_wheel_contents.py +++ b/tests/test_check_wheel_contents.py @@ -45,8 +45,14 @@ def _write_member( wheel.writestr(info, content) -def _patch_speakers_fixture_hashes(monkeypatch) -> None: - spec = checker.SPEAKERS_ANALYZE_TARGETS["linux-x86_64"] +def _patch_speakers_fixture_hashes( + monkeypatch, + *, + target: str = "linux-x86_64", + runtime_bytes: bytes = SPEAKERS_LIBRARY, + patch_runtime: bool = True, +) -> None: + spec = checker.SPEAKERS_ANALYZE_TARGETS[target] notices = ( replace( spec.notices[0], @@ -57,15 +63,13 @@ def _patch_speakers_fixture_hashes(monkeypatch) -> None: sha256=checker.hashlib.sha256(SPEAKERS_THIRD_PARTY_NOTICE).hexdigest(), ), ) - monkeypatch.setitem( - checker.SPEAKERS_ANALYZE_TARGETS, - "linux-x86_64", - replace( - spec, - runtime_sha256=checker.hashlib.sha256(SPEAKERS_LIBRARY).hexdigest(), - notices=notices, - ), - ) + replacement = replace(spec, notices=notices) + if patch_runtime: + replacement = replace( + replacement, + runtime_sha256=checker.hashlib.sha256(runtime_bytes).hexdigest(), + ) + monkeypatch.setitem(checker.SPEAKERS_ANALYZE_TARGETS, target, replacement) def test_script_runs_without_site_packages_from_outside_repo(tmp_path: Path) -> None: @@ -252,6 +256,44 @@ def test_speakers_analyze_wheel_validator_accepts_pinned_layout( assert checker.check_speakers_analyze_wheel(wheel) == [] +def test_speakers_analyze_macos_signed_dylib_bytes_are_allowed( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes( + monkeypatch, + target="macos-arm64", + patch_runtime=False, + ) + wheel = write_speakers_analyze_wheel( + tmp_path, + tag=checker.SOLSTONE_CORE_SPEAKERS_ANALYZE_PLATFORM_TAGS[("darwin", "arm64")], + library=minimal_macho(checker.CPU_TYPE_ARM64), + license_notice=SPEAKERS_LICENSE, + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + ) + + assert checker.check_speakers_analyze_wheel(wheel) == [] + + +def test_speakers_analyze_linux_rejects_substituted_runtime_library( + tmp_path: Path, monkeypatch +) -> None: + _patch_speakers_fixture_hashes(monkeypatch) + wheel = write_speakers_analyze_wheel( + tmp_path, + library=b"substituted libonnxruntime.so.1 GLIBC_2.27\n", + license_notice=SPEAKERS_LICENSE, + third_party_notice=SPEAKERS_THIRD_PARTY_NOTICE, + ) + + errors = checker.check_speakers_analyze_wheel(wheel) + + assert any( + "speakers analyze ONNX Runtime library digest mismatch" in error + for error in errors + ) + + def test_speakers_analyze_wheel_validator_requires_exact_member_set( tmp_path: Path, monkeypatch ) -> None: diff --git a/tests/test_release_candidate_driver.py b/tests/test_release_candidate_driver.py index 93c759760..bede9aa41 100644 --- a/tests/test_release_candidate_driver.py +++ b/tests/test_release_candidate_driver.py @@ -808,6 +808,37 @@ def _differing_payload_paths( return set() if first == second else {path} +def _macos_release_dir_from_host_result( + tmp_path: Path, host_result: BuildHostResult +) -> Path: + release_dir = tmp_path / "release-dir" + release_dir.mkdir() + for wheel in host_result.macos_wheels: + shutil.copy2(wheel, release_dir / wheel.name) + return release_dir + + +def _native_record_payloads(host_result: BuildHostResult) -> list[dict[str, Any]]: + return [ + json.loads(path.read_text(encoding="utf-8")) + for path in host_result.native_records + ] + + +def _macos_revalidation_inputs( + tmp_path: Path, +) -> tuple[Path, list[dict[str, Any]]]: + host_result = _write_macos_host_outputs(tmp_path / "host") + return ( + _macos_release_dir_from_host_result(tmp_path, host_result), + _native_record_payloads(host_result), + ) + + +def _record_by_role(records: Sequence[dict[str, Any]], role: str) -> dict[str, Any]: + return next(record for record in records if record.get("role") == role) + + def test_fake_all_host_candidate_and_recovery_are_deterministic( tmp_path: Path, ) -> None: @@ -883,6 +914,65 @@ def test_fake_all_host_candidate_and_recovery_are_deterministic( assert recovered.bundle_digest == first.bundle_digest +def test_revalidate_macos_wheels_accepts_matching_unsigned_speakers_pin( + tmp_path: Path, +) -> None: + release_dir, records = _macos_revalidation_inputs(tmp_path) + + driver._revalidate_macos_wheels( + release_dir, + records, + source_commit=SOURCE_COMMIT, + core_lock_sha256=LOCK_SHA, + ) + + +def test_revalidate_macos_wheels_rejects_unsigned_speakers_dylib_pin_mismatch( + tmp_path: Path, +) -> None: + release_dir, records = _macos_revalidation_inputs(tmp_path) + speakers = _record_by_role(records, "speakers-analyze") + dylib_name = wheel_checker.SPEAKERS_ANALYZE_TARGETS[ + "macos-arm64" + ].runtime_staged_name + speakers["unsigned_members"][dylib_name] = "0" * 64 + + with pytest.raises(driver.DriverError) as exc: + driver._revalidate_macos_wheels( + release_dir, + records, + source_commit=SOURCE_COMMIT, + core_lock_sha256=LOCK_SHA, + ) + + assert any( + failure.error + == "macOS speakers-analyze unsigned ONNX Runtime hash does not match staged pin" + for failure in exc.value.failures + ) + + +def test_revalidate_macos_wheels_rejects_unsigned_member_set_mismatch( + tmp_path: Path, +) -> None: + release_dir, records = _macos_revalidation_inputs(tmp_path) + speakers = _record_by_role(records, "speakers-analyze") + speakers["unsigned_members"].pop(wheel_checker.SPEAKERS_ANALYZE_SCRIPT_NAMES[0]) + + with pytest.raises(driver.DriverError) as exc: + driver._revalidate_macos_wheels( + release_dir, + records, + source_commit=SOURCE_COMMIT, + core_lock_sha256=LOCK_SHA, + ) + + assert any( + failure.error == "macOS native record unsigned member set is wrong" + for failure in exc.value.failures + ) + + def test_recovery_uses_explicit_selector_and_preserves_retained_bytes( tmp_path: Path, ) -> None: diff --git a/tests/test_release_ledger.py b/tests/test_release_ledger.py index fe9cc4b13..181af4366 100644 --- a/tests/test_release_ledger.py +++ b/tests/test_release_ledger.py @@ -13,6 +13,7 @@ import pytest import scripts.check_release_preflight as preflight import scripts.check_rust_release_manifest as checker +import scripts.release_candidate_driver as driver import scripts.release_ledger as ledger import scripts.release_tool_pins as pins from scripts.build_nvattest_authority import render_nvattest_authority_json @@ -211,6 +212,7 @@ def _native(role: str, wheel_name: str, member_path: str) -> dict: "wheel": {"name": wheel_name, "sha256": "e" * 64, "bytes": 12}, "member": {"path": member_path, "sha256": "f" * 64, "bytes": 6}, "members": members, + "unsigned_members": {name: "f" * 64 for name in members}, "tools": { "python": pins.PYTHON_MACOS_VERSION, "xcode": pins.MACOS_XCODE_PIN, @@ -366,6 +368,48 @@ def _ledger_path(root: Path) -> Path: ) +def _contains_key(value: object, key: str) -> bool: + if isinstance(value, dict): + return key in value or any( + _contains_key(child, key) for child in value.values() + ) + if isinstance(value, list): + return any(_contains_key(child, key) for child in value) + return False + + +def test_unsigned_members_from_native_records_do_not_reach_retained_surfaces( + tmp_path: Path, +) -> None: + candidate = _candidate(tmp_path) + native_records = _native_records() + for record in native_records: + record["unsigned_members"] = { + name: str(member["sha256"]) for name, member in record["members"].items() + } + + payload = ledger.build_ledger( + version="1.2.3", + source_commit=SOURCE_COMMIT, + release_dir=candidate, + core_lock_path=_core_lock(tmp_path), + tool_evidence=_tool_evidence(), + policy_run=_policy(), + native_records=native_records, + models=_models(), + nvattest=_nvattest(), + ) + + assert not _contains_key(payload, "unsigned_members") + assert not any( + "unsigned_members" in name + for name in driver._expected_payload_file_names(include_models=False) + ) + for wheel_path in candidate.glob("*.whl"): + with zipfile.ZipFile(wheel_path) as wheel: + assert not any("unsigned_members" in name for name in wheel.namelist()) + + def _fixture_payload() -> dict: return json.loads(CANONICAL_LEDGER_V2_FIXTURE.read_text(encoding="utf-8")) diff --git a/tests/test_release_native_records.py b/tests/test_release_native_records.py index 2918652bf..abb384b77 100644 --- a/tests/test_release_native_records.py +++ b/tests/test_release_native_records.py @@ -8,6 +8,7 @@ import json import subprocess import sys import zipfile +from dataclasses import replace from pathlib import Path import pytest @@ -81,8 +82,10 @@ def _speakers_analyze_wheel( def _facts(content: bytes) -> dict: + digest = hashlib.sha256(content).hexdigest() return { - "signed_binary_sha256": hashlib.sha256(content).hexdigest(), + "signed_binary_sha256": digest, + "unsigned_binary_sha256": digest, "signer_pinned": True, "team_pinned": True, "hardened_runtime": True, @@ -103,6 +106,15 @@ def _facts_file(tmp_path: Path, facts: dict) -> Path: return path +def _patch_macos_runtime_pin(monkeypatch: pytest.MonkeyPatch, content: bytes) -> None: + spec = native.SPEAKERS_ANALYZE_TARGETS["macos-arm64"] + monkeypatch.setitem( + native.SPEAKERS_ANALYZE_TARGETS, + "macos-arm64", + replace(spec, runtime_sha256=hashlib.sha256(content).hexdigest()), + ) + + def _core_facts(content: bytes) -> dict: return {"members": {name: _facts(content) for name in CORE_SCRIPT_NAMES}} @@ -133,8 +145,9 @@ def test_native_record_cli_and_makefile_use_package_module() -> None: def test_exactly_three_role_records_are_written_and_not_interchangeable( - tmp_path: Path, + tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: + _patch_macos_runtime_pin(monkeypatch, b"dylib") root_wheel = _root_wheel(tmp_path, b"root") core_wheel = _core_wheel(tmp_path, b"core") speakers_wheel = _speakers_analyze_wheel( @@ -174,6 +187,7 @@ def test_exactly_three_role_records_are_written_and_not_interchangeable( SPEAKERS_ANALYZE_SCRIPT_NAMES[0], SPEAKERS_ANALYZE_TARGETS["macos-arm64"].runtime_staged_name, } + assert set(speakers["unsigned_members"]) == set(speakers["members"]) assert native.validate_macos_native_record( root, role="core", @@ -407,6 +421,11 @@ def test_signing_helper_removes_arbitrary_identity_override() -> None: def test_signing_helper_records_tool_observations_not_pin_constants() -> None: source = Path("scripts/sign-and-notarize-helper.sh").read_text(encoding="utf-8") + assert 'UNSIGNED_BINARY_SHA256="$(shasum -a 256 "$BINARY"' in source + assert source.index("UNSIGNED_BINARY_SHA256=") < source.index( + 'echo "==> codesigning $BINARY' + ) + assert '"unsigned_binary_sha256": os.environ["UNSIGNED_BINARY_SHA256"]' in source assert 'SWIFT_FIRST_LINE="$SWIFT_FIRST_LINE"' in source assert '"swift": os.environ["SWIFT_FIRST_LINE"]' in source assert 'NOTARYTOOL_OUTPUT="$NOTARYTOOL_OUTPUT"' in source