"""Tests for ``muse verify-commit`` — verify Ed25519 signatures on commits. Coverage tiers: - Unit: _verify_one (valid sig, tampered commit_id, missing sig, missing public key, bit-flip in signature, unsigned commit with --strict) - Integration: commit --sign → verify-commit succeeds; tamper stored record → verify fails; batch verify (multiple commit IDs); --json schema; text output format; unsigned commit exits nonzero with --strict; unsigned commit exits 0 without; nonexistent commit_id; --check-key-status returns unknown when no hub - Security: bit-flip in signature; canonical message tamper; ANSI in commit ref rejected - Stress: 100 signed commits verified correctly; key_status_cache deduplicates calls """ from __future__ import annotations import base64 import datetime import hashlib import json import pathlib from unittest.mock import patch import pytest from tests.cli_test_helper import CliRunner from muse.core.object_store import write_object from muse.core.provenance import ( encode_public_key, provenance_payload, sign_commit_record, verify_commit_ed25519, ) from muse.core.snapshot import compute_commit_id, compute_snapshot_id from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot from muse.core._types import Manifest runner = CliRunner() _REPO_ID = "verify-commit-test" _counter = 0 # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _sha(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def _init_repo(path: pathlib.Path) -> pathlib.Path: muse = path / ".muse" for d in ("commits", "snapshots", "objects", "refs/heads", "code"): (muse / d).mkdir(parents=True, exist_ok=True) (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (muse / "repo.json").write_text( json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8" ) return path def _env(repo: pathlib.Path) -> dict[str, str]: return {"MUSE_REPO_ROOT": str(repo)} def _make_key(): """Generate a fresh Ed25519 private key.""" from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey return Ed25519PrivateKey.generate() def _commit_files( root: pathlib.Path, files: dict[str, bytes], branch: str = "main", message: str | None = None, sign: bool = False, private_key=None, agent_id: str = "test-agent", ) -> tuple[str, CommitRecord]: """Create a commit; optionally sign it. Returns (commit_id, CommitRecord).""" global _counter _counter += 1 manifest: Manifest = {} for rel_path, content in files.items(): obj_id = _sha(content) write_object(root, obj_id, content) manifest[rel_path] = obj_id abs_path = root / rel_path abs_path.parent.mkdir(parents=True, exist_ok=True) abs_path.write_bytes(content) snap_id = compute_snapshot_id(manifest) write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest)) committed_at = datetime.datetime.now(datetime.timezone.utc) ref_path = root / ".muse" / "refs" / "heads" / branch parent_id = ref_path.read_text(encoding="utf-8").strip() if ref_path.exists() else None parents = [parent_id] if parent_id else [] msg = message or f"commit {_counter}" commit_id = compute_commit_id(parents, snap_id, msg, committed_at.isoformat()) sig = "" pub_b64 = "" key_id = "" if sign and private_key is not None: result = sign_commit_record( commit_id, agent_id, private_key, committed_at=committed_at.isoformat(), ) if result: sig, pub_b64, key_id = result record = CommitRecord( commit_id=commit_id, repo_id=_REPO_ID, branch=branch, snapshot_id=snap_id, message=msg, committed_at=committed_at, parent_commit_id=parent_id, agent_id=agent_id if sign else "", signature=sig, signer_public_key=pub_b64, signer_key_id=key_id, ) write_commit(root, record) ref_path.write_text(commit_id, encoding="utf-8") return commit_id, record def _invoke(repo: pathlib.Path, *args: str): from muse.cli.app import main as cli return runner.invoke(cli, ["verify-commit", *args], env=_env(repo)) def _force_write_commit(root: pathlib.Path, record: CommitRecord) -> None: """Overwrite a commit file unconditionally (bypasses write_commit idempotency).""" import msgpack commit_file = root / ".muse" / "commits" / f"{record.commit_id}.msgpack" commit_file.write_bytes(msgpack.packb(record.to_dict(), use_bin_type=True)) # --------------------------------------------------------------------------- # Unit — _verify_one # --------------------------------------------------------------------------- def test_verify_one_valid_signature(tmp_path: pathlib.Path) -> None: from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) key = _make_key() commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) result = _verify_one(root, commit_id) assert result["valid"] is True assert result["commit_id"] == commit_id assert len(result["key_id"]) > 0 def test_verify_one_unsigned_commit(tmp_path: pathlib.Path) -> None: from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=False) result = _verify_one(root, commit_id) assert result["valid"] is False assert result["signer"] == "" def test_verify_one_tampered_commit_id(tmp_path: pathlib.Path) -> None: """Querying a non-existent commit_id returns valid=False.""" from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) fake_id = "a" * 64 result = _verify_one(root, fake_id) assert result["valid"] is False def test_verify_one_missing_public_key(tmp_path: pathlib.Path) -> None: """A commit with a signature but no public key returns valid=False.""" from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) key = _make_key() commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) # Force-overwrite the commit with signer_public_key stripped tampered = CommitRecord( commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch, snapshot_id=record.snapshot_id, message=record.message, committed_at=record.committed_at, parent_commit_id=record.parent_commit_id, agent_id=record.agent_id, signature=record.signature, signer_public_key="", # stripped signer_key_id=record.signer_key_id, ) _force_write_commit(root, tampered) result = _verify_one(root, commit_id) assert result["valid"] is False def test_verify_one_bit_flip_in_signature(tmp_path: pathlib.Path) -> None: """A single bit flip in the stored signature must invalidate it.""" from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) key = _make_key() commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) # Flip one byte in the base64 signature sig_bytes = base64.urlsafe_b64decode(record.signature + "==") flipped = bytes([sig_bytes[0] ^ 0xFF]) + sig_bytes[1:] bad_sig = base64.urlsafe_b64encode(flipped).rstrip(b"=").decode() tampered = CommitRecord( commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch, snapshot_id=record.snapshot_id, message=record.message, committed_at=record.committed_at, parent_commit_id=record.parent_commit_id, agent_id=record.agent_id, signature=bad_sig, signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id, ) _force_write_commit(root, tampered) result = _verify_one(root, commit_id) assert result["valid"] is False def test_verify_one_key_status_unknown_without_hub(tmp_path: pathlib.Path) -> None: """Without a hub configured, key_status must be 'unknown'.""" from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) key = _make_key() commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) result = _verify_one(root, commit_id, check_key_status=True, hub_url=None) assert result["key_status"] == "unknown" def test_verify_one_json_schema_keys(tmp_path: pathlib.Path) -> None: from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) key = _make_key() commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) result = _verify_one(root, commit_id) assert "commit_id" in result assert "valid" in result assert "signer" in result assert "key_id" in result assert "signed_at" in result assert "key_status" in result # --------------------------------------------------------------------------- # Integration — CLI # --------------------------------------------------------------------------- def test_verify_commit_valid_exits_zero(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) key = _make_key() commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) result = _invoke(root, commit_id) assert result.exit_code == 0 def test_verify_commit_json_output(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) key = _make_key() commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) result = _invoke(root, commit_id, "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert data["commit_id"] == commit_id assert data["valid"] is True assert "signer" in data assert "key_id" in data assert "key_status" in data def test_verify_commit_invalid_exits_nonzero(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) key = _make_key() commit_id, record = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) sig_bytes = base64.urlsafe_b64decode(record.signature + "==") flipped = bytes([sig_bytes[0] ^ 0xFF]) + sig_bytes[1:] bad_sig = base64.urlsafe_b64encode(flipped).rstrip(b"=").decode() tampered = CommitRecord( commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch, snapshot_id=record.snapshot_id, message=record.message, committed_at=record.committed_at, parent_commit_id=record.parent_commit_id, agent_id=record.agent_id, signature=bad_sig, signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id, ) _force_write_commit(root, tampered) result = _invoke(root, commit_id, "--json") assert result.exit_code != 0 data = json.loads(result.stdout) assert data["valid"] is False def test_verify_commit_unsigned_no_strict_exits_zero(tmp_path: pathlib.Path) -> None: """Unsigned commit without --strict: exits 0 but valid=False in output.""" root = _init_repo(tmp_path) commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=False) result = _invoke(root, commit_id, "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert data["valid"] is False def test_verify_commit_unsigned_strict_exits_nonzero(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=False) result = _invoke(root, commit_id, "--strict") assert result.exit_code != 0 def test_verify_commit_head_shorthand(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) key = _make_key() _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) result = _invoke(root, "HEAD", "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert data["valid"] is True def test_verify_commit_nonexistent_ref_exits_nonzero(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) result = _invoke(root, "a" * 64) assert result.exit_code != 0 def test_verify_commit_text_output_format(tmp_path: pathlib.Path) -> None: """Text output: ' '""" root = _init_repo(tmp_path) key = _make_key() commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) result = _invoke(root, commit_id) assert result.exit_code == 0 assert commit_id[:8] in result.stdout def test_verify_commit_batch_all_valid(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) key = _make_key() ids = [] for i in range(3): cid, _ = _commit_files(root, {"a.py": f"x = {i}\n".encode()}, sign=True, private_key=key) ids.append(cid) result = _invoke(root, *ids, "--json") # Batch: stdout is newline-separated JSON objects or a JSON array assert result.exit_code == 0 def test_verify_commit_batch_one_invalid_exits_nonzero(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) key = _make_key() cid1, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) cid2, _ = _commit_files(root, {"a.py": b"x = 2\n"}, sign=False) result = _invoke(root, cid1, cid2, "--strict") assert result.exit_code != 0 def test_verify_commit_check_key_status_unknown_no_hub(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) key = _make_key() commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) result = _invoke(root, commit_id, "--check-key-status", "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert data["key_status"] == "unknown" # --------------------------------------------------------------------------- # Security # --------------------------------------------------------------------------- def test_verify_commit_ansi_in_ref_rejected(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"x = 1\n"}) result = _invoke(root, "\x1b[31mbad\x1b[0m") assert result.exit_code != 0 def test_verify_commit_canonical_message_tamper_detected(tmp_path: pathlib.Path) -> None: """Changing agent_id in the stored record must invalidate the signature.""" from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) key = _make_key() commit_id, record = _commit_files( root, {"a.py": b"x = 1\n"}, sign=True, private_key=key, agent_id="agent-A" ) # Overwrite with a different agent_id — canonical message will differ tampered = CommitRecord( commit_id=record.commit_id, repo_id=record.repo_id, branch=record.branch, snapshot_id=record.snapshot_id, message=record.message, committed_at=record.committed_at, parent_commit_id=record.parent_commit_id, agent_id="agent-B", # tampered signature=record.signature, signer_public_key=record.signer_public_key, signer_key_id=record.signer_key_id, ) _force_write_commit(root, tampered) result = _verify_one(root, commit_id) assert result["valid"] is False # --------------------------------------------------------------------------- # Stress — 100 signed commits # --------------------------------------------------------------------------- def test_verify_commit_100_signed_commits(tmp_path: pathlib.Path) -> None: """100 signed commits all verify correctly.""" from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) key = _make_key() for i in range(100): commit_id, _ = _commit_files( root, {"f.py": f"v = {i}\n".encode()}, sign=True, private_key=key ) result = _verify_one(root, commit_id) assert result["valid"] is True, f"commit {i} failed" def test_verify_commit_key_status_cache_deduplicates(tmp_path: pathlib.Path) -> None: """key_status_cache is populated on first lookup and reused on subsequent ones.""" from muse.cli.commands.verify_commit import _verify_one root = _init_repo(tmp_path) key = _make_key() call_count = 0 def mock_check(hub_url, key_id): nonlocal call_count call_count += 1 return "active" cache: dict[str, str] = {} with patch("muse.cli.commands.verify_commit._fetch_key_status", side_effect=mock_check): commit_id, _ = _commit_files(root, {"a.py": b"x = 1\n"}, sign=True, private_key=key) _verify_one(root, commit_id, check_key_status=True, hub_url="http://fake", key_status_cache=cache) _verify_one(root, commit_id, check_key_status=True, hub_url="http://fake", key_status_cache=cache) assert call_count == 1, "cache must prevent duplicate key status lookups"