Require signed agent releases (Refs #64)
This commit is contained in:
@@ -11,7 +11,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
SOURCE = (Path(__file__).resolve().parents[1] / "install.sh").read_text()
|
||||
@@ -33,6 +33,8 @@ class InstallerTests(unittest.TestCase):
|
||||
self.root = self.base / "managed"
|
||||
self.config = self.base / "configuration" / "agent.json"
|
||||
self.config.parent.mkdir()
|
||||
self.keyring = self.config.with_name('release-signing.gpg')
|
||||
self.keyring.write_bytes(b'publisher public key')
|
||||
self.controller = "https://controller.example"
|
||||
self.saved = {"root": str(self.root), "controller": self.controller,
|
||||
"hostId": "host-one", "credential": "private"}
|
||||
@@ -80,6 +82,17 @@ class InstallerTests(unittest.TestCase):
|
||||
self.preflight()
|
||||
self.assertEqual((self.root / "unrelated.txt").read_text(), "keep")
|
||||
|
||||
def test_trust_key_must_be_preprovisioned_and_protected(self):
|
||||
self.keyring.unlink()
|
||||
with self.assertRaisesRegex(SystemExit, 'Provision the publisher public key'):
|
||||
self.preflight()
|
||||
self.keyring.write_bytes(b'publisher public key')
|
||||
for override in ({'uid': 1000}, {'mode': stat.S_IFREG | 0o666}, {'mode': stat.S_IFLNK | 0o777}):
|
||||
with self.subTest(override=override):
|
||||
self.overrides[str(self.keyring)] = override
|
||||
with self.assertRaisesRegex(SystemExit, 'regular, root-owned'):
|
||||
self.preflight()
|
||||
|
||||
def test_known_legacy_layout_can_upgrade_and_marker_binds_exact_enrollment(self):
|
||||
self.legacy_installation()
|
||||
original_config = self.config.read_bytes()
|
||||
@@ -337,20 +350,31 @@ docker() {
|
||||
class ReleaseTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.source = Path(self.temporary.name) / "source"
|
||||
self.target = Path(self.temporary.name) / "release"
|
||||
self.source = Path(self.temporary.name).resolve() / "source"
|
||||
self.target = Path(self.temporary.name).resolve() / "release"
|
||||
self.source.mkdir()
|
||||
(self.source / "ec_agent.py").write_text('VERSION = "3.12.7"\nraise RuntimeError("must never execute during packaging")\n')
|
||||
(self.source / "ec_agent_update.py").write_text('raise RuntimeError("must never execute during verification")\n')
|
||||
(self.source / "install.sh").write_text("#!/bin/bash\n")
|
||||
PACKAGER.package(self.source, self.target)
|
||||
self.keyring = self.source.parent / 'release-signing.gpg'
|
||||
self.keyring.write_bytes(b'publisher public key')
|
||||
(self.target / 'manifest.json.sig').write_bytes(b'detached signature')
|
||||
self.gpgv = Mock(return_value=subprocess.CompletedProcess([], 0))
|
||||
|
||||
def tearDown(self):
|
||||
self.temporary.cleanup()
|
||||
|
||||
def verify(self):
|
||||
argv = ["-", str(self.target / "manifest.json"), str(self.target / "ec-agent.py"), str(self.target / "ec-agent-update.py")]
|
||||
with patch.object(sys, "argv", argv):
|
||||
argv = ["-", str(self.target / "manifest.json"), str(self.target / "ec-agent.py"), str(self.target / "ec-agent-update.py"), str(self.keyring)]
|
||||
original_lstat = Path.lstat
|
||||
def root_owned(path):
|
||||
fields = list(original_lstat(path))
|
||||
fields[4] = 0
|
||||
if stat.S_ISDIR(fields[0]):
|
||||
fields[0] &= ~0o022
|
||||
return os.stat_result(fields)
|
||||
with patch.object(sys, "argv", argv), patch.object(Path, 'lstat', root_owned), patch.object(subprocess, 'run', self.gpgv):
|
||||
exec(compile(RELEASE, "installer-release", "exec"), {})
|
||||
|
||||
def manifest(self, **changes):
|
||||
@@ -363,7 +387,7 @@ class ReleaseTests(unittest.TestCase):
|
||||
def test_package_derives_version_and_hashes_both_sources_without_executing_them(self):
|
||||
manifest = self.manifest()
|
||||
self.assertEqual(manifest["version"], "3.12.7")
|
||||
self.assertEqual(manifest["updaterProtocolVersion"], 1)
|
||||
self.assertEqual(manifest["updaterProtocolVersion"], 2)
|
||||
self.assertEqual(manifest["sha256"], hashlib.sha256((self.source / "ec_agent.py").read_bytes()).hexdigest())
|
||||
self.assertEqual(manifest["updaterSha256"], hashlib.sha256((self.source / "ec_agent_update.py").read_bytes()).hexdigest())
|
||||
self.assertEqual((self.target / "ec-agent-update.py").read_bytes(), (self.source / "ec_agent_update.py").read_bytes())
|
||||
@@ -380,11 +404,38 @@ class ReleaseTests(unittest.TestCase):
|
||||
path.write_bytes(original)
|
||||
|
||||
def test_installer_rejects_missing_or_unsupported_updater_protocol(self):
|
||||
for protocol in (None, 0, 2, "1"):
|
||||
for protocol in (None, 0, 1, True, "2"):
|
||||
self.manifest(updaterProtocolVersion=protocol)
|
||||
with self.subTest(protocol=protocol), self.assertRaisesRegex(SystemExit, "Unsupported agent or updater"):
|
||||
self.verify()
|
||||
|
||||
def test_signature_is_verified_against_pinned_key_before_manifest_parsing(self):
|
||||
self.verify()
|
||||
args, kwargs = self.gpgv.call_args
|
||||
self.assertEqual(args[0][0], 'gpgv')
|
||||
self.assertEqual(args[0][-4:], ['--keyring', str(self.keyring), str(self.target / 'manifest.json.sig'), '-'])
|
||||
self.assertEqual(kwargs['input'], (self.target / 'manifest.json').read_bytes())
|
||||
self.assertEqual(kwargs['timeout'], 30)
|
||||
self.assertFalse(Path(args[0][2]).exists())
|
||||
(self.target / 'manifest.json').write_bytes(b'not even JSON')
|
||||
self.gpgv.return_value = subprocess.CompletedProcess([], 1)
|
||||
with self.assertRaisesRegex(SystemExit, 'signature verification failed'):
|
||||
self.verify()
|
||||
|
||||
def test_missing_signature_key_or_gpgv_prevents_installation(self):
|
||||
for path in (self.keyring, self.target / 'manifest.json.sig'):
|
||||
original = path.read_bytes()
|
||||
path.unlink()
|
||||
self.gpgv.reset_mock()
|
||||
with self.subTest(path=path), self.assertRaises(FileNotFoundError):
|
||||
self.verify()
|
||||
self.gpgv.assert_not_called()
|
||||
path.write_bytes(original)
|
||||
for failure in (FileNotFoundError(), subprocess.TimeoutExpired('gpgv', 30)):
|
||||
self.gpgv.side_effect = failure
|
||||
with self.subTest(failure=failure), self.assertRaisesRegex(SystemExit, 'working gpgv'):
|
||||
self.verify()
|
||||
|
||||
def test_installer_rejects_manifest_version_mismatch_and_invalid_code(self):
|
||||
self.manifest(version="3.12.8")
|
||||
with self.assertRaisesRegex(SystemExit, "version does not match"):
|
||||
|
||||
Reference in New Issue
Block a user