diff --git a/inbc-program/README.md b/inbc-program/README.md index b78943704..d63340214 100644 --- a/inbc-program/README.md +++ b/inbc-program/README.md @@ -424,6 +424,13 @@ inbc query --option sw Optionally Downloads and encrypts GPG key and stores it on the system under /usr/share/keyrings. Creates a file under /etc/apt/sources.list.d to store the update source information. This list file is used during 'sudo apt update' to update the application. Deb882 format may be used instead of downloading a GPG key. +**NOTE:** Make sure to add gpgKeyUri to trustedrepositories using INBC Config Append command before using Inbc source application add command + Step 1: Refer to Inbc Config Append command to set gpgKeyUri to trustedRepositories in intel-manageability.conf file + Example: inbc append --path trustedRepositories:https://deb.opera.com/ + Step 2: Use Inbc source appplication add command +``` + + ### Usage ``` inbc source application add @@ -449,7 +456,6 @@ inbc source application add - Each blank line has a period in it. -> " ." - Each line after the Signed-By: starts with a space -> " gibberish" - ``` inbc source application add --sources diff --git a/inbm/Changelog.md b/inbm/Changelog.md index 15b92ec13..aab0f0142 100644 --- a/inbm/Changelog.md +++ b/inbm/Changelog.md @@ -10,7 +10,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Added - RTC 536601 - Added 'source' command to INBM. This command manages `/etc/apt/sources.list` and `/etc/apt/sources.list.d/*` and associated gpg keys on Ubuntu. +- RTC 537769 - Added verification of GPG key URIs against a list of trusted repositories for enhanced security +check if sourceApplication Gpg key URL is in trusted repo ### Fixed - RTC 534426 - Could not write to /var/log/inbm-update-status.log on Yocto due to /var/log being a symlink to /var/volatile/log. - RTC 523677 - Improve INBC error logging - invalid child tag not printed diff --git a/inbm/dispatcher-agent/dispatcher/dispatcher_class.py b/inbm/dispatcher-agent/dispatcher/dispatcher_class.py index 60a62e41b..d5ca167c5 100644 --- a/inbm/dispatcher-agent/dispatcher/dispatcher_class.py +++ b/inbm/dispatcher-agent/dispatcher/dispatcher_class.py @@ -16,6 +16,7 @@ from threading import Thread, active_count from time import sleep from typing import Tuple +from typing import Optional, Any from dispatcher.config.config_operation import ConfigOperation from dispatcher.source.source_command import do_source_command @@ -293,7 +294,7 @@ def do_install(self, xml: str, schema_location: Optional[str] = None) -> Result: elif type_of_manifest == 'source': logger.debug('Running source command') # FIXME: actually detect OS - result = do_source_command(parsed_head, source.constants.OsType.Ubuntu) + result = do_source_command(parsed_head, source.constants.OsType.Ubuntu, self._dispatcher_broker) elif type_of_manifest == 'ota': # Parse manifest header = parsed_head.get_children('ota/header') diff --git a/inbm/dispatcher-agent/dispatcher/source/source_command.py b/inbm/dispatcher-agent/dispatcher/source/source_command.py index c15aaa6e7..d7d3d043e 100644 --- a/inbm/dispatcher-agent/dispatcher/source/source_command.py +++ b/inbm/dispatcher-agent/dispatcher/source/source_command.py @@ -7,6 +7,8 @@ import logging import json from dispatcher.common.result_constants import Result +from typing import Optional, Any +from dispatcher.dispatcher_broker import DispatcherBroker from dispatcher.source.constants import ( ApplicationAddSourceParameters, ApplicationRemoveSourceParameters, @@ -22,7 +24,7 @@ logger = logging.getLogger(__name__) -def do_source_command(parsed_head: XmlHandler, os_type: OsType) -> Result: +def do_source_command(parsed_head: XmlHandler, os_type: OsType, dispatcher_broker: DispatcherBroker) -> Result: """ Run a source command. @@ -42,7 +44,7 @@ def do_source_command(parsed_head: XmlHandler, os_type: OsType) -> Result: try: app_action = parsed_head.get_children("applicationSource") if app_action: - return _handle_app_source_command(parsed_head, os_type, app_action) + return _handle_app_source_command(parsed_head, os_type, app_action, dispatcher_broker) except XmlException as e: return Result(status=400, message=f"unable to handle source command XML: {e}") @@ -94,16 +96,17 @@ def _handle_os_source_command(parsed_head: XmlHandler, os_type: OsType, os_actio def _handle_app_source_command( - parsed_head: XmlHandler, os_type: OsType, app_action: dict) -> Result: + parsed_head: XmlHandler, os_type: OsType, app_action: dict, dispatcher_broker: DispatcherBroker) -> Result: """ Handle the application source commands. @param parsed_head: XmlHandler with command information @param os_type: OS type @param app_action: The action to be performed + @param dispatcher_broker: MQTT @return Result """ - application_source_manager = create_application_source_manager(os_type) + application_source_manager = create_application_source_manager(os_type, dispatcher_broker) if "list" in app_action: serialized_list = json.dumps( diff --git a/inbm/dispatcher-agent/dispatcher/source/source_manager_factory.py b/inbm/dispatcher-agent/dispatcher/source/source_manager_factory.py index 0f11c4b55..b89e4e157 100644 --- a/inbm/dispatcher-agent/dispatcher/source/source_manager_factory.py +++ b/inbm/dispatcher-agent/dispatcher/source/source_manager_factory.py @@ -5,9 +5,11 @@ SPDX-License-Identifier: Apache-2.0 """ import logging +from dispatcher.dispatcher_broker import DispatcherBroker from dispatcher.source.constants import OsType from dispatcher.source.source_manager import ApplicationSourceManager, OsSourceManager +from typing import Optional, Any from dispatcher.source.ubuntu_source_manager import ( UbuntuApplicationSourceManager, UbuntuOsSourceManager, @@ -23,8 +25,8 @@ def create_os_source_manager(os_type: OsType) -> OsSourceManager: raise ValueError(f"Unsupported OS type: {os_type}.") -def create_application_source_manager(os_type: OsType) -> ApplicationSourceManager: +def create_application_source_manager(os_type: OsType, dispatcher_broker: DispatcherBroker) -> ApplicationSourceManager: """Return correct OS application manager based on OS type""" if os_type is OsType.Ubuntu: - return UbuntuApplicationSourceManager() + return UbuntuApplicationSourceManager(dispatcher_broker) raise ValueError(f"Unsupported OS type: {os_type}.") diff --git a/inbm/dispatcher-agent/dispatcher/source/ubuntu_source_manager.py b/inbm/dispatcher-agent/dispatcher/source/ubuntu_source_manager.py index fa321d08a..402825876 100644 --- a/inbm/dispatcher-agent/dispatcher/source/ubuntu_source_manager.py +++ b/inbm/dispatcher-agent/dispatcher/source/ubuntu_source_manager.py @@ -7,6 +7,9 @@ import logging import os +from dispatcher.packagemanager.package_manager import verify_source +from dispatcher.dispatcher_broker import DispatcherBroker +from dispatcher.dispatcher_exception import DispatcherException from dispatcher.source.source_exception import SourceError from dispatcher.source.constants import ( UBUNTU_APT_SOURCES_LIST, @@ -94,16 +97,24 @@ def update(self, parameters: SourceParameters) -> None: class UbuntuApplicationSourceManager(ApplicationSourceManager): - def __init__(self) -> None: - pass + def __init__(self, broker: DispatcherBroker) -> None: + self._dispatcher_broker = broker def add(self, parameters: ApplicationAddSourceParameters) -> None: """Adds a source file and optional GPG key to be used during Ubuntu application updates.""" - # Step 1: Add key (Optional) + # Step 1: Verify gpg key uri from trusted repo list if parameters.gpg_key_name and parameters.gpg_key_uri: + try: + url = parameters.gpg_key_uri + #URL slicing to remove the last segment (filename) from the URL + source = url[:-(len(url.split('/')[-1]) + 1)] + verify_source(source=source, dispatcher_broker=self._dispatcher_broker) + except (DispatcherException, IndexError) as err: + raise SourceError(f"Source Gpg key URI verification check failed: {err}") + # Step 2: Add key (Optional) add_gpg_key(parameters.gpg_key_uri, parameters.gpg_key_name) - # Step 2: Add the source + # Step 3: Add the source try: create_file_with_contents( os.path.join(UBUNTU_APT_SOURCES_LIST_D, parameters.source_list_file_name), parameters.sources diff --git a/inbm/dispatcher-agent/tests/unit/source/test_source_cmd_factory.py b/inbm/dispatcher-agent/tests/unit/source/test_source_cmd_factory.py index 44160db3e..4a3603640 100644 --- a/inbm/dispatcher-agent/tests/unit/source/test_source_cmd_factory.py +++ b/inbm/dispatcher-agent/tests/unit/source/test_source_cmd_factory.py @@ -1,5 +1,6 @@ import pytest from dispatcher.source.constants import OsType +from ..common.mock_resources import MockDispatcherBroker from dispatcher.source.source_manager_factory import ( create_application_source_manager, create_os_source_manager, @@ -20,13 +21,13 @@ def test_create_os_source_manager_unsupported(): create_os_source_manager("UnsupportedOS") assert "Unsupported OS type" in str(excinfo.value) - def test_create_application_source_manager_ubuntu(): - command = create_application_source_manager(OsType.Ubuntu) + mock_disp_broker_obj = MockDispatcherBroker.build_mock_dispatcher_broker() + command = create_application_source_manager(OsType.Ubuntu, mock_disp_broker_obj) assert isinstance(command, UbuntuApplicationSourceManager) - def test_create_application_source_manager_unsupported(): + mock_disp_broker_obj = MockDispatcherBroker.build_mock_dispatcher_broker() with pytest.raises(ValueError) as excinfo: - create_application_source_manager("UnsupportedOS") + create_application_source_manager("UnsupportedOS", mock_disp_broker_obj) assert "Unsupported OS type" in str(excinfo.value) diff --git a/inbm/dispatcher-agent/tests/unit/source/test_source_command.py b/inbm/dispatcher-agent/tests/unit/source/test_source_command.py index 4d6d8fd19..31db61c07 100644 --- a/inbm/dispatcher-agent/tests/unit/source/test_source_command.py +++ b/inbm/dispatcher-agent/tests/unit/source/test_source_command.py @@ -7,6 +7,7 @@ import pytest from dispatcher.common.result_constants import Result +from ..common.mock_resources import MockDispatcherBroker from dispatcher.source.constants import ( ApplicationAddSourceParameters, ApplicationRemoveSourceParameters, @@ -66,8 +67,8 @@ def test_do_source_command_list( mock_source_manager.list.return_value = return_value mocker.patch(patch_target, return_value=mock_source_manager) - - result = do_source_command(xml_handler, OsType.Ubuntu) + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + result = do_source_command(xml_handler, OsType.Ubuntu, broker) assert result == Result(status=200, message=expected_message) mock_source_manager.list.assert_called_once() @@ -113,8 +114,8 @@ def test_do_source_command_remove( mock_manager.remove.return_value = None mocker.patch(manager_mock, return_value=mock_manager) - - result = do_source_command(xml_handler, os_type) + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + result = do_source_command(xml_handler, os_type, broker) mock_manager.remove.assert_called_once_with(expected_call) assert result == Result(status=200, message="SUCCESS") @@ -180,8 +181,8 @@ def test_do_source_command_add( mock_manager.add.return_value = None mocker.patch(manager_mock, return_value=mock_manager) - - result = do_source_command(xml_handler, os_type) + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + result = do_source_command(xml_handler, os_type, broker) mock_manager.add.assert_called_once_with(expected_call) assert result == Result(status=200, message="SUCCESS") @@ -240,8 +241,8 @@ def test_do_source_command_update( mock_manager.update.return_value = None mocker.patch(manager_mock, return_value=mock_manager) - - result = do_source_command(xml_handler, os_type) + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + result = do_source_command(xml_handler, os_type, broker) mock_manager.update.assert_called_once_with(expected_call) assert result == Result(status=200, message="SUCCESS") diff --git a/inbm/dispatcher-agent/tests/unit/source/test_ubuntu_source_cmd.py b/inbm/dispatcher-agent/tests/unit/source/test_ubuntu_source_cmd.py index 749ab3a4e..d2bb0fdd8 100644 --- a/inbm/dispatcher-agent/tests/unit/source/test_ubuntu_source_cmd.py +++ b/inbm/dispatcher-agent/tests/unit/source/test_ubuntu_source_cmd.py @@ -2,6 +2,8 @@ import pytest from unittest.mock import mock_open, patch from dispatcher.source.source_exception import SourceError +from ..common.mock_resources import MockDispatcherBroker +from dispatcher.dispatcher_exception import DispatcherException from dispatcher.source.constants import ( UBUNTU_APT_SOURCES_LIST_D, UBUNTU_APT_SOURCES_LIST, @@ -193,15 +195,17 @@ def test_update_sources_os_error(self): class TestUbuntuApplicationSourceManager: - def test_add_app_with_gpg_key_successfully(self): + @patch("dispatcher.source.ubuntu_source_manager.verify_source") + def test_add_app_with_gpg_key_successfully(self, mock_verify_source): try: params = ApplicationAddSourceParameters( - source_list_file_name="intel-gpu-jammy.list", + source_list_file_name="google-chrome.sources", sources="deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main", gpg_key_uri="https://dl-ssl.google.com/linux/linux_signing_key.pub", gpg_key_name="google-chrome.gpg" ) - command = UbuntuApplicationSourceManager() + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + command = UbuntuApplicationSourceManager(broker) with (patch("builtins.open", new_callable=mock_open()), patch("dispatcher.source.ubuntu_source_manager.add_gpg_key")): command.add(params) @@ -209,6 +213,7 @@ def test_add_app_with_gpg_key_successfully(self): pytest.fail(f"'UbuntuApplicationSourceManager.add' raised an exception {err}") def test_add_app_deb_822_format_successfully(self): + broker = MockDispatcherBroker.build_mock_dispatcher_broker() try: params = ApplicationAddSourceParameters( source_list_file_name="google-chrome.sources", @@ -219,7 +224,7 @@ def test_add_app_deb_822_format_successfully(self): "Suites: stable" "Components: main", ) - command = UbuntuApplicationSourceManager() + command = UbuntuApplicationSourceManager(broker) with patch("builtins.open", new_callable=mock_open()): command.add(params) except SourceError as err: @@ -227,10 +232,11 @@ def test_add_app_deb_822_format_successfully(self): def test_update_app_source_successfully(self): try: + broker = MockDispatcherBroker.build_mock_dispatcher_broker() params = ApplicationUpdateSourceParameters( source_list_file_name="intel-gpu-jammy.list", sources=APP_SOURCE ) - command = UbuntuApplicationSourceManager() + command = UbuntuApplicationSourceManager(broker) with patch("builtins.open", new_callable=mock_open()): command.update(params) except SourceError as err: @@ -249,7 +255,8 @@ def test_list(self, sources_list_d_content): with patch("glob.glob", return_value=["/etc/apt/sources.list.d/example.list"]), patch( "builtins.open", mock_open(read_data=sources_list_d_content) ): - command = UbuntuApplicationSourceManager() + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + command = UbuntuApplicationSourceManager(broker) sources = command.list() assert sources[0].name == "example.list" assert sources[0].sources == [ @@ -261,7 +268,8 @@ def test_list_raises_exception(self): with patch("glob.glob", return_value=["/etc/apt/sources.list.d/example.list"]), patch( "builtins.open", side_effect=OSError ): - command = UbuntuApplicationSourceManager() + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + command = UbuntuApplicationSourceManager(broker) with pytest.raises(SourceError) as exc_info: command.list() assert "Error listing application sources" in str(exc_info.value) @@ -273,17 +281,49 @@ def test_successfully_remove_gpg_key_and_source_list( parameters = ApplicationRemoveSourceParameters( gpg_key_name="example_source.gpg", source_list_file_name="example_source.list" ) - command = UbuntuApplicationSourceManager() + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + command = UbuntuApplicationSourceManager(broker) try: command.remove(parameters) except SourceError: self.fail("Remove GPG key raised DispatcherException unexpectedly!") + @patch("dispatcher.source.ubuntu_source_manager.verify_source", side_effect=DispatcherException('error')) + def test_failed_add_gpg_key_method(self, mock_verify_source): + parameters = ApplicationAddSourceParameters( + source_list_file_name="example_source.list", + sources="deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main", + gpg_key_uri="https://dl-ssl.google.com/linux/linux_signing_key.pub", + gpg_key_name="name" + ) + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + command = UbuntuApplicationSourceManager(broker) + with pytest.raises(SourceError) as ex: + command.add(parameters) + assert str(ex.value) == 'Source Gpg key URI verification check failed: error' + + + @patch("dispatcher.source.ubuntu_source_manager.verify_source") + def test_success_add_gpg_key_method(self, mock_verify_source): + mock_verify_source.return_value = True + parameters = ApplicationAddSourceParameters( + source_list_file_name="example_source.list", + sources="deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main", + gpg_key_uri="https://dl-ssl.google.com/linux/linux_signing_key.pub", + gpg_key_name="name" + ) + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + command = UbuntuApplicationSourceManager(broker) + with (patch("builtins.open", new_callable=mock_open()), + patch("dispatcher.source.ubuntu_source_manager.add_gpg_key")): + command.add(parameters) + def test_raises_when_space_check_fails(self): parameters = ApplicationRemoveSourceParameters( gpg_key_name="example_source.gpg", source_list_file_name="../example_source.list" ) - command = UbuntuApplicationSourceManager() + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + command = UbuntuApplicationSourceManager(broker) with pytest.raises(SourceError) as ex: command.remove(parameters) assert str(ex.value) == "Invalid file name: ../example_source.list" @@ -293,7 +333,8 @@ def test_raises_when_unable_to_remove_file(self, mock_remove_file): parameters = ApplicationRemoveSourceParameters( gpg_key_name="example_source.gpg", source_list_file_name="example_source.list" ) - command = UbuntuApplicationSourceManager() + broker = MockDispatcherBroker.build_mock_dispatcher_broker() + command = UbuntuApplicationSourceManager(broker) with pytest.raises(SourceError) as ex: command.remove(parameters) - assert str(ex.value) == "Error removing file: example_source.list" + assert str(ex.value) == "Error removing file: example_source.list" \ No newline at end of file diff --git a/inbm/integration-reloaded/test/source/SOURCE.sh b/inbm/integration-reloaded/test/source/SOURCE.sh index e8c69ef13..940384dc6 100755 --- a/inbm/integration-reloaded/test/source/SOURCE.sh +++ b/inbm/integration-reloaded/test/source/SOURCE.sh @@ -31,7 +31,10 @@ trap test_failed ERR echo "Starting source test." | systemd-cat -# OS tests +# Adding to trusted repo +inbc append --path trustedRepositories:https://deb.opera.com/ + +#OS tests inbc source os add --sources "$FAKE_SOURCE" grep "$FAKE_SOURCE" "$APT_SOURCES" inbc source os list 2>&1 | grep "$FAKE_SOURCE"