diff --git a/package/yast2-storage-ng.changes b/package/yast2-storage-ng.changes index 61e19f45f4..92c6164da3 100644 --- a/package/yast2-storage-ng.changes +++ b/package/yast2-storage-ng.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Thu Nov 2 13:52:11 UTC 2023 - Ancor Gonzalez Sosa + +- Encryption method TpmFde to be used by Agama (and later by YaST) + for setting up LUKS2 devices that are unlocked during boot using + a TPM chip (gh#yast/yast-storage-ng#1088, related to bsc#1210512) +- 5.0.4 + ------------------------------------------------------------------- Thu Oct 19 14:46:45 UTC 2023 - Ancor Gonzalez Sosa diff --git a/package/yast2-storage-ng.spec b/package/yast2-storage-ng.spec index 6374816c77..753b02785f 100644 --- a/package/yast2-storage-ng.spec +++ b/package/yast2-storage-ng.spec @@ -16,7 +16,7 @@ # Name: yast2-storage-ng -Version: 5.0.3 +Version: 5.0.4 Release: 0 Summary: YaST2 - Storage Configuration License: GPL-2.0-only OR GPL-3.0-only @@ -25,8 +25,8 @@ Url: https://github.com/yast/yast-storage-ng Source: %{name}-%{version}.tar.bz2 -# New Md size calculation -BuildRequires: libstorage-ng-ruby >= 4.4.76 +# Encryption#use_key_file_in_commit +BuildRequires: libstorage-ng-ruby >= 4.5.144 BuildRequires: update-desktop-files # Replace PackageSystem with Package BuildRequires: yast2 >= 4.4.38 @@ -47,8 +47,8 @@ BuildRequires: rubygem(%{rb_default_ruby_abi}:parallel_tests) # findutils for xargs Requires: findutils -# New Md size calculation -Requires: libstorage-ng-ruby >= 4.4.76 +# Encryption#use_key_file_in_commit +Requires: libstorage-ng-ruby >= 4.5.144 # Replace PackageSystem with Package Requires: yast2 >= 4.4.38 # Y2Packager::Repository diff --git a/src/lib/y2partitioner/widgets/overview.rb b/src/lib/y2partitioner/widgets/overview.rb index cb63332734..307d7b2341 100644 --- a/src/lib/y2partitioner/widgets/overview.rb +++ b/src/lib/y2partitioner/widgets/overview.rb @@ -1,4 +1,4 @@ -# Copyright (c) [2017-2022] SUSE LLC +# Copyright (c) [2017-2023] SUSE LLC # # All Rights Reserved. # @@ -209,13 +209,15 @@ def valid_setup? # # As a side effect, it will ask the user to install missing packages. # - # @see Y2Storage::UsedStorageFeatures + # @see Y2Storage::StorageFeature # # @return [Boolean] def packages_installed? return true if Yast::Mode.installation - pkgs = device_graph.actiongraph.used_features.pkg_list + features = device_graph.actiongraph.used_features + features.concat(device_graph.yast_commit_features) + pkgs = features.pkg_list Y2Storage::PackageHandler.new(pkgs).install end diff --git a/src/lib/y2storage/devicegraph.rb b/src/lib/y2storage/devicegraph.rb index aca8f3d727..d163a8de89 100644 --- a/src/lib/y2storage/devicegraph.rb +++ b/src/lib/y2storage/devicegraph.rb @@ -1,4 +1,4 @@ -# Copyright (c) [2017-2021] SUSE LLC +# Copyright (c) [2017-2023] SUSE LLC # # All Rights Reserved. # @@ -594,12 +594,19 @@ def finish_installation # List of storage features used by the devicegraph # + # Note this is used during system installation. In the installed system, the + # combination of Actiongraph#used_features and Devicegraph#yast_commit_features + # is used instead. + # # By default, it returns the features associated to all devices and filesystems # in the devicegraph. The required_only argument can be used to limit the result - # by excluding features associated to those filesystems that have no mount point. + # by excluding features that are not mandatory to produce a functional system. For + # example, it excludes features associated to those filesystems that have no mount + # point. # # @param required_only [Boolean] whether the result should only include those - # features that are mandatory (ie. associated to devices with a mount point) + # features that are mandatory (ie. associated to devices with a mount point or + # to devices that will be configured during the first boot of the new system) # @return [StorageFeaturesList] def used_features(required_only: false) type = @@ -609,7 +616,9 @@ def used_features(required_only: false) Storage::UsedFeaturesDependencyType_SUGGESTED end - StorageFeaturesList.from_bitfield(storage_used_features(type)) + list = StorageFeaturesList.from_bitfield(storage_used_features(type)) + list.concat(yast_commit_features) + list end # List of required (mandatory) storage features used by the devicegraph @@ -626,7 +635,20 @@ def optional_used_features all = storage_used_features(Storage::UsedFeaturesDependencyType_SUGGESTED) required = storage_used_features(Storage::UsedFeaturesDependencyType_REQUIRED) # Using binary XOR in those bit fields to calculate the difference - StorageFeaturesList.from_bitfield(all ^ required) + list = StorageFeaturesList.from_bitfield(all ^ required) + list.concat(yast_commit_features) + list + end + + # List of features that correspond to aspects handled by Y2Storage (not coming + # from libstorage-ng) and that need to be present in the target system either during + # the storage commit phase or at a later stage. Ie. features needed in the target + # system to access the device or to finish its configuration. + # + # @return [StorageFeaturesList] + def yast_commit_features + features = encryptions.flat_map(&:commit_features).uniq + StorageFeaturesList.new(features) end private diff --git a/src/lib/y2storage/encryption.rb b/src/lib/y2storage/encryption.rb index 95d2559ff8..ada6766370 100644 --- a/src/lib/y2storage/encryption.rb +++ b/src/lib/y2storage/encryption.rb @@ -62,6 +62,23 @@ class Encryption < BlkDevice storage_forward :key_file storage_forward :key_file= + # @!method use_key_file_in_commit? + # Whether the information at {#key_file} is used in the commit phase of libstorage-ng + # (in case it contains a valid value). + # + # The default value is true, but it can be set to false in order to fill the third column + # of the crypttab file without actually affecting the creation of the device. + # + # @return [Boolean] + storage_forward :use_key_file_in_commit? + + # @!method use_key_file_in_commit=(value) + # + # Sets the {#use_key_file_in_commit?} flag + # + # @param value [Boolean] + storage_forward :use_key_file_in_commit= + # @!attribute cipher # The encryption cipher # @@ -345,6 +362,14 @@ def finish_installation encryption_process&.finish_installation end + # Features that must be supported in the target system to finish the encryption + # process + # + # @return [Array] + def commit_features + encryption_process&.commit_features || [] + end + # If the current mount_by is suitable, it does nothing. # # Otherwise, it assigns the best option from all the suitable ones diff --git a/src/lib/y2storage/encryption_method.rb b/src/lib/y2storage/encryption_method.rb index 678c0767f4..160d517a0e 100644 --- a/src/lib/y2storage/encryption_method.rb +++ b/src/lib/y2storage/encryption_method.rb @@ -20,6 +20,7 @@ require "y2storage/encryption_method/luks1" require "y2storage/encryption_method/pervasive_luks2" require "y2storage/encryption_method/luks2" +require "y2storage/encryption_method/tpm_fde" require "y2storage/encryption_method/random_swap" require "y2storage/encryption_method/protected_swap" require "y2storage/encryption_method/secure_swap" @@ -47,6 +48,8 @@ module EncryptionMethod PERVASIVE_LUKS2 = PervasiveLuks2.new # Instance of the Luks2 method to be always returned by the module LUKS2 = Luks2.new + # Instance of the TpmFde method to be always returned by the module + TPM_FDE = TpmFde.new # Instance of the RandomSwap method to be always returned by the module RANDOM_SWAP = RandomSwap.new # Instance of the ProtectedSwap method to be always returned by the module @@ -57,7 +60,7 @@ module EncryptionMethod # Sorted list of all the method instances # @see .all ALL = [ - LUKS1, PERVASIVE_LUKS2, LUKS2, RANDOM_SWAP, PROTECTED_SWAP, SECURE_SWAP + LUKS1, PERVASIVE_LUKS2, LUKS2, TPM_FDE, RANDOM_SWAP, PROTECTED_SWAP, SECURE_SWAP ] private_constant :ALL diff --git a/src/lib/y2storage/encryption_method/tpm_fde.rb b/src/lib/y2storage/encryption_method/tpm_fde.rb new file mode 100644 index 0000000000..43ba28b1a8 --- /dev/null +++ b/src/lib/y2storage/encryption_method/tpm_fde.rb @@ -0,0 +1,147 @@ +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. +# Copyright (c) [2019] SUSE LLC + +require "yast" +require "y2storage/encryption_method/base" +require "y2storage/yast_feature" +require "y2storage/encryption_processes/tpm_fde_tools" + +Yast.import "Mode" +Yast.import "Package" + +module Y2Storage + module EncryptionMethod + # Encryption method that allows to encrypt a device using LUKS2 and configure the unlocking + # process via the system TPM using the fde-tools created by SUSE. + # + # This is a quite special encryption method due to the way the fde-tools work. First of all, + # if this method is used, it must be used at least for the root (/) filesystem and only + # additionally for some other devices. + # + # Check the documentation of fde-tools for further information. + # https://github.com/openSUSE/fde-tools + class TpmFde < Base + def initialize + textdomain "storage" + + super(:tpm_fde, _("TPM-Based Full Disk Encrytion")) + end + + # @see Base#used_for? + # + # @todo Not sure if this would be possible at the end, since the exact way to setup the + # system using fde-tools is still changing too often. In any case, having a precise result + # for this method will only be relevant when implementing support for creating encrypted + # devices in an installed system. During installation returning always false is perfectly + # correct. + # + # @return [Boolean] false + def used_for?(_encryption) + # One candidate criteria (still waiting for some conversations with fde-toold developers) + # could be: + # encryption.type.is?(:luks2) && key_file == EncryptionProcesses::TpmFdeTools.key_file_name + false + end + + # @see Base#available? + # + # In this initial implementation this always returns false because there are important + # limitations to use this in (Auto)YaST: + # + # - The current version of the inst-sys cannot talk to the TPM + # - There is still no corresponding UI in the Expert Partitioner + # - Some mechanism to ensure consistency (eg. checking all devices use the same recovery + # password) need to be introduced + # - The current implementation of the encryption method only covers system installation + # (with no support to add a new encrypted device to a system already using fde-tools) + # + # So far, the encryption method is implemented to be used by Agama (which doesn't honor + # the {#available?} method. + # + # @return [Boolean] false + def available? + false + end + + # Whether both the target system and the product being installed meet the requisites + # to setup devices using this encryption method. + # + # The encryption method must be used at least for the root filesystem (eg. is not possible to + # use it for /var but not for /), but that can't hardly be controlled here. A separate + # validation that considers the whole devicegraph is needed. + # + # @return [Boolean] + def possible? + tpm_system? && tpm_product? + end + + # Creates an encryption device for the given block device + # + # @param blk_device [Y2Storage::BlkDevice] + # @param dm_name [String] + # + # @return [Y2Storage::Encryption] + def create_device(blk_device, dm_name, label: "") + encryption_process.create_device(blk_device, dm_name, label: label) + end + + private + + # @see Base#encryption_process + def encryption_process + EncryptionProcesses::TpmFdeTools.new(self) + end + + # Whether the system is capable of using the encryption method + # + # @see #possible? + # + # @return [Boolean] + def tpm_system? + Y2Storage::Arch.new.efiboot? && tpm_present? + end + + # Whether a TPM2 chip is present and working + # + # @see #possible? + # + # @return [Boolean] + def tpm_present? + return @tpm_present unless @tpm_present.nil? + + @tpm_present = EncryptionProcesses::FdeTools.new.tpm_present? + end + + # Whether the product being installed has the ability to configure the encryption method + # + # @see #possible? + # + # @return [Boolean] + def tpm_product? + # TODO: We should likely do some memoization of the result. But it is not clear when + # such memoization would be invalidated (eg. new packages available due to some change + # in selected product or to new repositories). + + # Beware: apart from true and false, AvailableAll can return nil if things go wrong + !!Yast::Package.AvailableAll(YastFeature::ENCRYPTION_TPM_FDE.pkg_list) + end + end + end +end diff --git a/src/lib/y2storage/encryption_processes/base.rb b/src/lib/y2storage/encryption_processes/base.rb index affe9de4c0..0a9c2c8a4f 100644 --- a/src/lib/y2storage/encryption_processes/base.rb +++ b/src/lib/y2storage/encryption_processes/base.rb @@ -1,4 +1,4 @@ -# Copyright (c) [2019] SUSE LLC +# Copyright (c) [2019-2023] SUSE LLC # # All Rights Reserved. # @@ -18,6 +18,7 @@ # find current contact information at www.suse.com. require "yast" +require "y2storage/yast_feature" require "abstract_method" @@ -93,6 +94,15 @@ def crypt_options(_blk_device) [] end + # Features objects to describe the requirements to perform the commit phase + # and any subsequent operation (eg., initialization during the first boot) of + # the encryption procedure + # + # @return [Array] + def commit_features + [] + end + private # Open options with the format expected by the underlying tools (cryptsetup) diff --git a/src/lib/y2storage/encryption_processes/fde_tools.rb b/src/lib/y2storage/encryption_processes/fde_tools.rb new file mode 100644 index 0000000000..e27fbde12e --- /dev/null +++ b/src/lib/y2storage/encryption_processes/fde_tools.rb @@ -0,0 +1,104 @@ +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast2/execute" +require "yast2/systemd/service" +require "y2storage/secret_attributes" + +module Y2Storage + module EncryptionProcesses + # Auxiliary class to interact with the utilities provided by the fde-tools package + class FdeTools + include SecretAttributes + include Yast::Logger + + # Location of the fdectl command + FDECTL = "/usr/sbin/fdectl".freeze + private_constant :FDECTL + + # Passphrase that was used to encrypt the device and that is needed to + # perform several of the steps + # + # @return [String] + secret_attr :recovery_password + + # Constructor + def initialize(recovery_password = nil) + self.recovery_password = recovery_password + end + + # Whether fde-tools detect a working TPM2 chip in the system + # + # @return [Boolean] + def tpm_present + Yast::Execute.on_target!(FDECTL, "tpm-present") + log.info "FDE: TPMv2 detected" + true + rescue Cheetah::ExecutionFailed + log.info "FDE: TPMv2 not detected" + false + end + + alias_method :tpm_present?, :tpm_present + + # Adds an additional passphrase to the encrypted device and configures Grub2 + # to use that passphrase during the next boot + def add_secondary_password + command_with_password("add-secondary-password") + end + + # Adds to the encrypted device the new key that will be used as a base to complete + # the sealing process on the next boot + def add_secondary_key + command_with_password("add-secondary-key") + end + + # @see #enroll_service + ENROLL_SERVICE = "fde-tpm-enroll.service".freeze + private_constant :ENROLL_SERVICE + + # Systemd service that takes care of finishing the configuration of the encrypted + # devices, sealing the new key and dropping the temporary one used by Grub2 for + # the first boot. + # + # @return [Yast2::Systemd::Service] + def enroll_service + service = Yast2::Systemd::Service.find(ENROLL_SERVICE) + log.info "FDE: TPM enroll service: #{service}" + service + end + + private + + # Executes an fdectl command that requires the recovery password to complete + # + # @return [Boolean] true if the command was successfully executed + def command_with_password(command) + Yast::Execute.on_target!( + FDECTL, command, + stdin: "#{recovery_password}\n", + recorder: Yast::ReducedRecorder.new(skip: :stdin) + ) + true + rescue Cheetah::ExecutionFailed + false + end + end + end +end diff --git a/src/lib/y2storage/encryption_processes/fde_tools_config.rb b/src/lib/y2storage/encryption_processes/fde_tools_config.rb new file mode 100644 index 0000000000..a74c366de8 --- /dev/null +++ b/src/lib/y2storage/encryption_processes/fde_tools_config.rb @@ -0,0 +1,122 @@ +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" +require "singleton" +require "y2storage/pbkd_function" + +module Y2Storage + module EncryptionProcesses + # Class to read and write /etc/sysconfig/fde-tools + # + # The class is basically a simple wrapper for the SCR. The class does not + # remember any values. + class FdeToolsConfig + include Singleton + include Yast + include Yast::Logger + + # Reads value for FDE_LUKS_PBKDF key and converts it to a proper PbkdFunction object + # + # @note In case the value cannot be converted, a fallback value is used, + # see {LUKS_PBKDF_FALLBACK}. + # + # @return [Y2Storage::PbkdFunction] + def pbkd_function + found = Y2Storage::PbkdFunction.find(fde_luks_pbkdf&.downcase) + return found if found + + log.warn("sysconfig.fde-tools contains an invalid value '#{fde_luks_pbkdf}' for " \ + "FDE_LUKS_PBKDF. Using fallback value #{LUKS_PBKDF_FALLBACK}.") + Y2Storage::PbkdFunction.find(LUKS_PBKDF_FALLBACK) + end + + # Writes the proper FDE_LUKS_PBKDF value into the sysconfig file + # + # @note The PbkdFunction object is converted to a plain string. + # + # @param pbkdf [Y2Storage::PbkdFunction] + def pbkd_function=(pbkdf) + self.fde_luks_pbkdf = pbkdf.to_s + end + + def devices + (fde_devices || "").split + end + + def devices=(devices) + self.fde_devices = devices.compact.join(" ") + end + + private + + SYSCONFIG_PATH = ".sysconfig.fde-tools".freeze + + FDE_DEVICES = "FDE_DEVS".freeze + FDE_LUKS_PBKDF = "FDE_LUKS_PBKDF".freeze + + LUKS_PBKDF_FALLBACK = :pbkdf2 + + # Reads a key from the sysconfig file + # + # @param key [String] + # @return [String, nil] + def read(key) + Yast::SCR.Read(path("#{SYSCONFIG_PATH}.#{key}")) + end + + # Writes a value into the sysconfig file + # + # @param key [String] + # @param value [String] + def write(key, value) + Yast::SCR.Write(path("#{SYSCONFIG_PATH}.#{key}"), value) + Yast::SCR.Write(path(SYSCONFIG_PATH), nil) + end + + # Reads the raw value for the FDE_DEVICES key + # + # @return [String, nil] nil if there is no value for FDE_EXTRA_DEVS + def fde_devices + read(FDE_DEVICES) + end + + # Writes the value for the FDE_DEVICES key + # + # @param value [String] + def fde_devices=(value) + write(FDE_DEVICES, value) + end + + # Reads the raw value for the FDE_LUKS_PBKDF key + # + # @return [String, nil] nil if there is no value for FDE_LUKS_PBKDF + def fde_luks_pbkdf + read(FDE_LUKS_PBKDF) + end + + # Writes the value for the FDE_LUKS_PBKDF key + # + # @param value [String] + def fde_luks_pbkdf=(value) + write(FDE_LUKS_PBKDF, value) + end + end + end +end diff --git a/src/lib/y2storage/encryption_processes/pervasive.rb b/src/lib/y2storage/encryption_processes/pervasive.rb index 69a5fc8636..791072f455 100644 --- a/src/lib/y2storage/encryption_processes/pervasive.rb +++ b/src/lib/y2storage/encryption_processes/pervasive.rb @@ -1,4 +1,4 @@ -# Copyright (c) [2019-2020] SUSE LLC +# Copyright (c) [2019-2023] SUSE LLC # # All Rights Reserved. # @@ -23,6 +23,8 @@ require "yast2/execute" require "yast" +Yast.import "Mode" + module Y2Storage module EncryptionProcesses # Encryption process that allows to create and identify a volume encrypted diff --git a/src/lib/y2storage/encryption_processes/tpm_fde_tools.rb b/src/lib/y2storage/encryption_processes/tpm_fde_tools.rb new file mode 100644 index 0000000000..11af9e1854 --- /dev/null +++ b/src/lib/y2storage/encryption_processes/tpm_fde_tools.rb @@ -0,0 +1,175 @@ +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast2/execute" +require "yast" +require "y2storage/encryption_type" +require "y2storage/encryption_processes/fde_tools" +require "y2storage/encryption_processes/fde_tools_config" + +Yast.import "Mode" + +module Y2Storage + module EncryptionProcesses + # Encryption process that allow to setup device unlocking via the TPM2 chip + # based on the fde-tools shipped with (open)SUSE distributions + # + # The process is only valid for the system installation case, adding devices in an + # already installed system may imply different steps. + # + # This assumes all the devices use the same recovery password. That's a fde-tools + # requirement. + # + # The process is based on the information and commands described at [1] and + # looks like this: + # + # - The devices are encrypted using LUKS2 as usual but ensuring certain values + # expected by fde-tools at the third and fourth column of crypttab. + # - At the end of installation, the following steps are performed in the target + # system: + # * The value of FDE_DEVS is adjusted in the fde-tools configuration, so the + # fdectl subcommands know in which devices they must operate. + # * "fdectl add-secondary-password" is executed once, so it "leaves the key under + # the doormat" for Grub2 to be able to unlock all the involved devices in the + # first boot after installation. + # * "fdectl add-secondary-key" is executed once, so it prepares all devices for + # the upcoming configuration. + # - In the first boot of the target system after installation, the service + # "fde-tpm-enroll" (that is enabled by the installer) takes care of finishing the + # configuration for all devices that have been prepared with a secondary key. + # + # [1] https://github.com/openSUSE/fde-tools + # + class TpmFdeTools < Base + # Options to add to the fourth column of crypttab for all involved devices + CRYPT_OPTIONS = ["x-initrd.attach"] + private_constant :CRYPT_OPTIONS + + # Content of the third column of crypttab for all involved devices + KEY_FILE_NAME = "/.fde-virtual.key".freeze + private_constant :KEY_FILE_NAME + + # Class methods + class << self + # List of all block devices configured by fde-tools during system installation + # + # A class variable is used to hold a single list of devices for all the processes. + # The list with all the affected devices is built during post_commit and then all + # devices are configured at once with a single execution of #finish_execution. + attr_accessor :devices + + # Content of the third column of crypttab for all involved devices + # + # @return [String] + def key_file_name + KEY_FILE_NAME.dup + end + end + + # Creates an encryption layer over the given block device + # + # @param blk_device [Y2Storage::BlkDevice] + # @param dm_name [String] + # @param label [String] optional LUKS2 label + # + # @return [Encryption] + def create_device(blk_device, dm_name, label: nil) + enc = super(blk_device, dm_name) + enc.label = label if label + enc.crypt_options |= CRYPT_OPTIONS + enc.key_file = self.class.key_file_name + enc.use_key_file_in_commit = false + # Maybe this configuration value affects only the slots added by fde-tools. In that + # case we could use any other PBKDF here. But being consistent with the PBKDF used + # in the fde-tools slots looks like a safer bet in this first implementation. + enc.pbkdf = FdeToolsConfig.instance.pbkd_function + enc + end + + # @see Base#encryption_type + def encryption_type + EncryptionType::LUKS2 + end + + # @see Base#post_commit + # + # TODO: this is implemented only for the installation case. In an installed system the procedure + # would be completely different and will likely include steps like configuring the fde-tools, + # calling "fdectl regenerate-key" and regenerating initrd and the bootloader configuration. + # + # @param device [Encryption] encryption that has just been created in the system + def post_commit(device) + return unless Yast::Mode.installation + + self.class.devices ||= [] + self.class.devices << device + end + + # @see Base#finish_installation + def finish_installation + # This procedure is only needed once + return if self.class.devices.empty? + + return unless configure_fde_tools(self.class.devices) + + fde = FdeTools.new(recovery_password) + fde.add_secondary_password && fde.add_secondary_key && fde.enroll_service&.enable + + # Mark as done, no need to do the same again since this already configured all + # the devices that are associated to EncryptionProcesses::TpmFdeTools objects + self.class.devices = [] + end + + # @see Base#commit_features + def commit_features + # In installation mode is needed to ensure the enroll service is present in the new system. + # In an installed system is needed in order to be able to execute the fdectl commands. + [YastFeature::ENCRYPTION_TPM_FDE] + end + + private + + # Configure fde-tools to act on all the involved block devices + # + # @return [Boolean] true if fde-tools were correctly configured + def configure_fde_tools(devices) + plain_devices = plain_names(devices) + config = FdeToolsConfig.instance + config.devices = plain_devices + + # Check if everything went well + config.devices == plain_devices + end + + # Device names of all the block devices being configured + # + # @return [Array] + def plain_names(devices) + devices.map { |d| d.plain_device.preferred_name }.sort + end + + # Shared password used in the interactive slot of all affected devices + # + # @return [String] + def recovery_password + self.class.devices.first.password + end + end + end +end diff --git a/src/lib/y2storage/feature.rb b/src/lib/y2storage/feature.rb new file mode 100644 index 0000000000..6fd28aa95a --- /dev/null +++ b/src/lib/y2storage/feature.rb @@ -0,0 +1,123 @@ +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" +require "storage" + +module Y2Storage + # Generalization of the concept of {StorageFeature}. + # + # In libstorage-ng the concept of "feature" is used to communicate the usage of some + # functionality that may require the presence in the system of some packages and tools. + # + # The sibling concept of {YastFeature} makes it possible for Y2Storage to add its own + # requirements. + # + # This is the abstract base class for both. + class Feature + include Yast::Logger + + # Constructor + # + # @param id [Symbol] see {#id} + # @param packages [Array] see {#all_packages} + def initialize(id, packages) + @id = id + @all_packages = packages + end + + # Symbol representation of the feature + # + # For StorageFeature objects, this has the same form than the corresponding constant + # name in libstorage-ng, eg. :UF_NTFS + # + # @return [Symbol] + attr_reader :id + + alias_method :to_sym, :id + + # Names of the packages that should be installed if the feature is going to be used + # + # @return [Array] + def pkg_list + packages.map(&:name) + end + + # Drop the cache about which packages related to the feature are available + def drop_cache + @packages = nil + end + + private + + # All packages that would be relevant for the feature, no matter if they are really available + # @return [Array] + attr_reader :all_packages + + # List of available packages associated to the feature + # + # @return [Array] + def packages + return @packages unless @packages.nil? + + unavailable, @packages = all_packages.partition(&:unavailable_optional?) + if unavailable.any? + log.warn("WARNING: Skipping unavailable support packages #{unavailable.map(&:name)}") + end + + @packages + end + + # Internal class to represent a package associated to a feature + class Package + Yast.import "Package" + + # Constructor + # + # @param name [String] see {#name} + # @param optional [Boolean] see {#optional?} + def initialize(name, optional: false) + @name = name + @optional = optional + end + + # @return [String] name of the package + attr_reader :name + + # Whether installation of the package can be skipped if the package is not + # available + # + # See the comment in {StorageFeature::OPTIONAL_PACKAGES} for more details + # + # @return [Boolean] + def optional? + !!@optional + end + + # Check if a package is an optional package that is unavailable. + # See also bsc#1039830 + # + # @return [Boolean] true if package is optional and unavailable, + # false if not optional or if available. + def unavailable_optional? + optional? && !Yast::Package.Available(name) + end + end + end +end diff --git a/src/lib/y2storage/storage_feature.rb b/src/lib/y2storage/storage_feature.rb index 3f63539c6d..565671f22a 100644 --- a/src/lib/y2storage/storage_feature.rb +++ b/src/lib/y2storage/storage_feature.rb @@ -1,4 +1,4 @@ -# Copyright (c) [2016-2020] SUSE LLC +# Copyright (c) [2016-2023] SUSE LLC # # All Rights Reserved. # @@ -19,6 +19,7 @@ require "yast" require "storage" +require "y2storage/feature" module Y2Storage # @@ -38,9 +39,7 @@ module Y2Storage # which add-on packages would be needed so support that feature, so they # can be installed or marked for installation as needed. # - class StorageFeature - include Yast::Logger - + class StorageFeature < Feature #====================================================================== # Configurable part starts here # @@ -83,9 +82,10 @@ class StorageFeature UF_SWAP: [], # Crypto technologies - UF_LUKS: "cryptsetup", UF_PLAIN_ENCRYPTION: "cryptsetup", UF_BITLOCKER: [], + # Device mapper is needed if names like /dev/mapper/cr_root are used at boot + UF_LUKS: ["device-mapper", "cryptsetup"], # Data transport methods UF_ISCSI: "open-iscsi", @@ -113,7 +113,7 @@ class StorageFeature # configurable part ends here #====================================================================== - # All known features + # All known libstorage-ng features # # @return [Array] def self.all @@ -125,12 +125,12 @@ def self.all end end - # Drop the cache of storage packages that are available. + # Drop the cache of packages for all known storage features # # This is only ever needed if the available packages might have changed # since the last use of this class. def self.drop_cache - @all = nil + all.each(&:drop_cache) end # Constructor @@ -143,8 +143,7 @@ def self.drop_cache # @param id [Symbol] see {#id} # @param packages [Array] see {#all_packages} def initialize(id, packages) - @id = id - @all_packages = packages + super # Raising a NameError exception as soon as possible (i.e. in the constructor) # is a good way to make sure we are in sync with libstorage-ng regarding @@ -152,16 +151,6 @@ def initialize(id, packages) @bitmask = ::Storage.const_get(id) end - # Symbol representation of the feature - # - # It has the same form than the corresponding constant name in - # libstorage-ng, eg. :UF_NTFS - # - # @return [Symbol] - attr_reader :id - - alias_method :to_sym, :id - # Whether the feature is included in the given bit-field # # @param bitfield [Integer] @@ -170,18 +159,8 @@ def in_bitfield?(bitfield) (bitfield & bitmask) == bitmask end - # Names of the packages that should be installed if the feature is going to - # be used - # - # @return [Array] - def pkg_list - packages.map(&:name) - end - private - attr_reader :all_packages - # Bitmask for a storage feature # # This looks up a constant in the ::Storage (libstorage-ng) namespace with @@ -189,55 +168,5 @@ def pkg_list # # @return [Integer] attr_reader :bitmask - - # List of packages associated to the feature - # - # @return [Array] - def packages - return @packages unless @packages.nil? - - unavailable, @packages = all_packages.partition(&:unavailable_optional?) - if unavailable.any? - log.warn("WARNING: Skipping unavailable filesystem support packages #{unavailable.map(&:name)}") - end - - @packages - end - - # Internal class to represent a package associated to a storage feature - class Package - Yast.import "Package" - - # Constructor - # - # @param name [String] see {#name} - # @param optional [Boolean] see {#optional?} - def initialize(name, optional: false) - @name = name - @optional = optional - end - - # @return [String] name of the package - attr_reader :name - - # Whether installation of the package can be skipped if the package is not - # available - # - # See the comment in {StorageFeature::OPTIONAL_PACKAGES} for more details - # - # @return [Boolean] - def optional? - !!@optional - end - - # Check if a package is an optional package that is unavailable. - # See also bsc#1039830 - # - # @return [Boolean] true if package is optional and unavailable, - # false if not optional or if available. - def unavailable_optional? - optional? && !Yast::Package.Available(name) - end - end end end diff --git a/src/lib/y2storage/storage_features_list.rb b/src/lib/y2storage/storage_features_list.rb index 65220e21f8..5636783aba 100644 --- a/src/lib/y2storage/storage_features_list.rb +++ b/src/lib/y2storage/storage_features_list.rb @@ -1,4 +1,4 @@ -# Copyright (c) [2016-2017,2019-2020] SUSE LLC +# Copyright (c) [2016-2023] SUSE LLC # # All Rights Reserved. # @@ -20,6 +20,7 @@ require "yast" require "forwardable" require "y2storage/storage_feature" +require "y2storage/yast_feature" module Y2Storage # List of storage features @@ -70,5 +71,12 @@ def pkg_list @pkg_list end + + # Concatenate the give features into the current list + # + # @param other_list [#to_a] + def concat(other_list) + @features.concat(other_list.to_a) + end end end diff --git a/src/lib/y2storage/yast_feature.rb b/src/lib/y2storage/yast_feature.rb new file mode 100644 index 0000000000..90cb073046 --- /dev/null +++ b/src/lib/y2storage/yast_feature.rb @@ -0,0 +1,59 @@ +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require "yast" +require "y2storage/feature" +require "yast2/equatable" + +module Y2Storage + # Analogous to {StorageFeature}, but for requirements originated by Y2Storage and not + # from libstorage-ng. + class YastFeature < Feature + include Yast2::Equatable + + eql_attr :id + + # Constructor + def initialize(id, mandatory_pkgs, optional_pkgs) + pkgs = mandatory_pkgs.map { |pkg| Package.new(pkg, optional: false) } + pkgs.concat(optional_pkgs.map { |pkg| Package.new(pkg, optional: true) }) + super(id, pkgs) + end + + # Instance of the feature to be always returned by the class + ENCRYPTION_TPM_FDE = new(:encryption_tpm_fde, ["fde-tools"], []) + + # All possible instances + ALL = [ENCRYPTION_TPM_FDE].freeze + private_constant :ALL + + # Sorted list of all features defined by YaST + def self.all + ALL.dup + end + + # Drop the cache of packages for all known YaST features + # + # This is only ever needed if the available packages might have changed + # since the last use of this class. + def self.drop_cache + all.each(&:drop_cache) + end + end +end diff --git a/src/scrconf/sysconfig_fde-tools.scr b/src/scrconf/sysconfig_fde-tools.scr new file mode 100644 index 0000000000..b4df170dd1 --- /dev/null +++ b/src/scrconf/sysconfig_fde-tools.scr @@ -0,0 +1,18 @@ +/** + * File: + * sysconfig_fde-tools.scr + * Summary: + * SCR Agent for reading/writing /etc/sysconfig/fde-tools + * Access: + * read/write + * + * $Id$ + * + * Read/Sets the values defined in /etc/sysconfig/fde-tools + * in an easy manner. + */ +.sysconfig.fde-tools + +`ag_ini( + `SysConfigFile("/etc/sysconfig/fde-tools") +) diff --git a/test/y2storage/devicegraph_test.rb b/test/y2storage/devicegraph_test.rb index e31f815c2c..1efbe06107 100755 --- a/test/y2storage/devicegraph_test.rb +++ b/test/y2storage/devicegraph_test.rb @@ -1,6 +1,6 @@ #!/usr/bin/env rspec -# Copyright (c) [2017-2021] SUSE LLC +# Copyright (c) [2017-2023] SUSE LLC # # All Rights Reserved. # @@ -1307,6 +1307,22 @@ def with_sda2_deleted(initial_graph) expect(features.map(&:id)) .to contain_exactly(:UF_BTRFS, :UF_XFS, :UF_SWAP) end + + context "if YaST needs some extra feature to configure a device" do + before do + sda2 = fake_devicegraph.find_by_name("/dev/sda2") + sda2.encrypt(method: Y2Storage::EncryptionMethod::TPM_FDE) + end + + it "returns the expected set of features" do + features = fake_devicegraph.used_features + expect(features).to be_a Y2Storage::StorageFeaturesList + expect(features.map(&:id)) + .to contain_exactly( + :UF_BTRFS, :UF_EXT4, :UF_NTFS, :UF_XFS, :UF_SWAP, :UF_LUKS, :encryption_tpm_fde + ) + end + end end context "with unformatted DASD and FC devices" do diff --git a/test/y2storage/encryption_method/tpm_fde_test.rb b/test/y2storage/encryption_method/tpm_fde_test.rb new file mode 100644 index 0000000000..345554601d --- /dev/null +++ b/test/y2storage/encryption_method/tpm_fde_test.rb @@ -0,0 +1,161 @@ +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../spec_helper" +require "y2storage" + +describe Y2Storage::EncryptionMethod::TpmFde do + describe ".used_for?" do + let(:encryption) { double(Y2Storage::Encryption, type: type) } + + context "when the encryption type is LUKS1" do + let(:type) { Y2Storage::EncryptionType::LUKS1 } + + it "returns false" do + expect(subject.used_for?(encryption)).to eq(false) + end + end + + context "when the encryption type is plain" do + let(:type) { Y2Storage::EncryptionType::PLAIN } + + it "returns false" do + expect(subject.used_for?(encryption)).to eq(false) + end + end + + context "when the encryption type is LUKS2" do + let(:type) { Y2Storage::EncryptionType::LUKS2 } + + it "returns false" do + expect(subject.used_for?(encryption)).to eq(false) + end + end + end + + describe ".only_for_swap?" do + it "returns false" do + expect(subject.only_for_swap?).to eq(false) + end + end + + describe "#password_required?" do + it "returns true" do + expect(subject.password_required?).to eq(true) + end + end + + describe "#available? and #possible?" do + before do + Y2Storage::StorageManager.create_test_instance + + allow(Yast::Execute).to receive(:on_target!).with(/fdectl/, "tpm-present") do + raise(Cheetah::ExecutionFailed.new("", "", "", "")) unless tpm_present + end + + allow(Y2Storage::Arch).to receive(:new).and_return(arch) + + allow(Yast::Package).to receive(:AvailableAll).and_return pkgs_available + end + + let(:arch) { instance_double("Y2Storage::Arch", efiboot?: efi) } + + RSpec.shared_examples "TPM_FDE impossible and not available" do + it "both #possible? and #available? returns false" do + expect(subject.available?).to eq false + expect(subject.possible?).to eq false + end + end + + context "if the system boots using EFI" do + let(:efi) { true } + + context "and there is a working TPM2 chip" do + let(:tpm_present) { true } + + context "and the needed packages can be installed in the target system" do + let(:pkgs_available) { true } + + it "#possible? returns true and #available? returns false" do + expect(subject.available?).to eq false + expect(subject.possible?).to eq true + end + end + + context "and the needed packages can not be installed in the target system" do + let(:pkgs_available) { false } + + include_examples "TPM_FDE impossible and not available" + end + end + + context "and there is no TPM2 chip" do + let(:tpm_present) { false } + + context "and the needed packages can be installed in the target system" do + let(:pkgs_available) { true } + + include_examples "TPM_FDE impossible and not available" + end + + context "and the needed packages can not be installed in the target system" do + let(:pkgs_available) { false } + + include_examples "TPM_FDE impossible and not available" + end + end + end + + context "if the system does not use EFI" do + let(:efi) { false } + + context "and there is a working TPM2 chip" do + let(:tpm_present) { true } + + context "and the needed packages can be installed in the target system" do + let(:pkgs_available) { true } + + include_examples "TPM_FDE impossible and not available" + end + + context "and the needed packages can not be installed in the target system" do + let(:pkgs_available) { false } + + include_examples "TPM_FDE impossible and not available" + end + end + + context "and there is no TPM2 chip" do + let(:tpm_present) { false } + + context "and the needed packages can be installed in the target system" do + let(:pkgs_available) { true } + + include_examples "TPM_FDE impossible and not available" + end + + context "and the needed packages can not be installed in the target system" do + let(:pkgs_available) { false } + + include_examples "TPM_FDE impossible and not available" + end + end + end + end +end diff --git a/test/y2storage/encryption_method_test.rb b/test/y2storage/encryption_method_test.rb index 09492ea198..bb971382f4 100755 --- a/test/y2storage/encryption_method_test.rb +++ b/test/y2storage/encryption_method_test.rb @@ -49,8 +49,8 @@ expect(described_class.all.map(&:to_sym)).to include(:secure_swap) end - it "returns a method for pervasive luks2" do - expect(described_class.all.map(&:to_sym)).to include(:pervasive_luks2) + it "contains a method for TPM full-disk encryption" do + expect(described_class.all.map(&:to_sym)).to include(:tpm_fde) end end @@ -193,6 +193,26 @@ def lszcrypt_output(file) expect(described_class.available.map(&:to_sym)).to_not include(:secure_swap) end end + + context "if TPM full-disk encryption is available" do + before do + allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:available?).and_return(true) + end + + it "includes the corresponding method" do + expect(described_class.available.map(&:to_sym)).to include(:tpm_fde) + end + end + + context "if TPM full-disk encryption is not available" do + before do + allow(Y2Storage::EncryptionMethod::TPM_FDE).to receive(:available?).and_return(false) + end + + it "does not include the TPM FDE method" do + expect(described_class.available.map(&:to_sym)).to_not include(:tpm_fde) + end + end end describe ".find" do @@ -284,6 +304,33 @@ def lszcrypt_output(file) end end + context "when using :tpm_fde method" do + let(:method) { :tpm_fde } + + it "returns an encryption device" do + result = subject.create_device(device, "cr_dev") + + expect(result.is?(:encryption)).to eq(true) + end + + it "encrypts the given device with LUKS2 encryption" do + expect(device.encrypted?).to eq(false) + + subject.create_device(device, "cr_dev") + + expect(device.encrypted?).to eq(true) + expect(device.encryption.type.is?(:luks2)).to eq(true) + end + + it "sets the given label for the LUKS2 device" do + expect(device.encrypted?).to eq(false) + + subject.create_device(device, "cr_dev", label: "fde_label") + + expect(device.encryption.label).to eq "fde_label" + end + end + shared_examples "swap methods" do it "returns an encryption device" do result = subject.create_device(device, "cr_dev") diff --git a/test/y2storage/encryption_processes/fde_tools_config_test.rb b/test/y2storage/encryption_processes/fde_tools_config_test.rb new file mode 100755 index 0000000000..6bb787208b --- /dev/null +++ b/test/y2storage/encryption_processes/fde_tools_config_test.rb @@ -0,0 +1,135 @@ +#!/usr/bin/env rspec +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "../spec_helper" + +require "yast" +require "y2storage/encryption_processes/fde_tools_config" + +describe Y2Storage::EncryptionProcesses::FdeToolsConfig do + before { Y2Storage::StorageManager.create_test_instance } + subject { described_class.instance } + + describe "#pbkd_function" do + before do + allow(Yast::SCR).to receive(:Read) { |p| expect(p.to_s).to match(/fde-tools.FDE_LUKS_PBKDF/) } + .and_return(value) + end + + let(:value) { nil } + + it "returns a PbkdFunction object" do + expect(subject.pbkd_function).to be_a(Y2Storage::PbkdFunction) + end + + context "when there is a value for FDE_LUKS_PBKDF at the fde-tools config file" do + context "and the value corresponds to a known derivation function" do + let(:value) { "Argon2i" } + + it "returns the corresponding PbkdFunction object" do + expect(subject.pbkd_function.is?(:argon2i)).to eq(true) + end + end + + context "and the value does not correspond to any known derivation function" do + let(:value) { "foo" } + + it "returns the default pbkdf2 object" do + expect(subject.pbkd_function.is?(:pbkdf2)).to eq(true) + end + end + end + + context "when there is no value for FDE_LUKS_PBKDF at the fde-tools config file" do + let(:value) { nil } + + it "returns the default pbkdf2 object" do + expect(subject.pbkd_function.is?(:pbkdf2)).to eq(true) + end + end + end + + describe "#pbkd_function=" do + before { allow(Yast::SCR).to receive(:Write) } + + it "stores the corresponding value for FDE_LUKS_PBKDF at the fde-tools config file" do + expect(Yast::SCR).to receive(:Write) do |path, value| + expect(path.to_s).to match(/fde-tools.FDE_LUKS_PBKDF/) + expect(value).to eq("argon2id") + end + + subject.pbkd_function = Y2Storage::PbkdFunction::ARGON2ID + end + end + + describe "#devices" do + before do + allow(Yast::SCR).to receive(:Read) { |p| expect(p.to_s).to match(/fde-tools.FDE_DEVS/) } + .and_return(value) + end + + context "when there is a value for FDE_DEVS at the fde-tools config file" do + context "and the value is an empty string" do + let(:value) { "" } + + it "returns an empty array" do + expect(subject.devices).to eq [] + end + end + + context "and the value contains just a device name" do + let(:value) { "/dev/sda" } + + it "returns an array with the device name as only element" do + expect(subject.devices).to eq ["/dev/sda"] + end + end + + context "and the value contains several space-separated device names" do + let(:value) { "/dev/sda2 /dev/sdb2 /dev/sdc2" } + + it "returns an array with all the device names" do + expect(subject.devices).to eq ["/dev/sda2", "/dev/sdb2", "/dev/sdc2"] + end + end + end + + context "when there is no value for FDE_DEVS at the fde-tools config file" do + let(:value) { nil } + + it "returns an empty array" do + expect(subject.devices).to eq [] + end + end + end + + describe "#devices=" do + before { allow(Yast::SCR).to receive(:Write) } + + it "stores the elements of the given array as space-separated strings at FDE_DEVS" do + expect(Yast::SCR).to receive(:Write) do |path, value| + expect(path.to_s).to match(/fde-tools.FDE_DEVS/) + expect(value).to eq("/dev/one /dev/two") + end + + subject.devices = ["/dev/one", "/dev/two"] + end + end +end diff --git a/test/y2storage/storage_features_list_test.rb b/test/y2storage/storage_features_list_test.rb index ffa61b556d..069ff2db78 100755 --- a/test/y2storage/storage_features_list_test.rb +++ b/test/y2storage/storage_features_list_test.rb @@ -2,7 +2,7 @@ # # encoding: utf-8 -# Copyright (c) [2017,2019-2020] SUSE LLC +# Copyright (c) [2017-2023] SUSE LLC # # All Rights Reserved. # @@ -77,7 +77,7 @@ end it "includes the package only once (no duplicates)" do - expect(list.pkg_list.sort).to eq ["cryptsetup", "e2fsprogs"] + expect(list.pkg_list.sort).to eq ["cryptsetup", "device-mapper", "e2fsprogs"] end end @@ -110,5 +110,20 @@ expect(list.pkg_list).to eq [] end end + + context "when the list includes both storage and YaST features" do + let(:bits) { Storage::UF_LUKS } + + before do + # Not really needed yet since we have no optional packages in any YaST feature + Y2Storage::YastFeature.drop_cache + + list.concat(Y2Storage::YastFeature.all) + end + + it "includes the packages related to both kind of features" do + expect(list.pkg_list).to include("device-mapper", "cryptsetup", "fde-tools") + end + end end end diff --git a/test/y2storage/tpm_fde_encryption_test.rb b/test/y2storage/tpm_fde_encryption_test.rb new file mode 100755 index 0000000000..5d5e41ea81 --- /dev/null +++ b/test/y2storage/tpm_fde_encryption_test.rb @@ -0,0 +1,109 @@ +#!/usr/bin/env rspec +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "spec_helper" +require "y2storage" +require "yast2/execute" + +describe "TPM full-disk encryption" do + before do + fake_scenario("mixed_disks") + end + + let(:manager) { Y2Storage::StorageManager.instance } + let(:tpm_fde) { Y2Storage::EncryptionMethod::TPM_FDE } + + describe "Y2Storage::BlkDevice#encrypt" do + let(:blk_device) { manager.staging.find_by_name("/dev/sda2") } + + it "creates an encryption device with type LUKS2 and method TPM_FDE" do + enc = blk_device.encrypt(method: tpm_fde) + expect(enc.type.is?(:luks2)).to eq(true) + expect(enc.method).to eq tpm_fde + end + + it "initializes the crypttab fields to the appropriate values" do + enc = blk_device.encrypt(method: tpm_fde) + expect(enc.crypt_options).to include "x-initrd.attach" + expect(enc.key_file).to eq "/.fde-virtual.key" + expect(enc.use_key_file_in_commit?).to eq false + end + end + + describe "finish installation" do + before do + allow(manager.storage).to receive(:calculate_actiongraph) + allow(manager.storage).to receive(:commit) + + allow(Yast::Mode).to receive(:installation).and_return true + allow(Yast::Stage).to receive(:initial).and_return true + + allow(Yast::SCR).to receive(:Write) + # The code verifies the values were actually written, let's emulate that + allow(Yast::SCR).to receive(:Read) do |path| + blk_devices.join(" ") if path.to_s.match?(/fde-tools.FDE_DEVS/) + end + + expect(Yast2::Systemd::Service).to receive(:find).with("fde-tpm-enroll.service") + .and_return(enroll_service) + + devices = blk_devices.map { |d| manager.staging.find_by_name(d) } + devices.each { |d| d.encrypt(method: tpm_fde) } + + allow(Yast::Execute).to receive(:on_target!) + end + + let(:blk_devices) { ["/dev/sda2", "/dev/sdb2"] } + let(:enroll_service) { double(Yast2::Systemd::Service, enable: true) } + + it "adds the encrypted block devices to the fde-tools configuration" do + expect(Yast::SCR).to receive(:Write) do |path, value| + expect(path.to_s).to match(/fde-tools.FDE_DEVS/) + expect(value).to eq(blk_devices.join(" ")) + end + + manager.commit + manager.staging.finish_installation + end + + it "calls only once the command to add secondary passwords" do + expect(Yast::Execute).to receive(:on_target!) + .with(/fdectl/, "add-secondary-password", any_args).once + + manager.commit + manager.staging.finish_installation + end + + it "calls only once the command to add secondary keys" do + expect(Yast::Execute).to receive(:on_target!) + .with(/fdectl/, "add-secondary-key", any_args).once + + manager.commit + manager.staging.finish_installation + end + + it "enables the enroll service" do + expect(enroll_service).to receive(:enable).and_return true + + manager.commit + manager.staging.finish_installation + end + end +end diff --git a/test/y2storage/yast_feature_test.rb b/test/y2storage/yast_feature_test.rb new file mode 100755 index 0000000000..a09a18787c --- /dev/null +++ b/test/y2storage/yast_feature_test.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env rspec +# +# encoding: utf-8 + +# Copyright (c) [2023] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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, contact SUSE LLC. +# +# To contact SUSE LLC about this file by physical or electronic mail, you may +# find current contact information at www.suse.com. + +require_relative "spec_helper" +require "y2storage/yast_feature" + +describe Y2Storage::YastFeature do + describe "eql?" do + # Pretty trivial test now that we only have one feature, to be improved in the future + it "returns true when comparing the same feature" do + expect(Y2Storage::YastFeature.all.first).to eq Y2Storage::YastFeature::ENCRYPTION_TPM_FDE + end + end +end