From bb072098f9dc5cd78d1fde0bd229a3aa25ff855f Mon Sep 17 00:00:00 2001 From: Katerina Koukiou Date: Mon, 7 Oct 2024 15:38:13 +0200 Subject: [PATCH 01/16] security: implement the support to install certificates to Anaconda Kickstart %certificate section is used. Submodule, data structures, parsing. Resolves: INSTALLER-4030 Patch modified by rvykydal. --- configure.ac | 1 + pyanaconda/core/kickstart/specification.py | 5 +- pyanaconda/kickstart.py | 2 + .../modules/boss/kickstart_manager/parser.py | 3 +- .../modules/common/constants/objects.py | 7 +++ .../modules/common/structures/security.py | 58 ++++++++++++++++++ pyanaconda/modules/security/Makefile.am | 2 + .../modules/security/certificates/Makefile.am | 21 +++++++ .../modules/security/certificates/__init__.py | 20 +++++++ .../security/certificates/certificates.py | 59 +++++++++++++++++++ .../certificates/certificates_interface.py | 37 ++++++++++++ pyanaconda/modules/security/kickstart.py | 11 ++++ pyanaconda/modules/security/security.py | 15 +++++ .../modules/security/test_module_security.py | 2 +- .../test_kickstart_specification.py | 27 ++++++++- 15 files changed, 265 insertions(+), 5 deletions(-) create mode 100644 pyanaconda/modules/common/structures/security.py create mode 100644 pyanaconda/modules/security/certificates/Makefile.am create mode 100644 pyanaconda/modules/security/certificates/__init__.py create mode 100644 pyanaconda/modules/security/certificates/certificates.py create mode 100644 pyanaconda/modules/security/certificates/certificates_interface.py diff --git a/configure.ac b/configure.ac index b675b967a9c..988415baeda 100644 --- a/configure.ac +++ b/configure.ac @@ -156,6 +156,7 @@ AC_CONFIG_FILES([Makefile pyanaconda/modules/boss/kickstart_manager/Makefile pyanaconda/modules/boss/module_manager/Makefile pyanaconda/modules/security/Makefile + pyanaconda/modules/security/certificates/Makefile pyanaconda/modules/timezone/Makefile pyanaconda/modules/network/Makefile pyanaconda/modules/network/firewall/Makefile diff --git a/pyanaconda/core/kickstart/specification.py b/pyanaconda/core/kickstart/specification.py index ab092feed06..3ea87d996c6 100644 --- a/pyanaconda/core/kickstart/specification.py +++ b/pyanaconda/core/kickstart/specification.py @@ -87,7 +87,10 @@ def __init__(self, specification): self.registerData(name, data) for name, data in specification.sections_data.items(): - self.registerSectionData(name, data) + if name is "certificate": + self.certificates.append(data()) + else: + self.registerSectionData(name, data) if specification.addons: self.addons = AddonRegistry() diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py index 58b0f2660bd..04a7225b69d 100644 --- a/pyanaconda/kickstart.py +++ b/pyanaconda/kickstart.py @@ -237,6 +237,7 @@ def setupSections(self): self.registerSection(NullSection(self.handler, sectionOpen="%traceback")) self.registerSection(NullSection(self.handler, sectionOpen="%packages")) self.registerSection(NullSection(self.handler, sectionOpen="%addon")) + self.registerSection(NullSection(self.handler, sectionOpen="%certificate")) class AnacondaKSParser(KickstartParser): @@ -258,6 +259,7 @@ def setupSections(self): self.registerSection(OnErrorScriptSection(self.handler, dataObj=self.scriptClass)) self.registerSection(UselessSection(self.handler, sectionOpen="%packages")) self.registerSection(UselessSection(self.handler, sectionOpen="%addon")) + self.registerSection(UselessSection(self.handler, sectionOpen="%certificate")) def preScriptPass(f): diff --git a/pyanaconda/modules/boss/kickstart_manager/parser.py b/pyanaconda/modules/boss/kickstart_manager/parser.py index 7d10b9bffae..97644e7b46a 100644 --- a/pyanaconda/modules/boss/kickstart_manager/parser.py +++ b/pyanaconda/modules/boss/kickstart_manager/parser.py @@ -27,7 +27,8 @@ ) VALID_SECTIONS_ANACONDA = [ - "%pre", "%pre-install", "%post", "%onerror", "%traceback", "%packages", "%addon" + "%certificate", "%pre", "%pre-install", "%post", "%onerror", "%traceback", "%packages", + "%addon" ] diff --git a/pyanaconda/modules/common/constants/objects.py b/pyanaconda/modules/common/constants/objects.py index 573888a1614..e43d929301e 100644 --- a/pyanaconda/modules/common/constants/objects.py +++ b/pyanaconda/modules/common/constants/objects.py @@ -24,6 +24,7 @@ PARTITIONING_NAMESPACE, RHSM_NAMESPACE, RUNTIME_NAMESPACE, + SECURITY_NAMESPACE, STORAGE_NAMESPACE, ) @@ -155,3 +156,9 @@ namespace=RHSM_NAMESPACE, basename="Syspurpose" ) + +# Security objects +CERTIFICATES = DBusObjectIdentifier( + namespace=SECURITY_NAMESPACE, + basename="Certificates" +) diff --git a/pyanaconda/modules/common/structures/security.py b/pyanaconda/modules/common/structures/security.py new file mode 100644 index 00000000000..c0c93629ba1 --- /dev/null +++ b/pyanaconda/modules/common/structures/security.py @@ -0,0 +1,58 @@ +# +# DBus structures for the storage data. +# +# Copyright (C) 2024 Red Hat, Inc. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from dasbus.structure import DBusData +from dasbus.typing import * # pylint: disable=wildcard-import + +__all__ = ["CertificateData"] + + +class CertificateData(DBusData): + """Structure for the certificate data.""" + + def __init__(self): + self._filename = "" + self._cert = "" + self._dir = "" + + @property + def filename(self) -> Str: + """The certificate file name.""" + return self._filename + + @filename.setter + def filename(self, value: Str) -> None: + self._filename = value + + @property + def cert(self) -> Str: + """The certificate content.""" + return self._cert + + @cert.setter + def cert(self, value: Str) -> None: + self._cert = value + + @property + def dir(self) -> Str: + """The certificate directory.""" + return self._dir + + @dir.setter + def dir(self, value: Str) -> None: + self._dir = value diff --git a/pyanaconda/modules/security/Makefile.am b/pyanaconda/modules/security/Makefile.am index b141af08c79..ee20d747ac6 100644 --- a/pyanaconda/modules/security/Makefile.am +++ b/pyanaconda/modules/security/Makefile.am @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +SUBDIRS = certificates + pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME) securitydir = $(pkgpyexecdir)/modules/security dist_security_DATA = $(wildcard $(srcdir)/*.py) diff --git a/pyanaconda/modules/security/certificates/Makefile.am b/pyanaconda/modules/security/certificates/Makefile.am new file mode 100644 index 00000000000..931ea79102d --- /dev/null +++ b/pyanaconda/modules/security/certificates/Makefile.am @@ -0,0 +1,21 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +pkgpyexecdir = $(pyexecdir)/py$(PACKAGE_NAME) +certificatesdir = $(pkgpyexecdir)/modules/security/certificates +dist_certificates_DATA = $(wildcard $(srcdir)/*.py) + +MAINTAINERCLEANFILES = Makefile.in diff --git a/pyanaconda/modules/security/certificates/__init__.py b/pyanaconda/modules/security/certificates/__init__.py new file mode 100644 index 00000000000..c3bdd9f8f2f --- /dev/null +++ b/pyanaconda/modules/security/certificates/__init__.py @@ -0,0 +1,20 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +from pyanaconda.modules.security.certificates.certificates import CertificatesModule + +__all__ = ["CertificatesModule"] diff --git a/pyanaconda/modules/security/certificates/certificates.py b/pyanaconda/modules/security/certificates/certificates.py new file mode 100644 index 00000000000..a81496c3b43 --- /dev/null +++ b/pyanaconda/modules/security/certificates/certificates.py @@ -0,0 +1,59 @@ +# +# Certificate module +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +from pykickstart.parser import Certificate + +from pyanaconda.anaconda_loggers import get_module_logger +from pyanaconda.core.dbus import DBus +from pyanaconda.modules.common.base import KickstartBaseModule +from pyanaconda.modules.common.constants.objects import CERTIFICATES +from pyanaconda.modules.common.structures.security import CertificateData +from pyanaconda.modules.security.certificates.certificates_interface import ( + CertificatesInterface, +) + +log = get_module_logger(__name__) + +class CertificatesModule(KickstartBaseModule): + """The certificates installation module.""" + + def __init__(self): + super().__init__() + + self._certificates = [] + + def publish(self): + """Publish the module.""" + DBus.publish_object(CERTIFICATES.object_path, CertificatesInterface(self)) + + def process_kickstart(self, data): + """Process the kickstart data.""" + for cert in data.certificates: + cert_data = CertificateData() + cert_data.filename = cert.filename + cert_data.cert = cert.cert + if cert.dir: + cert_data.dir = cert.dir + self._certificates.append(cert_data) + + def setup_kickstart(self, data): + """Setup the kickstart data.""" + for cert in self._certificates: + cert_ksdata = Certificate(cert=cert.cert, filename=cert.filename, dir=cert.dir) + data.certificates.append(cert_ksdata) diff --git a/pyanaconda/modules/security/certificates/certificates_interface.py b/pyanaconda/modules/security/certificates/certificates_interface.py new file mode 100644 index 00000000000..a4e3076a599 --- /dev/null +++ b/pyanaconda/modules/security/certificates/certificates_interface.py @@ -0,0 +1,37 @@ +# +# DBus interface for the certificate module. +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +from dasbus.server.interface import dbus_interface +from dasbus.typing import * # pylint: disable=wildcard-import + +from pyanaconda.modules.common.base import KickstartModuleInterfaceTemplate +from pyanaconda.modules.common.constants.objects import CERTIFICATES +from pyanaconda.modules.common.structures.security import CertificateData + + +@dbus_interface(CERTIFICATES.interface_name) +class CertificatesInterface(KickstartModuleInterfaceTemplate): + """DBus interface for the certificate installation module.""" + + def GetCertificates(self) -> List[Str]: + """Get all certificates to be installed the system. + + :return: a list of certificates names with their content + """ + return [CertificateData.to_structure(cert) for cert in self.implementation.get_certificates()] diff --git a/pyanaconda/modules/security/kickstart.py b/pyanaconda/modules/security/kickstart.py index 915d1a5340f..357701f51a3 100644 --- a/pyanaconda/modules/security/kickstart.py +++ b/pyanaconda/modules/security/kickstart.py @@ -17,6 +17,9 @@ # License and may only be used or replicated with the express permission of # Red Hat, Inc. # +from pykickstart.parser import Certificate +from pykickstart.sections import CertificateSection + from pyanaconda.core.kickstart import KickstartSpecification from pyanaconda.core.kickstart import commands as COMMANDS @@ -28,3 +31,11 @@ class SecurityKickstartSpecification(KickstartSpecification): "selinux": COMMANDS.SELinux, "realm": COMMANDS.Realm } + + sections = { + "certificate": CertificateSection + } + + sections_data = { + "certificate": Certificate + } diff --git a/pyanaconda/modules/security/security.py b/pyanaconda/modules/security/security.py index 2f68e77ebae..95506dc9dad 100644 --- a/pyanaconda/modules/security/security.py +++ b/pyanaconda/modules/security/security.py @@ -29,6 +29,8 @@ from pyanaconda.modules.common.containers import TaskContainer from pyanaconda.modules.common.structures.realm import RealmData from pyanaconda.modules.common.structures.requirement import Requirement +from pyanaconda.modules.common.submodule_manager import SubmoduleManager +from pyanaconda.modules.security.certificates import CertificatesModule from pyanaconda.modules.security.constants import SELinuxMode from pyanaconda.modules.security.installation import ( ConfigureAuthselectTask, @@ -51,6 +53,12 @@ class SecurityService(KickstartService): def __init__(self): super().__init__() + # Initialize modules. + self._modules = SubmoduleManager() + + self._certificates_module = CertificatesModule() + self._modules.add_module(self._certificates_module) + self.selinux_changed = Signal() self._selinux = SELinuxMode.DEFAULT @@ -66,6 +74,9 @@ def __init__(self): def publish(self): """Publish the module.""" TaskContainer.set_namespace(SECURITY.namespace) + + self._modules.publish_modules() + DBus.publish_object(SECURITY.object_path, SecurityInterface(self)) DBus.register_service(SECURITY.service_name) @@ -76,6 +87,8 @@ def kickstart_specification(self): def process_kickstart(self, data): """Process the kickstart data.""" + self._modules.process_kickstart(data) + if data.selinux.selinux is not None: self.set_selinux(SELinuxMode(data.selinux.selinux)) @@ -92,6 +105,8 @@ def process_kickstart(self, data): def setup_kickstart(self, data): """Set up the kickstart data.""" + self._modules.setup_kickstart(data) + if self.selinux != SELinuxMode.DEFAULT: data.selinux.selinux = self.selinux.value diff --git a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_security.py b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_security.py index 90e8d1ca7ec..f760e412d60 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_security.py +++ b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_security.py @@ -83,7 +83,7 @@ def test_kickstart_properties(self): """Test kickstart properties.""" assert self.security_interface.KickstartCommands == \ ["authselect", "selinux", "realm"] - assert self.security_interface.KickstartSections == [] + assert self.security_interface.KickstartSections == ["certificate"] assert self.security_interface.KickstartAddons == [] self.callback.assert_not_called() diff --git a/tests/unit_tests/pyanaconda_tests/test_kickstart_specification.py b/tests/unit_tests/pyanaconda_tests/test_kickstart_specification.py index 05055d4d6bc..627b54ee830 100644 --- a/tests/unit_tests/pyanaconda_tests/test_kickstart_specification.py +++ b/tests/unit_tests/pyanaconda_tests/test_kickstart_specification.py @@ -27,8 +27,8 @@ from pykickstart.commands.user import F19_UserData, F24_User from pykickstart.errors import KickstartParseError from pykickstart.options import KSOptionParser -from pykickstart.parser import Packages -from pykickstart.sections import PackageSection +from pykickstart.parser import Certificate, Packages +from pykickstart.sections import CertificateSection, PackageSection from pykickstart.version import F30 from pykickstart.version import isRHEL as is_rhel @@ -213,6 +213,16 @@ class SpecificationF(KickstartSpecification): "my_test_2": TestData2 } + class SpecificationG(KickstartSpecification): + + sections = { + "certificate": CertificateSection, + } + + sections_data = { + "certificate": Certificate, + } + def setUp(self): self.maxDiff = None @@ -391,6 +401,19 @@ def test_addons_specification(self): %end """) + def test_certificates_specification(self): + specification = self.SpecificationG + + ks_in = """ + %certificate --filename=cert1.pem + -----BEGIN CERTIFICATE----- + MIIDazCCAlOgAwIBAgIJAJzQz1Zz1Zz1MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD + -----END CERTIFICATE----- + %end + """ + handler = self.parse_kickstart(specification, ks_in) + assert isinstance(handler.certificates[0], Certificate) + assert len(handler.certificates) == 1 class ModuleSpecificationsTestCase(unittest.TestCase): """Test the kickstart module specifications.""" From 2da79853ad65fd885af0a7d21e253744b74a141b Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Fri, 13 Dec 2024 12:37:07 +0100 Subject: [PATCH 02/16] kickstart: extend section specification for list of section data Unlike the %packages section where the data of all sections are merged into single data object the %certificates section holds the per instance section data in a list. Related: INSTALLER-4030 --- pyanaconda/core/kickstart/specification.py | 48 +++++++++++--- pyanaconda/modules/security/kickstart.py | 4 +- .../modules/security/test_module_security.py | 62 +++++++++++++++++++ .../test_kickstart_specification.py | 47 +++++++++----- 4 files changed, 137 insertions(+), 24 deletions(-) diff --git a/pyanaconda/core/kickstart/specification.py b/pyanaconda/core/kickstart/specification.py index 3ea87d996c6..5c0c76ebbc1 100644 --- a/pyanaconda/core/kickstart/specification.py +++ b/pyanaconda/core/kickstart/specification.py @@ -53,8 +53,14 @@ class KickstartSpecification: classes that represent them sections - mapping of kickstart sections names to classes that represent them + value is a class or a tuple (class, section_data_class) + where section_data_class is a value to be passed to dataObj + class argument (typically the corresponding sections_data class) sections_data - mapping of kickstart sections data names to classes that represent them + value is a class or a tuple (class, data_list_name) + where data_list_name is the name of the attribute holding + list of the section data objects of the class addons - mapping of kickstart addons names to classes that represent them @@ -73,6 +79,25 @@ class NoKickstartSpecification(KickstartSpecification): pass +class SectionDataListStrWrapper(): + """A wrapper for generating string from a list of kickstart data.""" + def __init__(self, data_list, data): + """Initializer. + + :param data_list: list of section data objects + :param data: class required for the object to be included in the string + """ + self._data_list = data_list + self._data = data + + def __str__(self): + retval = [] + for data_obj in self._data_list: + if isinstance(data_obj, self._data): + retval.append(data_obj.__str__()) + return "".join(retval) + + class KickstartSpecificationHandler(KickstartHandler): """Handler defined by a kickstart specification.""" @@ -87,10 +112,7 @@ def __init__(self, specification): self.registerData(name, data) for name, data in specification.sections_data.items(): - if name is "certificate": - self.certificates.append(data()) - else: - self.registerSectionData(name, data) + self.registerSectionData(name, data) if specification.addons: self.addons = AddonRegistry() @@ -102,8 +124,16 @@ def __init__(self, specification): def registerSectionData(self, name, data): """Register data used by a section.""" - obj = data() - setattr(self, name, obj) + if isinstance(data, tuple): + # Multiple data objects (section instances) stored in a list + data, data_list_name = data + data_list = [] + setattr(self, data_list_name, data_list) + obj = SectionDataListStrWrapper(data_list, data) + else: + # Single data object for all section instances + obj = data() + setattr(self, name, obj) self._registerWriteOrder(obj) def registerAddonData(self, name, data): @@ -129,7 +159,11 @@ def __init__(self, handler, specification): super().__init__(handler) for section in specification.sections.values(): - self.registerSection(section(handler)) + if isinstance(section, tuple): + section_cls, data_obj = section + self.registerSection(section_cls(handler, dataObj=data_obj)) + else: + self.registerSection(section(handler)) if specification.addons: self.registerSection(AddonSection(handler)) diff --git a/pyanaconda/modules/security/kickstart.py b/pyanaconda/modules/security/kickstart.py index 357701f51a3..de16e0f8bb6 100644 --- a/pyanaconda/modules/security/kickstart.py +++ b/pyanaconda/modules/security/kickstart.py @@ -33,9 +33,9 @@ class SecurityKickstartSpecification(KickstartSpecification): } sections = { - "certificate": CertificateSection + "certificate": (CertificateSection, Certificate) } sections_data = { - "certificate": Certificate + "certificate": (Certificate, "certificates") } diff --git a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_security.py b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_security.py index f760e412d60..8cd9e0e4102 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_security.py +++ b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_security.py @@ -170,6 +170,68 @@ def test_realm_kickstart(self): """ self._test_kickstart(ks_in, ks_out) + def test_certificates_kickstart(self): + """Test the %certificates section.""" + ks_in = """ + %certificate --filename=rvtest.pem --dir=/cert_dir + -----BEGIN CERTIFICATE----- + MIIBjTCCATOgAwIBAgIUWR5HO3v/0I80Ne0jQWVZFODuWLEwCgYIKoZIzj0EAwIw + FDESMBAGA1UEAwwJUlZURVNUIENBMB4XDTI0MTEyMDEzNTk1N1oXDTM0MTExODEz + NTk1N1owFDESMBAGA1UEAwwJUlZURVNUIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0D + AQcDQgAELghFKGEgS8+5/2nx50W0xOqTrKc2Jz/rD/jfL0m4z4fkeAslCOkIKv74 + 0wfBXMngxi+OF/b3Vh8FmokuNBQO5qNjMGEwHQYDVR0OBBYEFOJarl9Xkd13sLzI + mHqv6aESlvuCMB8GA1UdIwQYMBaAFOJarl9Xkd13sLzImHqv6aESlvuCMA8GA1Ud + EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMCA0gAMEUCIAet + 7nyre42ReoRKoyHWLDsQmQDzoyU3FQdC0cViqOtrAiEAxYIL+XTTp7Xy9RNE4Xg7 + yNWXfdraC/AfMM8fqsxlVJM= + -----END CERTIFICATE----- + %end + + %certificate --filename=rvtest2.pem --dir=/cert_dir2 + -----BEGIN CERTIFICATE----- + MIIBkTCCATegAwIBAgIUN6r4TjFJqP/TS6U25iOGL2Wt/6kwCgYIKoZIzj0EAwIw + FjEUMBIGA1UEAwwLUlZURVNUIDIgQ0EwHhcNMjQxMTIwMTQwMzIxWhcNMzQxMTE4 + MTQwMzIxWjAWMRQwEgYDVQQDDAtSVlRFU1QgMiBDQTBZMBMGByqGSM49AgEGCCqG + SM49AwEHA0IABOtXBMEhtcH43dIDHkelODXrSWQQ8PW7oo8lQUEYTNAL1rpWJJDD + 1u+bpLe62Z0kzYK0CpeKuXFfwGrzx7eA6vajYzBhMB0GA1UdDgQWBBStV+z7SZSi + YXlamkx+xjm/W1sMSTAfBgNVHSMEGDAWgBStV+z7SZSiYXlamkx+xjm/W1sMSTAP + BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAgNIADBF + AiEAkQjETC3Yx2xOkA+R0/YR+R+QqpR8p1fd/cGKWFUYxSoCIEuDJcfvPJfFYdzn + CFOCLuymezWz+1rdIXLU1+XStLuB + -----END CERTIFICATE----- + %end + """ + ks_out = """ + %certificate --filename=rvtest.pem --dir=/cert_dir + -----BEGIN CERTIFICATE----- + MIIBjTCCATOgAwIBAgIUWR5HO3v/0I80Ne0jQWVZFODuWLEwCgYIKoZIzj0EAwIw + FDESMBAGA1UEAwwJUlZURVNUIENBMB4XDTI0MTEyMDEzNTk1N1oXDTM0MTExODEz + NTk1N1owFDESMBAGA1UEAwwJUlZURVNUIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0D + AQcDQgAELghFKGEgS8+5/2nx50W0xOqTrKc2Jz/rD/jfL0m4z4fkeAslCOkIKv74 + 0wfBXMngxi+OF/b3Vh8FmokuNBQO5qNjMGEwHQYDVR0OBBYEFOJarl9Xkd13sLzI + mHqv6aESlvuCMB8GA1UdIwQYMBaAFOJarl9Xkd13sLzImHqv6aESlvuCMA8GA1Ud + EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMCA0gAMEUCIAet + 7nyre42ReoRKoyHWLDsQmQDzoyU3FQdC0cViqOtrAiEAxYIL+XTTp7Xy9RNE4Xg7 + yNWXfdraC/AfMM8fqsxlVJM= + -----END CERTIFICATE----- + %end + + %certificate --filename=rvtest2.pem --dir=/cert_dir2 + -----BEGIN CERTIFICATE----- + MIIBkTCCATegAwIBAgIUN6r4TjFJqP/TS6U25iOGL2Wt/6kwCgYIKoZIzj0EAwIw + FjEUMBIGA1UEAwwLUlZURVNUIDIgQ0EwHhcNMjQxMTIwMTQwMzIxWhcNMzQxMTE4 + MTQwMzIxWjAWMRQwEgYDVQQDDAtSVlRFU1QgMiBDQTBZMBMGByqGSM49AgEGCCqG + SM49AwEHA0IABOtXBMEhtcH43dIDHkelODXrSWQQ8PW7oo8lQUEYTNAL1rpWJJDD + 1u+bpLe62Z0kzYK0CpeKuXFfwGrzx7eA6vajYzBhMB0GA1UdDgQWBBStV+z7SZSi + YXlamkx+xjm/W1sMSTAfBgNVHSMEGDAWgBStV+z7SZSiYXlamkx+xjm/W1sMSTAP + BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAgNIADBF + AiEAkQjETC3Yx2xOkA+R0/YR+R+QqpR8p1fd/cGKWFUYxSoCIEuDJcfvPJfFYdzn + CFOCLuymezWz+1rdIXLU1+XStLuB + -----END CERTIFICATE----- + %end + """ + self._test_kickstart(ks_in, ks_out) + @patch_dbus_publish_object def test_realm_discover_default(self, publisher): """Test module in default state with realm discover task.""" diff --git a/tests/unit_tests/pyanaconda_tests/test_kickstart_specification.py b/tests/unit_tests/pyanaconda_tests/test_kickstart_specification.py index 627b54ee830..65360b171a2 100644 --- a/tests/unit_tests/pyanaconda_tests/test_kickstart_specification.py +++ b/tests/unit_tests/pyanaconda_tests/test_kickstart_specification.py @@ -216,11 +216,11 @@ class SpecificationF(KickstartSpecification): class SpecificationG(KickstartSpecification): sections = { - "certificate": CertificateSection, + "certificate": (CertificateSection, Certificate) } sections_data = { - "certificate": Certificate, + "certificate": (Certificate, "certificates") } def setUp(self): @@ -274,6 +274,36 @@ def test_section_specification(self): with pytest.raises(KickstartParseError): self.parse_kickstart(specification, "xconfig") + def test_section_specification_with_data_objects(self): + """Test a specification with a section handling multiple data objects.""" + specification = self.SpecificationG + + ks_in = """ + %certificate --filename=cert1.pem --dir=/cert_dir + -----BEGIN CERTIFICATE----- + MIIDazCCAlOgAwIBAgIJAJzQz1Zz1Zz1MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD + -----END CERTIFICATE----- + %end + + %certificate --filename=cert2.pem --dir=/cert_dir + -----BEGIN CERTIFICATE----- + XIIDazCCAlOgAwIBAgIJAJzQz1Zz1Zz1MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD + -----END CERTIFICATE----- + %end + """ + + with pytest.raises(KickstartParseError): + self.parse_kickstart(specification, "xconfig") + + self.parse_kickstart(specification, "") + handler = self.parse_kickstart(specification, ks_in) + assert len(handler.certificates) == 2 + cert1, cert2 = handler.certificates + assert isinstance(cert1, Certificate) + assert isinstance(cert2, Certificate) + assert cert1.filename == "cert1.pem" + assert cert2.filename == "cert2.pem" + def test_full_specification(self): """Test a full specification.""" specification = self.SpecificationE @@ -401,19 +431,6 @@ def test_addons_specification(self): %end """) - def test_certificates_specification(self): - specification = self.SpecificationG - - ks_in = """ - %certificate --filename=cert1.pem - -----BEGIN CERTIFICATE----- - MIIDazCCAlOgAwIBAgIJAJzQz1Zz1Zz1MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD - -----END CERTIFICATE----- - %end - """ - handler = self.parse_kickstart(specification, ks_in) - assert isinstance(handler.certificates[0], Certificate) - assert len(handler.certificates) == 1 class ModuleSpecificationsTestCase(unittest.TestCase): """Test the kickstart module specifications.""" From 7354c6f0a936b1f9b465c3a79137d14196f5e69d Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Tue, 26 Nov 2024 14:54:17 +0100 Subject: [PATCH 03/16] security: add API: Certificate getter Resolves: INSTALLER-4030 --- .../security/certificates/certificates.py | 21 +++- .../certificates/certificates_interface.py | 13 ++- .../security/test_module_certificates.py | 107 ++++++++++++++++++ 3 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py diff --git a/pyanaconda/modules/security/certificates/certificates.py b/pyanaconda/modules/security/certificates/certificates.py index a81496c3b43..7abbc36a031 100644 --- a/pyanaconda/modules/security/certificates/certificates.py +++ b/pyanaconda/modules/security/certificates/certificates.py @@ -21,6 +21,7 @@ from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core.dbus import DBus +from pyanaconda.core.signal import Signal from pyanaconda.modules.common.base import KickstartBaseModule from pyanaconda.modules.common.constants.objects import CERTIFICATES from pyanaconda.modules.common.structures.security import CertificateData @@ -30,12 +31,14 @@ log = get_module_logger(__name__) + class CertificatesModule(KickstartBaseModule): """The certificates installation module.""" def __init__(self): super().__init__() + self.certificates_changed = Signal() self._certificates = [] def publish(self): @@ -44,16 +47,32 @@ def publish(self): def process_kickstart(self, data): """Process the kickstart data.""" + certificates = [] for cert in data.certificates: cert_data = CertificateData() cert_data.filename = cert.filename cert_data.cert = cert.cert if cert.dir: cert_data.dir = cert.dir - self._certificates.append(cert_data) + certificates.append(cert_data) + self.set_certificates(certificates) def setup_kickstart(self, data): """Setup the kickstart data.""" for cert in self._certificates: cert_ksdata = Certificate(cert=cert.cert, filename=cert.filename, dir=cert.dir) data.certificates.append(cert_ksdata) + + @property + def certificates(self): + """Return the certificates.""" + return self._certificates + + def set_certificates(self, certificates): + """Set the certificates.""" + self._certificates = certificates + self.certificates_changed.emit() + # as there is no public setter in the DBus API, we need to emit + # the properties changed signal here manually + self.module_properties_changed.emit() + log.debug("Certificates is set to %s.", certificates) diff --git a/pyanaconda/modules/security/certificates/certificates_interface.py b/pyanaconda/modules/security/certificates/certificates_interface.py index a4e3076a599..89318def51a 100644 --- a/pyanaconda/modules/security/certificates/certificates_interface.py +++ b/pyanaconda/modules/security/certificates/certificates_interface.py @@ -29,9 +29,14 @@ class CertificatesInterface(KickstartModuleInterfaceTemplate): """DBus interface for the certificate installation module.""" - def GetCertificates(self) -> List[Str]: - """Get all certificates to be installed the system. + def connect_signals(self): + super().connect_signals() + self.watch_property("Certificates", self.implementation.certificates_changed) - :return: a list of certificates names with their content + @property + def Certificates(self) -> List[Structure]: + """All certificates. + + :return: a list of certificate DBus Structures """ - return [CertificateData.to_structure(cert) for cert in self.implementation.get_certificates()] + return CertificateData.to_structure_list(self.implementation.certificates) diff --git a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py new file mode 100644 index 00000000000..fc2480c0bcc --- /dev/null +++ b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py @@ -0,0 +1,107 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Radek Vykydal +# +import unittest + +from dasbus.typing import * # pylint: disable=wildcard-import + +from pyanaconda.modules.common.constants.objects import CERTIFICATES +from pyanaconda.modules.common.structures.security import CertificateData +from pyanaconda.modules.security.certificates.certificates import CertificatesModule +from pyanaconda.modules.security.certificates.certificates_interface import ( + CertificatesInterface, +) +from tests.unit_tests.pyanaconda_tests import ( + check_dbus_property, + check_task_creation, + patch_dbus_publish_object, +) + +CERT_RVTEST = """-----BEGIN CERTIFICATE----- +MIIBjTCCATOgAwIBAgIUWR5HO3v/0I80Ne0jQWVZFODuWLEwCgYIKoZIzj0EAwIw +FDESMBAGA1UEAwwJUlZURVNUIENBMB4XDTI0MTEyMDEzNTk1N1oXDTM0MTExODEz +NTk1N1owFDESMBAGA1UEAwwJUlZURVNUIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAELghFKGEgS8+5/2nx50W0xOqTrKc2Jz/rD/jfL0m4z4fkeAslCOkIKv74 +0wfBXMngxi+OF/b3Vh8FmokuNBQO5qNjMGEwHQYDVR0OBBYEFOJarl9Xkd13sLzI +mHqv6aESlvuCMB8GA1UdIwQYMBaAFOJarl9Xkd13sLzImHqv6aESlvuCMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMCA0gAMEUCIAet +7nyre42ReoRKoyHWLDsQmQDzoyU3FQdC0cViqOtrAiEAxYIL+XTTp7Xy9RNE4Xg7 +yNWXfdraC/AfMM8fqsxlVJM= +-----END CERTIFICATE-----""" + +CERT_RVTEST2 = """-----BEGIN CERTIFICATE----- +MIIBkTCCATegAwIBAgIUN6r4TjFJqP/TS6U25iOGL2Wt/6kwCgYIKoZIzj0EAwIw +FjEUMBIGA1UEAwwLUlZURVNUIDIgQ0EwHhcNMjQxMTIwMTQwMzIxWhcNMzQxMTE4 +MTQwMzIxWjAWMRQwEgYDVQQDDAtSVlRFU1QgMiBDQTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABOtXBMEhtcH43dIDHkelODXrSWQQ8PW7oo8lQUEYTNAL1rpWJJDD +1u+bpLe62Z0kzYK0CpeKuXFfwGrzx7eA6vajYzBhMB0GA1UdDgQWBBStV+z7SZSi +YXlamkx+xjm/W1sMSTAfBgNVHSMEGDAWgBStV+z7SZSiYXlamkx+xjm/W1sMSTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAgNIADBF +AiEAkQjETC3Yx2xOkA+R0/YR+R+QqpR8p1fd/cGKWFUYxSoCIEuDJcfvPJfFYdzn +CFOCLuymezWz+1rdIXLU1+XStLuB +-----END CERTIFICATE-----""" + + +class CertificatesInterfaceTestCase(unittest.TestCase): + """Test DBus interface of the Certificates module.""" + + def setUp(self): + """Set up the module.""" + self.certificates_module = CertificatesModule() + self.certificates_interface = CertificatesInterface(self.certificates_module) + + def _check_dbus_property(self, *args, **kwargs): + check_dbus_property( + CERTIFICATES, + self.certificates_interface, + *args, **kwargs + ) + + @staticmethod + def _get_dbus_certs(certs): + return [ + { + 'cert': get_variant(Str, cert), + 'filename': get_variant(Str, filename), + 'dir': get_variant(Str, cdir) + } + for cert, filename, cdir in certs + ] + + def _iface_certificates_setter(self): + """Provide setter for testing read-only Certificates property.""" + return lambda value: self.certificates_module.set_certificates( + CertificateData.from_structure_list(value) + ) + + def test_certificates_property(self): + """Test the certificates property.""" + assert self.certificates_interface.Certificates == [] + + certs_value = self._get_dbus_certs([ + (CERT_RVTEST, 'rvtest.pem', '/etc/pki/ca-trust/extracted/pem'), + (CERT_RVTEST2, 'rvtest2.pem', ''), + ]) + + self._check_dbus_property( + "Certificates", + certs_value, + # read-only property, so provide setter + setter=self._iface_certificates_setter() + ) From 1e76e03362cf67d3b6ce303ecf0a2930dd908875 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Fri, 13 Dec 2024 18:47:48 +0100 Subject: [PATCH 04/16] security: add API to import certificates to Anaconda environment Related: INSTALLER-4030 --- .../security/certificates/certificates.py | 11 +++++ .../certificates/certificates_interface.py | 10 +++++ .../security/certificates/installation.py | 43 +++++++++++++++++++ .../security/test_module_certificates.py | 28 ++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 pyanaconda/modules/security/certificates/installation.py diff --git a/pyanaconda/modules/security/certificates/certificates.py b/pyanaconda/modules/security/certificates/certificates.py index 7abbc36a031..8695661cc91 100644 --- a/pyanaconda/modules/security/certificates/certificates.py +++ b/pyanaconda/modules/security/certificates/certificates.py @@ -28,6 +28,7 @@ from pyanaconda.modules.security.certificates.certificates_interface import ( CertificatesInterface, ) +from pyanaconda.modules.security.certificates.installation import ImportCertificatesTask log = get_module_logger(__name__) @@ -76,3 +77,13 @@ def set_certificates(self, certificates): # the properties changed signal here manually self.module_properties_changed.emit() log.debug("Certificates is set to %s.", certificates) + + def import_with_task(self): + """Import certificates into the installer environment + + :return: an installation task + """ + return ImportCertificatesTask( + sysroot="/", + certificates=self.certificates, + ) diff --git a/pyanaconda/modules/security/certificates/certificates_interface.py b/pyanaconda/modules/security/certificates/certificates_interface.py index 89318def51a..b06f302226d 100644 --- a/pyanaconda/modules/security/certificates/certificates_interface.py +++ b/pyanaconda/modules/security/certificates/certificates_interface.py @@ -22,6 +22,7 @@ from pyanaconda.modules.common.base import KickstartModuleInterfaceTemplate from pyanaconda.modules.common.constants.objects import CERTIFICATES +from pyanaconda.modules.common.containers import TaskContainer from pyanaconda.modules.common.structures.security import CertificateData @@ -40,3 +41,12 @@ def Certificates(self) -> List[Structure]: :return: a list of certificate DBus Structures """ return CertificateData.to_structure_list(self.implementation.certificates) + + def ImportWithTask(self) -> ObjPath: + """Import certificates in the installer environment + + :return: a DBus path of the import task + """ + return TaskContainer.to_object_path( + self.implementation.import_with_task() + ) diff --git a/pyanaconda/modules/security/certificates/installation.py b/pyanaconda/modules/security/certificates/installation.py new file mode 100644 index 00000000000..7059b337545 --- /dev/null +++ b/pyanaconda/modules/security/certificates/installation.py @@ -0,0 +1,43 @@ +# +# Copyright (C) 2024 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +from pyanaconda.anaconda_loggers import get_module_logger +from pyanaconda.modules.common.task import Task + +log = get_module_logger(__name__) + + +class ImportCertificatesTask(Task): + """Task for importing certificates into a system.""" + + def __init__(self, sysroot, certificates): + """Create a new certificates import task. + + :param str sysroot: a path to the root of the target system + :param certificates: list of certificate data holders + """ + super().__init__() + self._sysroot = sysroot + self._certificates = certificates + + @property + def name(self): + return "Import CA certificates" + + def run(self): + for cert in self._certificates: + log.debug("Importing certificate with filename: %s dir: %s", cert.filename, cert.dir) diff --git a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py index fc2480c0bcc..df4d0d5f38d 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py +++ b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py @@ -27,6 +27,7 @@ from pyanaconda.modules.security.certificates.certificates_interface import ( CertificatesInterface, ) +from pyanaconda.modules.security.certificates.installation import ImportCertificatesTask from tests.unit_tests.pyanaconda_tests import ( check_dbus_property, check_task_creation, @@ -105,3 +106,30 @@ def test_certificates_property(self): # read-only property, so provide setter setter=self._iface_certificates_setter() ) + + @patch_dbus_publish_object + def test_import_with_task_default(self, publisher): + """Test the ImportWithTask method""" + task_path = self.certificates_interface.ImportWithTask() + obj = check_task_creation(task_path, publisher, ImportCertificatesTask) + assert obj.implementation._sysroot == "/" + + @patch_dbus_publish_object + def test_import_with_task_configured(self, publisher): + """Test the ImportWithTask method""" + c1 = (CERT_RVTEST, 'rvtest.pem', '/etc/pki/ca-trust/extracted/pem') + c2 = (CERT_RVTEST2, 'rvtest2.pem', '') + certs_value = self._get_dbus_certs([ + c1, + c2, + ]) + set_certificates = self._iface_certificates_setter() + set_certificates(certs_value) + + task_path = self.certificates_interface.ImportWithTask() + obj = check_task_creation(task_path, publisher, ImportCertificatesTask) + assert obj.implementation._sysroot == "/" + assert len(obj.implementation._certificates) == 2 + obj_c1, obj_c2 = obj.implementation._certificates + assert c1 == (obj_c1.cert, obj_c1.filename, obj_c1.dir) + assert c2 == (obj_c2.cert, obj_c2.filename, obj_c2.dir) From 9d7ff2aebe8029fe05f8b0a2dadc95a8b4feacce Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Tue, 26 Nov 2024 10:12:24 +0100 Subject: [PATCH 05/16] security: import certificates early after Anaconda start The certificates imported in initramfs are already imported earlier by a service. Resolves: INSTALLER-4030 --- pyanaconda/startup_utils.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pyanaconda/startup_utils.py b/pyanaconda/startup_utils.py index c3d20aa416e..a6f53b225d3 100644 --- a/pyanaconda/startup_utils.py +++ b/pyanaconda/startup_utils.py @@ -64,7 +64,10 @@ locale_has_translation, setup_locale, ) -from pyanaconda.modules.common.constants.objects import STORAGE_CHECKER +from pyanaconda.modules.common.constants.objects import ( + CERTIFICATES, + STORAGE_CHECKER, +) from pyanaconda.modules.common.constants.services import ( LOCALIZATION, RUNTIME, @@ -78,7 +81,7 @@ GeolocationData, TimeSourceData, ) -from pyanaconda.modules.common.task import wait_for_task +from pyanaconda.modules.common.task import sync_run_task, wait_for_task from pyanaconda.modules.common.util import is_module_available from pyanaconda.screensaver import inhibit_screensaver @@ -642,6 +645,19 @@ def initialize_security(): if not flags.automatedInstall: security_proxy.FingerprintAuthEnabled = True + # Import certificates from kickstart + # In most cases they have already been imported from kickstart + # during the initramfs stage kickstart processing and passed to the + # installer enviroment either from initramfs or early after + # switch root by a dedicated systemd service. + # However they would not be already imported for example in case the + # certificate is included by a snippet created in kickstart %pre + # section. + certificates_proxy = SECURITY.get_proxy(CERTIFICATES) + import_task_path = certificates_proxy.ImportWithTask() + task_proxy = SECURITY.get_proxy(import_task_path) + sync_run_task(task_proxy) + def print_dracut_errors(stdout_logger): """Print Anaconda critical warnings from Dracut to user before starting Anaconda. From ad6d01849e885eb84dfcd39fa8bcd84ab9866f66 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Mon, 16 Dec 2024 13:20:30 +0100 Subject: [PATCH 06/16] security: Add API for installation on target system Resolved: INSTALLER-4030 --- .../security/certificates/certificates.py | 11 +++ .../certificates/certificates_interface.py | 9 +++ .../security/certificates/installation.py | 21 ++++++ .../security/test_module_certificates.py | 75 +++++++++++++++++-- 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/pyanaconda/modules/security/certificates/certificates.py b/pyanaconda/modules/security/certificates/certificates.py index 8695661cc91..903f22fd944 100644 --- a/pyanaconda/modules/security/certificates/certificates.py +++ b/pyanaconda/modules/security/certificates/certificates.py @@ -20,6 +20,7 @@ from pykickstart.parser import Certificate from pyanaconda.anaconda_loggers import get_module_logger +from pyanaconda.core.configuration.anaconda import conf from pyanaconda.core.dbus import DBus from pyanaconda.core.signal import Signal from pyanaconda.modules.common.base import KickstartBaseModule @@ -87,3 +88,13 @@ def import_with_task(self): sysroot="/", certificates=self.certificates, ) + + def install_with_task(self): + """Import certificates into the installed system + + :return: a DBus path of the import task + """ + return ImportCertificatesTask( + sysroot=conf.target.system_root, + certificates=self.certificates, + ) diff --git a/pyanaconda/modules/security/certificates/certificates_interface.py b/pyanaconda/modules/security/certificates/certificates_interface.py index b06f302226d..3a162fc910b 100644 --- a/pyanaconda/modules/security/certificates/certificates_interface.py +++ b/pyanaconda/modules/security/certificates/certificates_interface.py @@ -50,3 +50,12 @@ def ImportWithTask(self) -> ObjPath: return TaskContainer.to_object_path( self.implementation.import_with_task() ) + + def InstallWithTask(self) -> ObjPath: + """Import certificates into the installed system + + :return: a DBus path of the import task + """ + return TaskContainer.to_object_path( + self.implementation.install_with_task() + ) diff --git a/pyanaconda/modules/security/certificates/installation.py b/pyanaconda/modules/security/certificates/installation.py index 7059b337545..d0b78daded5 100644 --- a/pyanaconda/modules/security/certificates/installation.py +++ b/pyanaconda/modules/security/certificates/installation.py @@ -15,7 +15,10 @@ # License and may only be used or replicated with the express permission of # Red Hat, Inc. # +import os + from pyanaconda.anaconda_loggers import get_module_logger +from pyanaconda.core.path import join_paths, make_directories from pyanaconda.modules.common.task import Task log = get_module_logger(__name__) @@ -38,6 +41,24 @@ def __init__(self, sysroot, certificates): def name(self): return "Import CA certificates" + def _dump_certificate(self, cert, root): + """Dump the certificate into specified file and directory.""" + dst_dir = join_paths(root, cert.dir) + if not os.path.exists(dst_dir): + log.debug("Path %s for certificate %s does not exist, creating.", + dst_dir, cert.filename) + make_directories(dst_dir) + + dst = join_paths(dst_dir, cert.filename) + with open(dst, 'w') as f: + f.write(cert.cert) + f.write('\n') + def run(self): + """Import CA certificates. + + Dump the certificates into specified files and directories. + """ for cert in self._certificates: log.debug("Importing certificate with filename: %s dir: %s", cert.filename, cert.dir) + self._dump_certificate(cert, self._sysroot) diff --git a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py index df4d0d5f38d..fa36fe6007a 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py +++ b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py @@ -17,6 +17,8 @@ # # Red Hat Author(s): Radek Vykydal # +import os +import tempfile import unittest from dasbus.typing import * # pylint: disable=wildcard-import @@ -85,6 +87,15 @@ def _get_dbus_certs(certs): for cert, filename, cdir in certs ] + @staticmethod + def _get_certs(certs_values): + certs = [] + for values in certs_values: + c = CertificateData() + c.cert, c.filename, c.dir = values + certs.append(c) + return certs + def _iface_certificates_setter(self): """Provide setter for testing read-only Certificates property.""" return lambda value: self.certificates_module.set_certificates( @@ -114,17 +125,17 @@ def test_import_with_task_default(self, publisher): obj = check_task_creation(task_path, publisher, ImportCertificatesTask) assert obj.implementation._sysroot == "/" + def _set_certificates(self, certs_values): + certs_value = self._get_dbus_certs(certs_values) + set_certificates = self._iface_certificates_setter() + set_certificates(certs_value) + @patch_dbus_publish_object def test_import_with_task_configured(self, publisher): """Test the ImportWithTask method""" c1 = (CERT_RVTEST, 'rvtest.pem', '/etc/pki/ca-trust/extracted/pem') c2 = (CERT_RVTEST2, 'rvtest2.pem', '') - certs_value = self._get_dbus_certs([ - c1, - c2, - ]) - set_certificates = self._iface_certificates_setter() - set_certificates(certs_value) + self._set_certificates([c1, c2]) task_path = self.certificates_interface.ImportWithTask() obj = check_task_creation(task_path, publisher, ImportCertificatesTask) @@ -133,3 +144,55 @@ def test_import_with_task_configured(self, publisher): obj_c1, obj_c2 = obj.implementation._certificates assert c1 == (obj_c1.cert, obj_c1.filename, obj_c1.dir) assert c2 == (obj_c2.cert, obj_c2.filename, obj_c2.dir) + + @patch_dbus_publish_object + def test_install_with_task_default(self, publisher): + """Test the InstallWithTask method""" + task_path = self.certificates_interface.InstallWithTask() + obj = check_task_creation(task_path, publisher, ImportCertificatesTask) + assert obj.implementation._sysroot == "/mnt/sysroot" + assert obj.implementation._certificates == [] + + @patch_dbus_publish_object + def test_install_with_task_configured(self, publisher): + """Test the InstallWithTask method""" + c1 = (CERT_RVTEST, 'rvtest.pem', '/etc/pki/ca-trust/extracted/pem') + c2 = (CERT_RVTEST2, 'rvtest2.pem', '') + self._set_certificates([c1, c2]) + + task_path = self.certificates_interface.InstallWithTask() + obj = check_task_creation(task_path, publisher, ImportCertificatesTask) + assert obj.implementation._sysroot == "/mnt/sysroot" + assert len(obj.implementation._certificates) == 2 + obj_c1, obj_c2 = obj.implementation._certificates + assert c1 == (obj_c1.cert, obj_c1.filename, obj_c1.dir) + assert c2 == (obj_c2.cert, obj_c2.filename, obj_c2.dir) + + def _check_cert_file(self, cert, sysroot, is_missing=False): + cert_file = sysroot + cert.dir + "/" + cert.filename + if is_missing: + assert os.path.exists(cert_file) is False + else: + with open(cert_file) as f: + # Anaconda adds `\n` to the value when dumping it + assert f.read() == cert.cert+'\n' + + def test_import_certificates_task_files(self): + """Test the ImportCertificatesTask task dumping the certificate""" + certs = self._get_certs([ + (CERT_RVTEST, 'rvtest.pem', '/etc/pki/dir1'), + (CERT_RVTEST2, 'rvtest2.pem', '/etc/pki/dir2'), + ]) + + with tempfile.TemporaryDirectory() as sysroot: + # c1 has existing dir + os.makedirs(sysroot+certs[0].dir) + # c2 has non-existing dir + + ImportCertificatesTask( + sysroot=sysroot, + certificates=certs, + ).run() + + for c in certs: + self._check_cert_file(c, sysroot) From 21091797f9e74342f663fd2644cd09399213be79 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Tue, 3 Dec 2024 14:16:05 +0100 Subject: [PATCH 07/16] security: install certificates on target system Resolves: INSTALLER-4030 --- pyanaconda/installation.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pyanaconda/installation.py b/pyanaconda/installation.py index 8563a757805..489c6947037 100644 --- a/pyanaconda/installation.py +++ b/pyanaconda/installation.py @@ -48,6 +48,7 @@ ) from pyanaconda.modules.common.constants.objects import ( BOOTLOADER, + CERTIFICATES, FIREWALL, SCRIPTS, SNAPSHOT, @@ -137,6 +138,20 @@ def _prepare_configuration(self, payload, ksdata): configuration_queue.queue_started.connect(self._queue_started_cb) configuration_queue.task_completed.connect(self._task_completed_cb) + # import certificates first + # they may be required for subscription, initramfs regenerating, ... ? + if is_module_available(SECURITY): + certificates_import = TaskQueue( + "Certificates import", + _("Importing certificates"), + CATEGORY_SYSTEM + ) + certificates_proxy = SECURITY.get_proxy(CERTIFICATES) + certificates_import.append_dbus_tasks(SECURITY, [ + certificates_proxy.InstallWithTask() + ]) + configuration_queue.append(certificates_import) + # add installation tasks for the Subscription DBus module if is_module_available(SUBSCRIPTION): # we only run the tasks if the Subscription module is available From 7fcd4396ccbf0414a7e09c2c89dd8592169981b7 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Tue, 3 Dec 2024 14:18:28 +0100 Subject: [PATCH 08/16] security: add API to install certificates early before payload In case they are needed or processed by package scriptlets Resolves: INSTALLER-4030 --- .../security/certificates/certificates.py | 12 ++++++++++ .../certificates/certificates_interface.py | 11 +++++++++ .../security/test_module_certificates.py | 23 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/pyanaconda/modules/security/certificates/certificates.py b/pyanaconda/modules/security/certificates/certificates.py index 903f22fd944..e9288d56a07 100644 --- a/pyanaconda/modules/security/certificates/certificates.py +++ b/pyanaconda/modules/security/certificates/certificates.py @@ -98,3 +98,15 @@ def install_with_task(self): sysroot=conf.target.system_root, certificates=self.certificates, ) + + def pre_install_with_task(self): + """Import certificates into the system before the payload installation + + NOTE: the reason is potential use by rpm scriptlets + + :return: a DBus path of the import task + """ + return ImportCertificatesTask( + sysroot=conf.target.system_root, + certificates=self.certificates, + ) diff --git a/pyanaconda/modules/security/certificates/certificates_interface.py b/pyanaconda/modules/security/certificates/certificates_interface.py index 3a162fc910b..1dcdf4110d2 100644 --- a/pyanaconda/modules/security/certificates/certificates_interface.py +++ b/pyanaconda/modules/security/certificates/certificates_interface.py @@ -59,3 +59,14 @@ def InstallWithTask(self) -> ObjPath: return TaskContainer.to_object_path( self.implementation.install_with_task() ) + + def PreInstallWithTask(self) -> ObjPath: + """Import certificates into the system before the payload installation + + NOTE: the reason is potential use by rpm scriptlets + + :return: a DBus path of the import task + """ + return TaskContainer.to_object_path( + self.implementation.pre_install_with_task() + ) diff --git a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py index fa36fe6007a..db1800cdb33 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py +++ b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py @@ -177,6 +177,29 @@ def _check_cert_file(self, cert, sysroot, is_missing=False): # Anaconda adds `\n` to the value when dumping it assert f.read() == cert.cert+'\n' + @patch_dbus_publish_object + def test_pre_install_with_task_default(self, publisher): + """Test the PreInstallWithTask method""" + task_path = self.certificates_interface.PreInstallWithTask() + obj = check_task_creation(task_path, publisher, ImportCertificatesTask) + assert obj.implementation._sysroot == "/mnt/sysroot" + assert obj.implementation._certificates == [] + + @patch_dbus_publish_object + def test_pre_install_with_task_configured(self, publisher): + """Test the PreInstallWithTask method""" + c1 = (CERT_RVTEST, 'rvtest.pem', '/etc/pki/ca-trust/extracted/pem') + c2 = (CERT_RVTEST2, 'rvtest2.pem', '') + self._set_certificates([c1, c2]) + + task_path = self.certificates_interface.PreInstallWithTask() + obj = check_task_creation(task_path, publisher, ImportCertificatesTask) + assert obj.implementation._sysroot == "/mnt/sysroot" + assert len(obj.implementation._certificates) == 2 + obj_c1, obj_c2 = obj.implementation._certificates + assert c1 == (obj_c1.cert, obj_c1.filename, obj_c1.dir) + assert c2 == (obj_c2.cert, obj_c2.filename, obj_c2.dir) + def test_import_certificates_task_files(self): """Test the ImportCertificatesTask task dumping the certificate""" certs = self._get_certs([ From 3fd4ebf712ba2e51c9aeb9ddbc91446d5a38055a Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Tue, 3 Dec 2024 15:00:02 +0100 Subject: [PATCH 09/16] security: pre-install certificates before payload installation Resolves: INSTALLER-4030 --- pyanaconda/installation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyanaconda/installation.py b/pyanaconda/installation.py index 489c6947037..8dbf21b74b1 100644 --- a/pyanaconda/installation.py +++ b/pyanaconda/installation.py @@ -455,6 +455,11 @@ def _prepare_installation(self, payload, ksdata): fips_task = security_proxy.PreconfigureFIPSWithTask(payload.type) pre_install.append_dbus_tasks(SECURITY, [fips_task]) + # Import certificates so they are available for rpm scripts + certificates_proxy = SECURITY.get_proxy(CERTIFICATES) + certificates_task = certificates_proxy.PreInstallWithTask() + pre_install.append_dbus_tasks(SECURITY, [certificates_task]) + # Install the payload. pre_install.append(Task( "Find additional packages & run pre_install()", From 649c6bb4b8f2806a095bc8f098a62315d89a4b01 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Mon, 16 Dec 2024 13:52:39 +0100 Subject: [PATCH 10/16] security: log a warning when dumping certificate over an existing file Resolves: INSTALLER-4030 --- .../security/certificates/installation.py | 4 ++++ .../security/test_module_certificates.py | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/pyanaconda/modules/security/certificates/installation.py b/pyanaconda/modules/security/certificates/installation.py index d0b78daded5..8bf7b427720 100644 --- a/pyanaconda/modules/security/certificates/installation.py +++ b/pyanaconda/modules/security/certificates/installation.py @@ -50,6 +50,10 @@ def _dump_certificate(self, cert, root): make_directories(dst_dir) dst = join_paths(dst_dir, cert.filename) + + if os.path.exists(dst): + log.warning("Certificate file %s already exists, replacing.", dst) + with open(dst, 'w') as f: f.write(cert.cert) f.write('\n') diff --git a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py index db1800cdb33..d422b3f422c 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py +++ b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py @@ -219,3 +219,23 @@ def test_import_certificates_task_files(self): for c in certs: self._check_cert_file(c, sysroot) + + def test_import_certificates_task_existing_file(self): + """Test the ImportCertificatesTask task with existing file to be imported""" + certs = self._get_certs([ + (CERT_RVTEST, 'rvtest.pem', '/etc/pki/dir1'), + ]) + + c1 = certs[0] + with tempfile.TemporaryDirectory() as sysroot: + # certificate file to be dumped already exists + os.makedirs(sysroot+c1.dir) + c1_file = sysroot + c1.dir + "/" + c1.filename + open(c1_file, 'w') + + ImportCertificatesTask( + sysroot=sysroot, + certificates=[c1], + ).run() + + self._check_cert_file(c1, sysroot) From ff2a5ce5f4ca750831e2cdb80195ac5f84acc762 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Mon, 16 Dec 2024 14:00:27 +0100 Subject: [PATCH 11/16] security: raise exception if certificate destination is unknown Don't require it on parsing level, don't own a default. Resolves: INSTALLER-4030 --- .../modules/security/certificates/installation.py | 6 ++++++ .../modules/security/test_module_certificates.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pyanaconda/modules/security/certificates/installation.py b/pyanaconda/modules/security/certificates/installation.py index 8bf7b427720..7cb92f67980 100644 --- a/pyanaconda/modules/security/certificates/installation.py +++ b/pyanaconda/modules/security/certificates/installation.py @@ -19,6 +19,7 @@ from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core.path import join_paths, make_directories +from pyanaconda.modules.common.errors.installation import SecurityInstallationError from pyanaconda.modules.common.task import Task log = get_module_logger(__name__) @@ -43,6 +44,11 @@ def name(self): def _dump_certificate(self, cert, root): """Dump the certificate into specified file and directory.""" + if not cert.dir: + raise SecurityInstallationError( + "Certificate destination is missing for {}".format(cert.filename) + ) + dst_dir = join_paths(root, cert.dir) if not os.path.exists(dst_dir): log.debug("Path %s for certificate %s does not exist, creating.", diff --git a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py index d422b3f422c..e8f9879fef3 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py +++ b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py @@ -24,6 +24,7 @@ from dasbus.typing import * # pylint: disable=wildcard-import from pyanaconda.modules.common.constants.objects import CERTIFICATES +from pyanaconda.modules.common.errors.installation import SecurityInstallationError from pyanaconda.modules.common.structures.security import CertificateData from pyanaconda.modules.security.certificates.certificates import CertificatesModule from pyanaconda.modules.security.certificates.certificates_interface import ( @@ -239,3 +240,16 @@ def test_import_certificates_task_existing_file(self): ).run() self._check_cert_file(c1, sysroot) + + def test_import_certificates_missing_destination(self): + """Test the ImportCertificatesTask task with missing destination""" + certs = self._get_certs([ + (CERT_RVTEST, 'rvtest.pem', ''), + ]) + + with tempfile.TemporaryDirectory() as sysroot: + with self.assertRaises(SecurityInstallationError): + ImportCertificatesTask( + sysroot=sysroot, + certificates=certs, + ).run() From c29a13da5476f883df9860556828ab66d103e8ac Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Thu, 5 Dec 2024 09:41:26 +0100 Subject: [PATCH 12/16] security: install certificates in pre-install phase only for dnf payload Resolves: INSTALLER-4030 --- pyanaconda/core/constants.py | 3 ++ pyanaconda/installation.py | 2 +- .../security/certificates/certificates.py | 6 ++- .../certificates/certificates_interface.py | 5 +- .../security/certificates/installation.py | 13 +++++- .../security/test_module_certificates.py | 46 ++++++++++++++++++- 6 files changed, 68 insertions(+), 7 deletions(-) diff --git a/pyanaconda/core/constants.py b/pyanaconda/core/constants.py index 385bab5acab..fbc81a78d2b 100644 --- a/pyanaconda/core/constants.py +++ b/pyanaconda/core/constants.py @@ -526,3 +526,6 @@ class DisplayModes(Enum): CATEGORY_SOFTWARE = "SOFTWARE_INSTALLATION" CATEGORY_BOOTLOADER = "BOOTLOADER_INSTALLATION" CATEGORY_SYSTEM = "SYSTEM_CONFIGURATION" + +# Installation phases +INSTALLATION_PHASE_PREINSTALL = "pre-install" diff --git a/pyanaconda/installation.py b/pyanaconda/installation.py index 8dbf21b74b1..9bd8b9f470c 100644 --- a/pyanaconda/installation.py +++ b/pyanaconda/installation.py @@ -457,7 +457,7 @@ def _prepare_installation(self, payload, ksdata): # Import certificates so they are available for rpm scripts certificates_proxy = SECURITY.get_proxy(CERTIFICATES) - certificates_task = certificates_proxy.PreInstallWithTask() + certificates_task = certificates_proxy.PreInstallWithTask(payload.type) pre_install.append_dbus_tasks(SECURITY, [certificates_task]) # Install the payload. diff --git a/pyanaconda/modules/security/certificates/certificates.py b/pyanaconda/modules/security/certificates/certificates.py index e9288d56a07..78122bf00d9 100644 --- a/pyanaconda/modules/security/certificates/certificates.py +++ b/pyanaconda/modules/security/certificates/certificates.py @@ -21,6 +21,7 @@ from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core.configuration.anaconda import conf +from pyanaconda.core.constants import INSTALLATION_PHASE_PREINSTALL from pyanaconda.core.dbus import DBus from pyanaconda.core.signal import Signal from pyanaconda.modules.common.base import KickstartBaseModule @@ -99,14 +100,17 @@ def install_with_task(self): certificates=self.certificates, ) - def pre_install_with_task(self): + def pre_install_with_task(self, payload_type): """Import certificates into the system before the payload installation NOTE: the reason is potential use by rpm scriptlets + :param payload_type: a string with the payload type :return: a DBus path of the import task """ return ImportCertificatesTask( sysroot=conf.target.system_root, certificates=self.certificates, + payload_type=payload_type, + phase=INSTALLATION_PHASE_PREINSTALL, ) diff --git a/pyanaconda/modules/security/certificates/certificates_interface.py b/pyanaconda/modules/security/certificates/certificates_interface.py index 1dcdf4110d2..1f7b2a335d8 100644 --- a/pyanaconda/modules/security/certificates/certificates_interface.py +++ b/pyanaconda/modules/security/certificates/certificates_interface.py @@ -60,13 +60,14 @@ def InstallWithTask(self) -> ObjPath: self.implementation.install_with_task() ) - def PreInstallWithTask(self) -> ObjPath: + def PreInstallWithTask(self, payload_type: Str) -> ObjPath: """Import certificates into the system before the payload installation NOTE: the reason is potential use by rpm scriptlets + :param payload_type: a string with the payload type :return: a DBus path of the import task """ return TaskContainer.to_object_path( - self.implementation.pre_install_with_task() + self.implementation.pre_install_with_task(payload_type) ) diff --git a/pyanaconda/modules/security/certificates/installation.py b/pyanaconda/modules/security/certificates/installation.py index 7cb92f67980..31f1cd6e71d 100644 --- a/pyanaconda/modules/security/certificates/installation.py +++ b/pyanaconda/modules/security/certificates/installation.py @@ -18,6 +18,7 @@ import os from pyanaconda.anaconda_loggers import get_module_logger +from pyanaconda.core.constants import INSTALLATION_PHASE_PREINSTALL, PAYLOAD_TYPE_DNF from pyanaconda.core.path import join_paths, make_directories from pyanaconda.modules.common.errors.installation import SecurityInstallationError from pyanaconda.modules.common.task import Task @@ -28,15 +29,19 @@ class ImportCertificatesTask(Task): """Task for importing certificates into a system.""" - def __init__(self, sysroot, certificates): + def __init__(self, sysroot, certificates, payload_type=None, phase=None): """Create a new certificates import task. :param str sysroot: a path to the root of the target system :param certificates: list of certificate data holders + :param payload_type: a type of the payload + :param phase: installation phase - INSTALLATION_PHASE_PREINSTALL or None for any other """ super().__init__() self._sysroot = sysroot self._certificates = certificates + self._payload_type = payload_type + self._phase = phase @property def name(self): @@ -69,6 +74,12 @@ def run(self): Dump the certificates into specified files and directories. """ + if self._phase == INSTALLATION_PHASE_PREINSTALL: + if self._payload_type != PAYLOAD_TYPE_DNF: + log.debug("Not importing certificates in pre install phase for %s payload.", + self._payload_type) + return + for cert in self._certificates: log.debug("Importing certificate with filename: %s dir: %s", cert.filename, cert.dir) self._dump_certificate(cert, self._sysroot) diff --git a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py index e8f9879fef3..919dff9b031 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py +++ b/tests/unit_tests/pyanaconda_tests/modules/security/test_module_certificates.py @@ -23,6 +23,11 @@ from dasbus.typing import * # pylint: disable=wildcard-import +from pyanaconda.core.constants import ( + INSTALLATION_PHASE_PREINSTALL, + PAYLOAD_TYPE_DNF, + PAYLOAD_TYPE_LIVE_OS, +) from pyanaconda.modules.common.constants.objects import CERTIFICATES from pyanaconda.modules.common.errors.installation import SecurityInstallationError from pyanaconda.modules.common.structures.security import CertificateData @@ -181,7 +186,7 @@ def _check_cert_file(self, cert, sysroot, is_missing=False): @patch_dbus_publish_object def test_pre_install_with_task_default(self, publisher): """Test the PreInstallWithTask method""" - task_path = self.certificates_interface.PreInstallWithTask() + task_path = self.certificates_interface.PreInstallWithTask(PAYLOAD_TYPE_DNF) obj = check_task_creation(task_path, publisher, ImportCertificatesTask) assert obj.implementation._sysroot == "/mnt/sysroot" assert obj.implementation._certificates == [] @@ -193,7 +198,7 @@ def test_pre_install_with_task_configured(self, publisher): c2 = (CERT_RVTEST2, 'rvtest2.pem', '') self._set_certificates([c1, c2]) - task_path = self.certificates_interface.PreInstallWithTask() + task_path = self.certificates_interface.PreInstallWithTask(PAYLOAD_TYPE_DNF) obj = check_task_creation(task_path, publisher, ImportCertificatesTask) assert obj.implementation._sysroot == "/mnt/sysroot" assert len(obj.implementation._certificates) == 2 @@ -253,3 +258,40 @@ def test_import_certificates_missing_destination(self): sysroot=sysroot, certificates=certs, ).run() + + def test_import_certificates_pre_nondnf_payload(self): + """Test the ImportCertificatesTask in pre install with non-dnf payload""" + certs = self._get_certs([ + (CERT_RVTEST, 'rvtest.pem', '/etc/pki/dir1'), + (CERT_RVTEST2, 'rvtest2.pem', '/etc/pki/dir2'), + ]) + c1 = certs[0] + c2 = certs[1] + + with tempfile.TemporaryDirectory() as sysroot: + + # non pre-install phase => install + ImportCertificatesTask( + sysroot=sysroot, + certificates=[c1], + payload_type=PAYLOAD_TYPE_LIVE_OS, + ).run() + self._check_cert_file(c1, sysroot) + + # pre-install phase, payload dnf => don't install + ImportCertificatesTask( + sysroot=sysroot, + certificates=[c2], + payload_type=PAYLOAD_TYPE_LIVE_OS, + phase=INSTALLATION_PHASE_PREINSTALL + ).run() + self._check_cert_file(c2, sysroot, is_missing=True) + + # pre-install phase, payload dnf => install + ImportCertificatesTask( + sysroot=sysroot, + certificates=[c2], + payload_type=PAYLOAD_TYPE_DNF, + phase=INSTALLATION_PHASE_PREINSTALL + ).run() + self._check_cert_file(c2, sysroot) From 9646836d08cb615075dcb28919026035ddd35200 Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Fri, 15 Nov 2024 13:56:08 +0100 Subject: [PATCH 13/16] security: import certificates in initramfs Also dump for transfer durig switchroot so that the certificates can be potentially imported early after switchroot by a service. Resolves: INSTALLER-4030 --- dracut/parse-kickstart | 41 +++++++++++ .../dracut_tests/test_parse_kickstart.py | 69 +++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/dracut/parse-kickstart b/dracut/parse-kickstart index 95d2ef42b58..0f678d955f4 100755 --- a/dracut/parse-kickstart +++ b/dracut/parse-kickstart @@ -66,6 +66,8 @@ TMPDIR = "/tmp" ARPHRD_ETHER = "1" ARPHRD_INFINIBAND = "32" +CERT_TRANSPORT_DIR = "/run/install/certificates" + # Helper function for reading simple files in /sys def readsysfile(f): '''Return the contents of f, or "" if missing.''' @@ -403,6 +405,44 @@ def ksnet_to_dracut(args, lineno, net, bootdev=False): return " ".join(line) + +def _dump_certificate(cert, root="/", dump_dir=None): + """Dump the certificate into specified file.""" + dump_dir = dump_dir or cert.dir + if not dump_dir: + log.error("Certificate destination is missing for %s", cert.filename) + return + + dst_dir = os.path.join(root, dump_dir.lstrip('/')) + log.debug("Dumping certificate %s into %s.", cert.filename, dst_dir) + if not os.path.exists(dst_dir): + log.debug("Path %s for certificate does not exist, creating.", dst_dir) + os.makedirs(dst_dir) + + dst = os.path.join(dst_dir, cert.filename) + + if os.path.exists(dst): + log.warning("Certificate file %s already exists, replacing.", dst) + + with open(dst, 'w') as f: + f.write(cert.cert) + f.write('\n') + + +def process_certificates(handler): + """Import certificates defined in %certificate sections.""" + for cert in handler.certificates: + log.info("Processing kickstart certificate %s", cert.filename) + + if not cert.filename: + log.error("Missing certificate file name, skipping.") + continue + + _dump_certificate(cert) + # Dump for transport to switchroot + _dump_certificate(cert, root=CERT_TRANSPORT_DIR+"/path/") + + def process_kickstart(ksfile): handler = DracutHandler() try: @@ -422,6 +462,7 @@ def process_kickstart(ksfile): with open(TMPDIR+"/ks.info", "a") as f: f.write('parsed_kickstart="%s"\n' % processed_file) log.info("finished parsing kickstart") + process_certificates(handler) return processed_file, handler.output if __name__ == '__main__': diff --git a/tests/unit_tests/dracut_tests/test_parse_kickstart.py b/tests/unit_tests/dracut_tests/test_parse_kickstart.py index 1160b1cc3c7..826b72c82f7 100644 --- a/tests/unit_tests/dracut_tests/test_parse_kickstart.py +++ b/tests/unit_tests/dracut_tests/test_parse_kickstart.py @@ -22,6 +22,18 @@ import tempfile import unittest +CERT_CONTENT = """-----BEGIN CERTIFICATE----- +MIIBjTCCATOgAwIBAgIUWR5HO3v/0I80Ne0jQWVZFODuWLEwCgYIKoZIzj0EAwIw +FDESMBAGA1UEAwwJUlZURVNUIENBMB4XDTI0MTEyMDEzNTk1N1oXDTM0MTExODEz +NTk1N1owFDESMBAGA1UEAwwJUlZURVNUIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAELghFKGEgS8+5/2nx50W0xOqTrKc2Jz/rD/jfL0m4z4fkeAslCOkIKv74 +0wfBXMngxi+OF/b3Vh8FmokuNBQO5qNjMGEwHQYDVR0OBBYEFOJarl9Xkd13sLzI +mHqv6aESlvuCMB8GA1UdIwQYMBaAFOJarl9Xkd13sLzImHqv6aESlvuCMA8GA1Ud +EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMCA0gAMEUCIAet +7nyre42ReoRKoyHWLDsQmQDzoyU3FQdC0cViqOtrAiEAxYIL+XTTp7Xy9RNE4Xg7 +yNWXfdraC/AfMM8fqsxlVJM= +-----END CERTIFICATE-----""" + class BaseTestCase(unittest.TestCase): def setUp(self): @@ -243,3 +255,60 @@ def test_bootloader(self): lines = self.execParseKickstart(ks_file.name) assert lines[0] == "inst.extlinux", lines + + def _check_cert_file(self, cert_file, content): + with open(cert_file) as f: + # Anaconda adds `\n` to the value when dumping it + assert f.read() == content+'\n' + + def test_certificate(self): + filename = "rvtest.pem" + cdir = os.path.join(self.tmpdir, "cert_dir/subdir") + content = CERT_CONTENT + ks_cert = f""" +%certificate --filename={filename} --dir={cdir} +{content} +%end +""" + cert_file = os.path.join(cdir, filename) + + with tempfile.NamedTemporaryFile(mode="w+t") as ks_file: + ks_file.write(ks_cert) + ks_file.flush() + lines = self.execParseKickstart(ks_file.name) + assert lines == [] + + self._check_cert_file(cert_file, content) + + # Check existence for file for transport to root + CERT_TRANSPORT_DIR = "/run/install/certificates" + transport_file = os.path.join(CERT_TRANSPORT_DIR, cert_file) + self._check_cert_file(transport_file, content) + + def test_certificate_existing(self): + filename = "rvtest.pem" + cdir = os.path.join(self.tmpdir, "cert_dir/subdir") + content = CERT_CONTENT + ks_cert = f""" +%certificate --filename={filename} --dir={cdir} +{content} +%end +""" + cert_file = os.path.join(cdir, filename) + + # Existing file should be overwritten + os.makedirs(cdir) + open(cert_file, 'w') + + with tempfile.NamedTemporaryFile(mode="w+t") as ks_file: + ks_file.write(ks_cert) + ks_file.flush() + lines = self.execParseKickstart(ks_file.name) + assert lines == [] + + self._check_cert_file(cert_file, content) + + # Check existence for file for transport to root + CERT_TRANSPORT_DIR = "/run/install/certificates" + transport_file = os.path.join(CERT_TRANSPORT_DIR, cert_file) + self._check_cert_file(transport_file, content) From a10649c3c98384807b4d3132c736a541b8f5382f Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Wed, 20 Nov 2024 13:36:21 +0100 Subject: [PATCH 14/16] security: add a service to transfer certificates from initramfs Resolves: INSTALLER-4030 --- data/systemd/Makefile.am | 1 + data/systemd/anaconda-generator | 1 + data/systemd/anaconda-import-initramfs-certs.service | 8 ++++++++ scripts/Makefile.am | 2 +- scripts/anaconda-import-initramfs-certs | 6 ++++++ 5 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 data/systemd/anaconda-import-initramfs-certs.service create mode 100755 scripts/anaconda-import-initramfs-certs diff --git a/data/systemd/Makefile.am b/data/systemd/Makefile.am index 7e2c8cf6c49..deabc992ffe 100644 --- a/data/systemd/Makefile.am +++ b/data/systemd/Makefile.am @@ -29,6 +29,7 @@ dist_systemd_DATA = anaconda.service \ anaconda-nm-config.service \ anaconda-nm-disable-autocons.service \ anaconda-nm-disable-autocons-rhel.service \ + anaconda-import-initramfs-certs.service \ anaconda-pre.service \ anaconda-s390-device-config-import.service \ anaconda-fips.service diff --git a/data/systemd/anaconda-generator b/data/systemd/anaconda-generator index bd735004a3f..7dd82b816f7 100755 --- a/data/systemd/anaconda-generator +++ b/data/systemd/anaconda-generator @@ -45,3 +45,4 @@ ln -sf "$systemd_dir/anaconda-nm-config.service" "$target_dir/anaconda-nm-config ln -sf "$systemd_dir/anaconda-nm-disable-autocons.service" "$target_dir/anaconda-nm-disable-autocons.service" ln -sf "$systemd_dir/anaconda-nm-disable-autocons-rhel.service" "$target_dir/anaconda-nm-disable-autocons-rhel.service" ln -sf "$systemd_dir/anaconda-pre.service" "$target_dir/anaconda-pre.service" +ln -sf "$systemd_dir/anaconda-import-initramfs-certs.service" "$target_dir/anaconda-import-initramfs-certs.service" diff --git a/data/systemd/anaconda-import-initramfs-certs.service b/data/systemd/anaconda-import-initramfs-certs.service new file mode 100644 index 00000000000..dcd4e3f0ddc --- /dev/null +++ b/data/systemd/anaconda-import-initramfs-certs.service @@ -0,0 +1,8 @@ +[Unit] +Description=Import of certificates added in initramfs stage of Anaconda via kickstart +Before=NetworkManager.service +Before=anaconda.target + +[Service] +Type=oneshot +ExecStart=/usr/libexec/anaconda/anaconda-import-initramfs-certs diff --git a/scripts/Makefile.am b/scripts/Makefile.am index 09cd9d925ec..6b6f660a4f6 100644 --- a/scripts/Makefile.am +++ b/scripts/Makefile.am @@ -17,7 +17,7 @@ scriptsdir = $(libexecdir)/$(PACKAGE_NAME) dist_scripts_SCRIPTS = run-anaconda anaconda-pre-log-gen log-capture start-module apply-updates \ - run-in-new-session + run-in-new-session anaconda-import-initramfs-certs dist_noinst_SCRIPTS = makeupdates makebumpver diff --git a/scripts/anaconda-import-initramfs-certs b/scripts/anaconda-import-initramfs-certs new file mode 100755 index 00000000000..b186a6fc6cd --- /dev/null +++ b/scripts/anaconda-import-initramfs-certs @@ -0,0 +1,6 @@ +#! /bin/bash +# Transfers CA certificates imported in initramfs via kickstart +# to anaconda environment after switchroot. + +# certificates dumped to the specified file are copied to root +cp -rv /run/install/certificates/path/* / || true From 844c5616fede7f102a8b432fd597a30eb04044cf Mon Sep 17 00:00:00 2001 From: Radek Vykydal Date: Tue, 14 Jan 2025 16:21:08 +0100 Subject: [PATCH 15/16] Add release notes for certificates import Related: INSTALLER-4030 --- docs/release-notes/certificates-import.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/release-notes/certificates-import.rst diff --git a/docs/release-notes/certificates-import.rst b/docs/release-notes/certificates-import.rst new file mode 100644 index 00000000000..cc28d6a623c --- /dev/null +++ b/docs/release-notes/certificates-import.rst @@ -0,0 +1,13 @@ +:Type: Kickstart +:Summary: Support certificates import via kickstart file + +:Description: + New kickstart section %certificate is supported. + It allows users to securely embed certificates directly within + the kickstart file. + +:Links: + - https://issues.redhat.com/browse/RHELBU-2913 + - https://issues.redhat.com/browse/INSTALLER-4027 + - https://github.com/rhinstaller/anaconda/pull/6045 + - https://github.com/pykickstart/pykickstart/pull/517 From a9b689badbf581ee62019a1875ec3640ae859be6 Mon Sep 17 00:00:00 2001 From: Katerina Koukiou Date: Tue, 14 Jan 2025 17:13:38 +0100 Subject: [PATCH 16/16] infra: bump pykickstart version version requirement in the spec file --- anaconda.spec.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anaconda.spec.in b/anaconda.spec.in index a7f0b85b452..906c1160def 100644 --- a/anaconda.spec.in +++ b/anaconda.spec.in @@ -38,7 +38,7 @@ Source0: https://github.com/rhinstaller/%{name}/releases/download/%{name}-%{vers %define libreportanacondaver 2.0.21-1 %define mehver 0.23-1 %define nmver 1.0 -%define pykickstartver 3.58-1 +%define pykickstartver 3.61-1 %define pypartedver 2.5-2 %define pythonblivetver 1:3.9.0-1 %define rpmver 4.15.0