#!/usr/bin/env python3 import base64 import json import subprocess import sys import tempfile import unittest from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] SDK_SRC = PROJECT_ROOT / "packages" / "jwebot-sdk" / "src" KERNEL_SRC = PROJECT_ROOT / "packages" / "jwebot-kernel" / "src" sys.path[:0] = [str(SDK_SRC), str(KERNEL_SRC)] from jwebot_kernel import ( # noqa: E402 ActivationError, ReleaseError, SignatureError, build_release, install_release, parse_manifest, rollback_release, sign_release_record, verify_installed_release_signature, verify_release_signature, ) SSH_KEYGEN = Path("/usr/bin/ssh-keygen") PUBLISHER = "test-publisher" def manifest(): return parse_manifest( { "id": "hello-command", "author": "tester", "description": "Say hello", "help": ["/hello"], "version": "1.0.0", "api_version": "1", "entrypoint": "hello_command:plugin", "type": "command", "capabilities": { "commands": ["/hello"], "storage": False, "network_profiles": [], }, } ) def extension(root: Path, reply: str = "hello") -> Path: root.mkdir() (root / "src").mkdir() (root / "plugin.toml").write_text( 'id = "hello-command"\n', encoding="utf-8", ) (root / "src" / "hello_command.py").write_text( "\n".join( ( "from jwebot_sdk import ExtensionResult, ReplyPlan", "", "def handle(event, arguments):", f" return ExtensionResult(reply=ReplyPlan.text({reply!r}))", ) ), encoding="utf-8", ) return root def publisher_key(root: Path, publisher: str = PUBLISHER): root.mkdir(parents=True, exist_ok=True) private_key = root / "publisher" result = subprocess.run( [ str(SSH_KEYGEN), "-q", "-t", "ed25519", "-N", "", "-f", str(private_key), ], capture_output=True, check=False, ) if result.returncode != 0: raise RuntimeError("cannot generate test publisher key") allowed_signers = root / "allowed_signers" public_key = private_key.with_suffix(".pub").read_text( encoding="utf-8" ).strip() allowed_signers.write_text( f"{publisher} {public_key}\n", encoding="utf-8", ) return private_key, allowed_signers @unittest.skipUnless( SSH_KEYGEN.is_file(), "/usr/bin/ssh-keygen is unavailable", ) class ReleaseSigningTests(unittest.TestCase): def build( self, base: Path, name: str = "extension", reply: str = "hello", ): return build_release( extension(base / name, reply), base / f"{name}-dist", manifest=manifest(), ) def test_valid_signature_is_deterministic_and_verifies(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) _, record_path, _ = self.build(base) private_key, allowed_signers = publisher_key(base) first_path, first = sign_release_record( record_path, private_key, PUBLISHER, ) first_bytes = first_path.read_bytes() second_path, second = sign_release_record( record_path, private_key, PUBLISHER, ) self.assertEqual(first, second) self.assertEqual(first_bytes, second_path.read_bytes()) verified = verify_release_signature( record_path, first_path, allowed_signers, expected_publisher=PUBLISHER, ) self.assertEqual(verified.publisher, PUBLISHER) def test_signature_rejects_release_record_tampering(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) _, record_path, _ = self.build(base) private_key, allowed_signers = publisher_key(base) signature_path, _ = sign_release_record( record_path, private_key, PUBLISHER, ) value = json.loads(record_path.read_text(encoding="utf-8")) value["artifact_size"] += 1 record_path.write_text( json.dumps( value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), ) + "\n", encoding="utf-8", ) with self.assertRaisesRegex( SignatureError, "signed release digest mismatch", ): verify_release_signature( record_path, signature_path, allowed_signers, ) def test_signing_rejects_tampered_artifact(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) artifact, record_path, _ = self.build(base) private_key, _ = publisher_key(base) artifact.write_bytes(artifact.read_bytes() + b"tampered") with self.assertRaisesRegex(ReleaseError, "size mismatch"): sign_release_record( record_path, private_key, PUBLISHER, ) def test_signature_rejects_cryptographic_tampering(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) _, record_path, _ = self.build(base) private_key, allowed_signers = publisher_key(base) signature_path, _ = sign_release_record( record_path, private_key, PUBLISHER, ) value = json.loads(signature_path.read_text(encoding="utf-8")) raw = bytearray(base64.b64decode(value["signature_base64"])) raw[len(raw) // 2] ^= 0x01 value["signature_base64"] = base64.b64encode(raw).decode("ascii") signature_path.write_text( json.dumps( value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), ) + "\n", encoding="utf-8", ) with self.assertRaisesRegex( SignatureError, "signature verification failed", ): verify_release_signature( record_path, signature_path, allowed_signers, ) def test_signature_rejects_untrusted_publisher_key(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) _, record_path, _ = self.build(base) private_key, _ = publisher_key(base / "trusted") untrusted_root = base / "untrusted" untrusted_root.mkdir() _, wrong_allowed_signers = publisher_key(untrusted_root) signature_path, _ = sign_release_record( record_path, private_key, PUBLISHER, ) with self.assertRaisesRegex( SignatureError, "signature verification failed", ): verify_release_signature( record_path, signature_path, wrong_allowed_signers, ) def test_trust_file_rejects_non_ed25519_entries(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) _, record_path, _ = self.build(base) private_key, allowed_signers = publisher_key(base) signature_path, _ = sign_release_record( record_path, private_key, PUBLISHER, ) allowed_signers.write_text( allowed_signers.read_text(encoding="utf-8").replace( "ssh-ed25519", "ssh-rsa", ), encoding="utf-8", ) with self.assertRaisesRegex(SignatureError, "simple Ed25519"): verify_release_signature( record_path, signature_path, allowed_signers, ) def test_signature_binds_publisher_even_when_key_has_two_principals(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) _, record_path, _ = self.build(base) private_key, allowed_signers = publisher_key(base) public_key = private_key.with_suffix(".pub").read_text( encoding="utf-8" ).strip() with allowed_signers.open("a", encoding="utf-8") as stream: stream.write(f"other-publisher {public_key}\n") signature_path, _ = sign_release_record( record_path, private_key, PUBLISHER, ) value = json.loads(signature_path.read_text(encoding="utf-8")) value["publisher"] = "other-publisher" signature_path.write_text( json.dumps( value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), ) + "\n", encoding="utf-8", ) with self.assertRaisesRegex( SignatureError, "signature verification failed", ): verify_release_signature( record_path, signature_path, allowed_signers, ) def test_signing_rejects_noncanonical_release_record(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) _, record_path, _ = self.build(base) private_key, _ = publisher_key(base) record_path.write_bytes(b" " + record_path.read_bytes()) with self.assertRaisesRegex(SignatureError, "not canonical"): sign_release_record( record_path, private_key, PUBLISHER, ) def test_signed_activation_persists_and_reverifies_signature(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) artifact, record_path, _ = self.build(base) private_key, allowed_signers = publisher_key(base) signature_path, _ = sign_release_record( record_path, private_key, PUBLISHER, ) release_dir = install_release( artifact, record_path, base / "state", signature=signature_path, allowed_signers=allowed_signers, expected_publisher=PUBLISHER, health_check=lambda release: None, ) self.assertTrue((release_dir / ".release.sig.json").is_file()) record, signature = verify_installed_release_signature( release_dir, allowed_signers, expected_publisher=PUBLISHER, ) self.assertEqual(record.plugin_id, "hello-command") self.assertEqual(signature.publisher, PUBLISHER) def test_signed_activation_requires_passing_health_check(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) artifact, record_path, _ = self.build(base) private_key, allowed_signers = publisher_key(base) signature_path, _ = sign_release_record( record_path, private_key, PUBLISHER, ) state = base / "state" with self.assertRaisesRegex( ActivationError, "health check is required", ): install_release( artifact, record_path, state, signature=signature_path, allowed_signers=allowed_signers, expected_publisher=PUBLISHER, ) self.assertFalse(state.exists()) def test_failed_health_check_does_not_switch_current(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) artifact, record_path, record = self.build(base) private_key, allowed_signers = publisher_key(base) signature_path, _ = sign_release_record( record_path, private_key, PUBLISHER, ) state = base / "state" with self.assertRaisesRegex( ActivationError, "health check failed", ): install_release( artifact, record_path, state, signature=signature_path, allowed_signers=allowed_signers, expected_publisher=PUBLISHER, health_check=lambda release: (_ for _ in ()).throw( RuntimeError("unhealthy") ), ) self.assertFalse((state / "current").exists()) self.assertFalse( (state / "releases" / record.source_digest).exists() ) def test_signed_activation_rejects_wrong_publisher_policy(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) artifact, record_path, _ = self.build(base) private_key, allowed_signers = publisher_key(base) signature_path, _ = sign_release_record( record_path, private_key, PUBLISHER, ) state = base / "state" with self.assertRaisesRegex( SignatureError, "publisher does not match policy", ): install_release( artifact, record_path, state, signature=signature_path, allowed_signers=allowed_signers, expected_publisher="other-publisher", ) self.assertFalse(state.exists()) def test_signed_rollback_reverifies_previous_release(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) private_key, allowed_signers = publisher_key(base) first_artifact, first_record_path, first_record = self.build( base, "first", "one", ) first_signature, _ = sign_release_record( first_record_path, private_key, PUBLISHER, ) second_artifact, second_record_path, second_record = self.build( base, "second", "two", ) second_signature, _ = sign_release_record( second_record_path, private_key, PUBLISHER, ) state = base / "state" install_release( first_artifact, first_record_path, state, signature=first_signature, allowed_signers=allowed_signers, expected_publisher=PUBLISHER, health_check=lambda release: None, ) install_release( second_artifact, second_record_path, state, signature=second_signature, allowed_signers=allowed_signers, expected_publisher=PUBLISHER, health_check=lambda release: None, ) destination = rollback_release( state, allowed_signers=allowed_signers, expected_publisher=PUBLISHER, ) self.assertEqual(destination.name, first_record.source_digest) self.assertEqual( (state / "current").resolve().name, first_record.source_digest, ) self.assertEqual( (state / "previous").resolve().name, second_record.source_digest, ) if __name__ == "__main__": unittest.main()