personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

build(transparency): gate minisign with a real signing round-trip

Replace the publish CLI check-minisign shim with a standalone install-check script that follows the scripts/check_*.py convention.

The gate generates a throwaway keypair, signs the committed transparency entry fixture, verifies it, tampers one byte and requires verification failure. Missing minisign is a loud failure with the install command, never a skip.

+234 -26
+5 -4
CONTRIBUTING.md
··· 13 13 - Git 14 14 - ripgrep (`rg`) 15 15 - ffmpeg for audio processing 16 + - minisign 0.12 for transparency signing checks 16 17 17 18 Linux is the primary development platform. macOS is supported. Source-checkout installs on Apple Silicon need Xcode command line tools to build the CoreML parakeet helper; packaged host installs (`uv tool install solstone-journal && uv tool install solstone`) on macOS 14 or newer ship the helper as a pre-built binary. 18 19 19 20 Fedora/RHEL: 20 21 21 22 ```bash 22 - sudo dnf install python3 git ripgrep ffmpeg pipewire gstreamer1-plugins-base gstreamer1-plugin-pipewire pulseaudio-utils 23 + sudo dnf install python3 git ripgrep ffmpeg minisign pipewire gstreamer1-plugins-base gstreamer1-plugin-pipewire pulseaudio-utils 23 24 curl -LsSf https://astral.sh/uv/install.sh | sh 24 25 ``` 25 26 26 27 Ubuntu/Debian: 27 28 28 29 ```bash 29 - sudo apt install python3 git ripgrep ffmpeg pipewire gstreamer1.0-tools gstreamer1.0-pipewire pulseaudio-utils 30 + sudo apt install python3 git ripgrep ffmpeg minisign pipewire gstreamer1.0-tools gstreamer1.0-pipewire pulseaudio-utils 30 31 curl -LsSf https://astral.sh/uv/install.sh | sh 31 32 ``` 32 33 33 34 Arch: 34 35 35 36 ```bash 36 - sudo pacman -S python git ripgrep ffmpeg pipewire gstreamer gst-plugin-pipewire libpulse 37 + sudo pacman -S python git ripgrep ffmpeg minisign pipewire gstreamer gst-plugin-pipewire libpulse 37 38 curl -LsSf https://astral.sh/uv/install.sh | sh 38 39 ``` 39 40 ··· 41 42 42 43 ```bash 43 44 xcode-select --install 44 - brew install python git ripgrep ffmpeg uv 45 + brew install python git ripgrep ffmpeg minisign uv 45 46 ``` 46 47 47 48 ## Source-checkout install
+1 -1
Makefile
··· 783 783 784 784 .PHONY: check-transparency-minisign 785 785 check-transparency-minisign: .installed 786 - $(VENV_BIN)/python scripts/transparency_publish.py check-minisign 786 + $(VENV_BIN)/python scripts/check_transparency_minisign.py 787 787 788 788 .PHONY: publish-transparency resign-transparency-pointer 789 789 publish-transparency: .installed
+191
scripts/check_transparency_minisign.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: AGPL-3.0-only 3 + # Copyright (c) 2026 sol pbc 4 + 5 + """Exercise the real minisign transparency signing path.""" 6 + 7 + from __future__ import annotations 8 + 9 + import argparse 10 + import shutil 11 + import subprocess 12 + import sys 13 + import tempfile 14 + from pathlib import Path 15 + from typing import Protocol, Sequence 16 + from unittest.mock import patch 17 + 18 + ROOT = Path(__file__).resolve().parent.parent 19 + if str(ROOT) not in sys.path: 20 + sys.path.insert(0, str(ROOT)) 21 + 22 + from scripts.release_candidate_driver import DriverError # noqa: E402 23 + from scripts.transparency_core import failure # noqa: E402 24 + from scripts.transparency_signing import ( # noqa: E402 25 + LocalMinisignSigner, 26 + check_minisign_binary, 27 + ) 28 + 29 + FIXTURE_DIR = ROOT / "tests" / "fixtures" / "transparency" 30 + ENTRY_FIXTURE = FIXTURE_DIR / "canonical-entry-v1.json" 31 + ENTRY_TRUSTED_COMMENT = FIXTURE_DIR / "entry-trusted-comment.txt" 32 + 33 + 34 + class _Verifier(Protocol): 35 + def verify_file( 36 + self, 37 + message_path: Path, 38 + signature_path: Path, 39 + *, 40 + expected_trusted_comment: str, 41 + ) -> None: ... 42 + 43 + 44 + def _print_failures(error: DriverError) -> None: 45 + for item in error.failures: 46 + print(f"ERROR: {item.error}", file=sys.stderr) 47 + print(f" expected: {item.expected}", file=sys.stderr) 48 + print(f" actual: {item.actual}", file=sys.stderr) 49 + print(f" repair: {item.repair}", file=sys.stderr) 50 + 51 + 52 + def _run_minisign(args: Sequence[str], *, input_text: str) -> None: 53 + result = subprocess.run( 54 + ["minisign", *args], 55 + input=input_text, 56 + text=True, 57 + capture_output=True, 58 + check=False, 59 + ) 60 + if result.returncode != 0: 61 + raise DriverError( 62 + [ 63 + failure( 64 + "transparency minisign key generation failed", 65 + expected="minisign -G exit 0", 66 + actual=( 67 + result.stderr or result.stdout or str(result.returncode) 68 + ).strip(), 69 + repair="retry after confirming the minisign binary works locally", 70 + ) 71 + ] 72 + ) 73 + 74 + 75 + def _generate_keypair(stage: Path, *, passphrase: str) -> tuple[Path, Path]: 76 + secret_key = stage / "secret.key" 77 + public_key = stage / "public.key" 78 + _run_minisign( 79 + ["-G", "-s", str(secret_key), "-p", str(public_key)], 80 + input_text=f"{passphrase}\n{passphrase}\n", 81 + ) 82 + return secret_key, public_key 83 + 84 + 85 + def _tamper_one_byte(payload: bytes) -> bytes: 86 + if not payload: 87 + raise DriverError( 88 + [ 89 + failure( 90 + "transparency minisign tamper fixture is empty", 91 + expected="non-empty message bytes", 92 + actual="0 bytes", 93 + repair="restore tests/fixtures/transparency/canonical-entry-v1.json", 94 + ) 95 + ] 96 + ) 97 + return bytes((payload[0] ^ 0x01,)) + payload[1:] 98 + 99 + 100 + def _assert_tampered_verify_fails( 101 + verifier: _Verifier, 102 + message_path: Path, 103 + signature_path: Path, 104 + *, 105 + expected_trusted_comment: str, 106 + ) -> None: 107 + tampered_path = message_path.with_name(f"{message_path.name}.tampered") 108 + tampered_path.write_bytes(_tamper_one_byte(message_path.read_bytes())) 109 + try: 110 + verifier.verify_file( 111 + tampered_path, 112 + signature_path, 113 + expected_trusted_comment=expected_trusted_comment, 114 + ) 115 + except DriverError: 116 + return 117 + raise DriverError( 118 + [ 119 + failure( 120 + "transparency minisign tampered message verified", 121 + expected="tampered message verification fails", 122 + actual="verification succeeded", 123 + repair="inspect minisign verification and trusted-comment handling", 124 + ) 125 + ] 126 + ) 127 + 128 + 129 + def run_gate() -> None: 130 + check_minisign_binary() 131 + passphrase = "transparency-minisign-check" 132 + with tempfile.TemporaryDirectory(prefix="transparency-minisign-") as tmp: 133 + stage = Path(tmp) 134 + message_path = stage / ENTRY_FIXTURE.name 135 + comment_path = stage / ENTRY_TRUSTED_COMMENT.name 136 + shutil.copy2(ENTRY_FIXTURE, message_path) 137 + shutil.copy2(ENTRY_TRUSTED_COMMENT, comment_path) 138 + trusted_comment = comment_path.read_text(encoding="utf-8").rstrip("\n") 139 + secret_key, public_key = _generate_keypair(stage, passphrase=passphrase) 140 + signature_path = stage / f"{message_path.name}.minisig" 141 + signer = LocalMinisignSigner(secret_key=secret_key, public_key=public_key) 142 + with patch("getpass.getpass", return_value=passphrase): 143 + signer.sign_file( 144 + message_path, 145 + signature_path, 146 + trusted_comment=trusted_comment, 147 + ) 148 + signer.verify_file( 149 + message_path, 150 + signature_path, 151 + expected_trusted_comment=trusted_comment, 152 + ) 153 + extracted_comment = signer.trusted_comment(signature_path) 154 + if extracted_comment != trusted_comment: 155 + raise DriverError( 156 + [ 157 + failure( 158 + "transparency minisign trusted comment extraction mismatch", 159 + expected=trusted_comment, 160 + actual=extracted_comment, 161 + repair="inspect LocalMinisignSigner.trusted_comment", 162 + ) 163 + ] 164 + ) 165 + _assert_tampered_verify_fails( 166 + signer, 167 + message_path, 168 + signature_path, 169 + expected_trusted_comment=trusted_comment, 170 + ) 171 + 172 + 173 + def build_parser() -> argparse.ArgumentParser: 174 + return argparse.ArgumentParser( 175 + description="Check real transparency minisign signing." 176 + ) 177 + 178 + 179 + def main(argv: Sequence[str] | None = None) -> int: 180 + build_parser().parse_args(list(argv) if argv is not None else None) 181 + try: 182 + run_gate() 183 + except DriverError as exc: 184 + _print_failures(exc) 185 + return 1 186 + print("transparency minisign check ok") 187 + return 0 188 + 189 + 190 + if __name__ == "__main__": 191 + raise SystemExit(main())
-8
scripts/transparency_publish.py
··· 79 79 highest_seq, 80 80 ) 81 81 from scripts.transparency_signing import ( 82 - MISSING_MINISIGN_MESSAGE, 83 82 LocalMinisignSigner, 84 83 TransparencySigner, 85 - check_minisign_binary, 86 84 ) 87 85 from scripts.transparency_transport import ( 88 86 CurlTransparencyTransport, ··· 1814 1812 resign_parser.add_argument("--root", default=".") 1815 1813 resign_parser.add_argument("--version", default="resign") 1816 1814 resign_parser.add_argument("--source-commit", default="0" * 40) 1817 - subparsers.add_parser("check-minisign") 1818 1815 return parser 1819 1816 1820 1817 ··· 1826 1823 args = parser.parse_args(list(argv) if argv is not None else None) 1827 1824 runtime_env = dict(os.environ if env is None else env) 1828 1825 try: 1829 - if args.command == "check-minisign": 1830 - check_minisign_binary() 1831 - return 0 1832 1826 config = _config_from_args(args, runtime_env) 1833 1827 transport = _transport_from_config(config) 1834 1828 signer = _signer_from_config(config) ··· 1849 1843 return 2 1850 1844 except DriverError as exc: 1851 1845 _print_failures(exc) 1852 - if any(item.error == MISSING_MINISIGN_MESSAGE for item in exc.failures): 1853 - return 1 1854 1846 return 1 1855 1847 print(json.dumps(result.as_dict(), sort_keys=True)) 1856 1848 return 0
+37 -13
tests/test_transparency_cli.py
··· 1 1 from __future__ import annotations 2 2 3 3 import json 4 - import logging 5 4 from argparse import Namespace 6 5 from pathlib import Path 7 6 8 7 import pytest 9 8 9 + import scripts.check_transparency_minisign as minisign_gate 10 10 import scripts.transparency_publish as publisher 11 11 from scripts.release_candidate_driver import DriverError 12 12 from scripts.transparency_core import DEFAULT_BASE_URL, PRODUCT ··· 61 61 assert config.source_commit == "b" * 40 62 62 63 63 64 - def test_cli_check_minisign_success(monkeypatch: pytest.MonkeyPatch) -> None: 65 - monkeypatch.setattr(publisher, "check_minisign_binary", lambda: "minisign 0.12") 66 - assert publisher.main(["check-minisign"], env={}) == 0 67 - 68 - 69 - def test_cli_check_minisign_missing_logs_loud_message( 64 + def test_check_transparency_minisign_missing_binary_fails_loudly( 70 65 monkeypatch: pytest.MonkeyPatch, 71 - caplog: pytest.LogCaptureFixture, 66 + capsys: pytest.CaptureFixture[str], 72 67 ) -> None: 73 - def fail() -> str: 68 + def fail() -> None: 74 69 raise DriverError( 75 70 [ 76 71 publisher.failure( ··· 82 77 ] 83 78 ) 84 79 85 - monkeypatch.setattr(publisher, "check_minisign_binary", fail) 86 - caplog.set_level(logging.ERROR) 87 - assert publisher.main(["check-minisign"], env={}) == 1 88 - assert MISSING_MINISIGN_MESSAGE in caplog.text 80 + monkeypatch.setattr(minisign_gate, "check_minisign_binary", fail) 81 + assert minisign_gate.main([]) == 1 82 + captured = capsys.readouterr() 83 + assert MISSING_MINISIGN_MESSAGE in captured.err 84 + assert "sudo dnf install minisign" in captured.err 85 + 86 + 87 + def test_check_transparency_minisign_tamper_must_fail(tmp_path: Path) -> None: 88 + class AcceptingVerifier: 89 + def verify_file( 90 + self, 91 + message_path: Path, 92 + signature_path: Path, 93 + *, 94 + expected_trusted_comment: str, 95 + ) -> None: 96 + return None 97 + 98 + message = tmp_path / "message.json" 99 + signature = tmp_path / "message.json.minisig" 100 + message.write_bytes(b'{"ok":1}\n') 101 + signature.write_text("placeholder\n", encoding="utf-8") 102 + with pytest.raises(DriverError) as error: 103 + minisign_gate._assert_tampered_verify_fails( 104 + AcceptingVerifier(), 105 + message, 106 + signature, 107 + expected_trusted_comment="trusted", 108 + ) 109 + assert ( 110 + error.value.failures[0].error 111 + == "transparency minisign tampered message verified" 112 + ) 89 113 90 114 91 115 def test_cli_publish_prints_operator_summary(