diff --git a/pkg/storaged/crypto-keyslots.jsx b/pkg/storaged/crypto-keyslots.jsx index 12754dd342d8..f4842e5c0502 100644 --- a/pkg/storaged/crypto-keyslots.jsx +++ b/pkg/storaged/crypto-keyslots.jsx @@ -25,21 +25,24 @@ import { Checkbox, ClipboardCopy, Form, FormGroup, DataListItem, DataListItemRow, DataListItemCells, DataListCell, DataList, - Text, TextVariants, TextInput as TextInputPF, Stack, + TextContent, Text, TextVariants, TextList, TextListItem, TextInput as TextInputPF, Stack, } from "@patternfly/react-core"; import { EditIcon, MinusIcon, PlusIcon } from "@patternfly/react-icons"; import sha1 from "js-sha1"; import sha256 from "js-sha256"; import stable_stringify from "json-stable-stringify-without-jsonify"; +import { check_missing_packages, install_missing_packages, Enum as PkEnum } from "packagekit"; import { dialog_open, SelectOneRadio, TextInput, PassInput, Skip } from "./dialog.jsx"; -import { array_find, decode_filename, block_name } from "./utils.js"; +import { array_find, decode_filename, encode_filename, block_name, for_each_async } from "./utils.js"; import { fmt_to_fragments } from "utils.jsx"; import { StorageButton } from "./storage-controls.jsx"; +import { parse_options, unparse_options } from "./format-dialog.jsx"; +import { edit_config } from "./crypto-tab.jsx"; import clevis_luks_passphrase_sh from "raw-loader!./clevis-luks-passphrase.sh"; @@ -253,6 +256,197 @@ export function init_existing_passphrase(block, just_type, callback) { }; } +/* Getting the system ready for NBDE on the root filesystem. + + We need the clevis module in the initrd. If it is not there, the + clevis-dracut package should be installed and the initrd needs to + be regenerated. We do this only after the user has agreed to it. + + The kernel command line needs to have rd.neednet=1 in it. We just + do this unconditionally because it's so fast. +*/ + +function ensure_package_installed(steps, progress, package_name) { + function status_callback(progress) { + return p => { + let text = null; + if (p.waiting) { + text = _("Waiting for other software management operations to finish"); + } else if (p.package) { + let fmt; + if (p.info == PkEnum.INFO_DOWNLOADING) + fmt = _("Downloading $0"); + else if (p.info == PkEnum.INFO_REMOVING) + fmt = _("Removing $0"); + else + fmt = _("Installing $0"); + text = cockpit.format(fmt, p.package); + } + progress(text, p.cancel); + }; + } + + progress(cockpit.format(_("Checking for $0 package"), package_name), null); + return check_missing_packages([package_name], null) + .then(data => { + progress(null, null); + if (data.missing_names.length + data.unavailable_names.length > 0) + steps.push({ + title: cockpit.format(_("The $0 package must be installed."), package_name), + func: progress => { + if (data.remove_names.length > 0) + return Promise.reject(cockpit.format(_("Installing $0 would remove $1."), name, data.remove_names[0])); + else if (data.unavailable_names.length > 0) + return Promise.reject(cockpit.format(_("The $0 package is not available from any repository."), name)); + else + return install_missing_packages(data, status_callback(progress)); + } + }); + }); +} + +function ensure_initrd_clevis_support(steps, progress, package_name) { + const task = cockpit.spawn(["lsinitrd", "-m"], { superuser: true, err: "message" }); + progress(_("Checking for NBDE support in the initrd"), () => task.close()); + return task.then(data => { + progress(null, null); + if (data.indexOf("clevis") < 0) { + return ensure_package_installed(steps, progress, package_name) + .then(() => { + steps.push({ + title: _("The initrd must be regenerated."), + func: progress => { + // dracut doesn't react to SIGINT, so let's not enable our Cancel button + progress(_("Regenerating initrd"), null); + return cockpit.spawn(["dracut", "--force", "--regenerate-all"], + { superuser: true, err: "message" }); + } + }); + }); + } + }); +} + +function ensure_root_nbde_support(steps, progress) { + progress(_("Adding rd.neednet=1 to kernel command line"), null); + return cockpit.spawn(["grubby", "--update-kernel=ALL", "--args=rd.neednet=1"], + { superuser: true, err: "message" }) + .then(() => ensure_initrd_clevis_support(steps, progress, "clevis-dracut")); +} + +function ensure_fstab_option(steps, progress, client, block, option) { + const cleartext = client.blocks_cleartext[block.path]; + const crypto = client.blocks_crypto[block.path]; + const fsys_config = (cleartext + ? array_find(cleartext.Configuration, function (c) { return c[0] == "fstab" }) + : array_find(crypto.ChildConfiguration, function (c) { return c[0] == "fstab" })); + const fsys_options = fsys_config && parse_options(decode_filename(fsys_config[1].opts.v)); + + if (!fsys_options || fsys_options.indexOf(option) >= 0) + return Promise.resolve(); + + const new_fsys_options = fsys_options.concat([option]); + const new_fsys_config = [ + "fstab", + Object.assign({ }, fsys_config[1], + { + opts: { + t: 'ay', + v: encode_filename(unparse_options(new_fsys_options)) + } + }) + ]; + progress(cockpit.format(_("Adding \"$0\" to filesystem options"), option), null); + return block.UpdateConfigurationItem(fsys_config, new_fsys_config, { }); +} + +function ensure_crypto_option(steps, progress, client, block, option) { + const crypto_config = array_find(block.Configuration, function (c) { return c[0] == "crypttab" }); + const crypto_options = crypto_config && parse_options(decode_filename(crypto_config[1].options.v)); + if (!crypto_options || crypto_options.indexOf(option) >= 0) + return Promise.resolve(); + + const new_crypto_options = crypto_options.concat([option]); + progress(cockpit.format(_("Adding \"$0\" to encryption options"), option), null); + return edit_config(block, (config, commit) => { + config.options = { t: 'ay', v: encode_filename(unparse_options(new_crypto_options)) }; + return commit(); + }); +} + +function ensure_systemd_unit_enabled(steps, progress, name, package_name) { + progress(cockpit.format(_("Enabling $0"), name)); + return cockpit.spawn(["systemctl", "is-enabled", name], { err: "message" }) + .catch((err, output) => { + if (err && output == "" && package_name) { + // We assume that installing the package will enable the unit. + return ensure_package_installed(steps, progress, package_name); + } else + return cockpit.spawn(["systemctl", "enable", name], + { superuser: true, err: "message" }); + }); +} + +function ensure_non_root_nbde_support(steps, progress, client, block) { + return ensure_systemd_unit_enabled(steps, progress, "remote-cryptsetup.target") + .then(() => ensure_systemd_unit_enabled(steps, progress, "clevis-luks-askpass.path", "clevis-systemd")) + .then(() => ensure_fstab_option(steps, progress, client, block, "_netdev")) + .then(() => ensure_crypto_option(steps, progress, client, block, "_netdev")); +} + +function ensure_nbde_support(steps, progress, client, block) { + const cleartext = client.blocks_cleartext[block.path]; + const crypto = client.blocks_crypto[block.path]; + const fsys_config = (cleartext + ? array_find(cleartext.Configuration, function (c) { return c[0] == "fstab" }) + : array_find(crypto.ChildConfiguration, function (c) { return c[0] == "fstab" })); + const dir = decode_filename(fsys_config[1].dir.v); + + if (dir == "/") { + if (client.get_config("nbde_root_help", false)) { + steps.is_root = true; + return ensure_root_nbde_support(steps, progress); + } else + return Promise.resolve(); + } else + return ensure_non_root_nbde_support(steps, progress, client, block); +} + +function ensure_nbde_support_dialog(steps, client, block, url, adv, old_key, existing_passphrase) { + const dlg = dialog_open({ + Title: _("Add Network Bound Disk Encryption"), + Body: ( + + + { steps.is_root + ? _("The system does not currently support unlocking the root filesystem with a Tang keyserver.") + : _("The system does not currently support unlocking a filesystem with a Tang keyserver during boot.") + } + + + {_("These additional steps are necessary:")} + + + { steps.map((s, i) => {s.title}) } + + ), + Fields: existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase.")), + Action: { + Title: _("Fix NBDE support"), + action: (vals, progress) => { + return for_each_async(steps, s => s.func(progress)) + .then(() => { + steps = []; + progress(_("Adding key"), null); + return add_or_update_tang(dlg, vals, block, + url, adv, old_key, + vals.passphrase || existing_passphrase); + }); + } + } + }); +} + function parse_url(url) { // clevis-encrypt-tang defaults to "http://" (via curl), so we do the same here. if (!/^[a-zA-Z]+:\/\//.test(url)) @@ -312,15 +506,16 @@ function add_dialog(client, block) { ].concat(existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase."))), Action: { Title: _("Add"), - action: function (vals) { + action: function (vals, progress) { const existing_passphrase = vals.passphrase || recovered_passphrase; if (!client.features.clevis || vals.type == "luks-passphrase") { return passphrase_add(block, vals.new_passphrase, existing_passphrase); } else { - return get_tang_adv(vals.tang_url).then(function (adv) { - edit_tang_adv(client, block, null, - vals.tang_url, adv, existing_passphrase); - }); + return get_tang_adv(vals.tang_url) + .then(adv => { + edit_tang_adv(client, block, null, + vals.tang_url, adv, existing_passphrase); + }); } } }, @@ -376,6 +571,14 @@ function edit_clevis_dialog(client, block, key) { }); } +function add_or_update_tang(dlg, vals, block, url, adv, old_key, passphrase) { + return clevis_add(block, "tang", { url: url, adv: adv }, vals.passphrase || passphrase).then(() => { + if (old_key) + return clevis_remove(block, old_key); + }) + .catch(request_passphrase_on_error_handler(dlg, vals, passphrase, block)); +} + function edit_tang_adv(client, block, key, url, adv, passphrase) { const parsed = parse_url(url); const cmd = cockpit.format("ssh $0 tang-show-keys $1", parsed.hostname, parsed.port); @@ -408,12 +611,26 @@ function edit_tang_adv(client, block, key, url, adv, passphrase) { Fields: existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase.")), Action: { Title: _("Trust key"), - action: function (vals) { - return clevis_add(block, "tang", { url: url, adv: adv }, vals.passphrase || passphrase).then(() => { - if (key) - return clevis_remove(block, key); - }) - .catch(request_passphrase_on_error_handler(dlg, vals, passphrase, block)); + action: function (vals, progress) { + if (key) { + return add_or_update_tang(dlg, vals, block, + url, adv, key, + passphrase); + } else { + const steps = []; + return ensure_nbde_support(steps, progress, client, block) + .then(() => { + if (steps.length > 0) + ensure_nbde_support_dialog(steps, client, block, url, + adv, key, passphrase); + else { + progress(null, null); + return add_or_update_tang(dlg, vals, block, + url, adv, key, + passphrase); + } + }); + } } } }); diff --git a/pkg/storaged/manifest.json b/pkg/storaged/manifest.json index 1e686ab11b44..fac4f1dcf687 100644 --- a/pkg/storaged/manifest.json +++ b/pkg/storaged/manifest.json @@ -61,7 +61,11 @@ "stratis_package": { "fedora": "stratisd", "centos": "stratisd", "arch": "stratisd" - } + }, + "nbde_root_help": { "fedora": true, + "centos": true, + "rhel": true + } }, "content-security-policy": "img-src 'self' data:" } diff --git a/test/common/packagelib.py b/test/common/packagelib.py index 3a8828374741..7d7c719f9b8b 100644 --- a/test/common/packagelib.py +++ b/test/common/packagelib.py @@ -383,6 +383,9 @@ def createYumUpdateInfo(self): xml += '\n' return xml + def addPackageSet(self, name): + self.machine.execute(f"mkdir -p {self.repo_dir}; cp /var/lib/package-sets/{name}/* {self.repo_dir}") + def enableRepo(self): if self.backend == "apt": self.createAptChangelogs() diff --git a/test/common/storagelib.py b/test/common/storagelib.py index dfe8c9f2c51c..d35f84f04f22 100644 --- a/test/common/storagelib.py +++ b/test/common/storagelib.py @@ -329,7 +329,7 @@ def dialog_cancel(self): def dialog_wait_close(self): # file system operations often take longer than 10s - with self.browser.wait_timeout(60): + with self.browser.wait_timeout(max(self.browser.cdp.timeout, 60)): self.browser.wait_not_present('#dialog') def dialog_check(self, expect): @@ -496,13 +496,13 @@ def setup_systemd_password_agent(self, password): self.write_file("/usr/local/bin/test-password-agent", f"""#!/bin/sh +# Sleep a bit to avoid starting this agent too quickly over and over, +# and so that other agents get a chance as well. +sleep 30 + for s in $(grep -h ^Socket= /run/systemd/ask-password/ask.* | sed 's/^Socket=//'); do printf '%s' '{password}' | /usr/lib/systemd/systemd-reply-password 1 $s done - -# Sleep a bit to avoid starting this agent too quickly over and over, -# which would be wasteful and also cause systemd to block us. -sleep 2 """, perm="0755") self.write_file("/etc/systemd/system/test-password-agent.service", @@ -529,6 +529,83 @@ def setup_systemd_password_agent(self, password): """) self.machine.execute("ln -s ../test-password-agent.path /etc/systemd/system/sysinit.target.wants/") + def encrypt_root(self, passphrase): + m = self.machine + + # Set up a password agent in the old root and then arrange for + # it to be included in the initrd. This will unlock the new + # encrypted root during boot. + # + # The password agent and its initrd configuration will be + # copied to the new root, so it will stay in place also when + # the initrd is regenerated again from within the new root. + + self.setup_systemd_password_agent(passphrase) + m.write("/etc/dracut.conf.d/01-askpass.conf", + 'install_items+=" /usr/local/bin/test-password-agent ' + + '/etc/systemd/system/test-password-agent.service ' + + '/etc/systemd/system/test-password-agent.path ' + + '/etc/systemd/system/sysinit.target.wants/test-password-agent.path "') + + # The first step is to move /boot to a new unencrypted + # partition on the new disk but keep it mounted at /boot. + # This helps when running grub2-install and grub2-mkconfig, + # which will look at /boot and do the right thing. + # + # Then we copy (most of) the old root to the new disk, into a + # LUKS container. + # + # The kernel command line is changed to use the new root + # filesystem, and grub is installed on the new disk. The boot + # configuration of the VM has been changed to boot from the + # new disk. + # + # At that point the new root can be booted by the existing + # initrd, but the initrd will prompt for the passphrase (as + # expected). Thus, the initrd is regenerated to include the + # password agent from above. + # + # Before the reboot, we destroy the original disk to make + # really sure that it wont be used anymore. + + info = m.add_disk("4G", serial="NEWROOT", boot_disk=True) + dev = "/dev/" + info["dev"] + wait(lambda: m.execute(f"test -b {dev} && echo present").strip() == "present") + m.execute(f""" +parted -s {dev} mktable msdos +parted -s {dev} mkpart primary ext4 1M 300M +parted -s {dev} mkpart primary ext4 300M 100% +echo {passphrase} | cryptsetup luksFormat {dev}2 +echo {passphrase} | cryptsetup luksOpen {dev}2 dm-test +luks_uuid=$(blkid -p {dev}2 -s UUID -o value) +mkfs.ext4 /dev/mapper/dm-test +mkdir /new-root +mount /dev/mapper/dm-test /new-root +mkfs.ext4 {dev}1 +mkdir /new-root/boot +mount {dev}1 /new-root/boot +tar --one-file-system -cf - --exclude /boot --exclude='/var/tmp/*' --exclude='/var/cache/*' --exclude='/var/lib/mock/*' --exclude='/var/lib/containers/*' / | tar -C /new-root -xf - +touch /new-root/.autorelabel +tar --one-file-system -C /boot -cf - . | tar -C /new-root/boot -xf - +umount /new-root/boot +mount {dev}1 /boot +echo "(hd0) {dev}" >/boot/grub2/device.map +sed -i -e 's,/boot/,/,' /boot/loader/entries/* +uuid=$(blkid -p /dev/mapper/dm-test -s UUID -o value) +buuid=$(blkid -p {dev}1 -s UUID -o value) +echo "UUID=$uuid / auto defaults 0 0" >/new-root/etc/fstab +echo "UUID=$buuid /boot auto defaults 0 0" >>/new-root/etc/fstab +dracut --regenerate-all --force +grub2-install {dev} +grub2-mkconfig -o /boot/grub2/grub.cfg +grubby --update-kernel=ALL --args="root=UUID=$uuid rootflags=defaults rd.luks.uuid=$luks_uuid" +! test -f /etc/kernel/cmdline || cp /etc/kernel/cmdline /new-root/etc/kernel/cmdline +""", timeout=300) + luks_uuid = m.execute(f"blkid -p {dev}2 -s UUID -o value").strip() + m.spawn("dd if=/dev/zero of=/dev/vda bs=1M count=100; reboot", "reboot", check=False) + m.wait_reboot(300) + self.assertEqual(m.execute("findmnt -n -o SOURCE /").strip(), f"/dev/mapper/luks-{luks_uuid}") + class StorageCase(MachineCase, StorageHelpers): diff --git a/test/verify/check-storage-luks b/test/verify/check-storage-luks index 0d60d8aa9557..51a875324234 100755 --- a/test/verify/check-storage-luks +++ b/test/verify/check-storage-luks @@ -19,7 +19,16 @@ import parent # noqa: F401 from storagelib import StorageCase -from testlib import test_main, todoPybridge, wait +from packagelib import PackageCase +from testlib import skipImage, test_main, todoPybridge, wait, timeout, attach +import subprocess + + +def console_screenshot(machine, name): + subprocess.run("virsh -c qemu:///session screenshot %s '%s'" % (str(machine._domain.ID()), name), + shell=True) + attach(name, move=True) + print("Wrote screenshot to " + name) class TestStorageLuks(StorageCase): @@ -190,87 +199,6 @@ class TestStorageLuks(StorageCase): b.logout() wait(lambda: m.execute("(loginctl list-users | grep admin) || true") == "") - def testClevisTang(self): - m = self.machine - b = self.browser - - mount_point_secret = "/run/secret" - - m.execute("systemctl start tangd.socket") - - self.login_and_go("/storage") - - # Add a disk and format it with luks - m.add_disk("50M", serial="MYDISK") - b.wait_in_text("#drives", "MYDISK") - b.click('.sidepanel-row:contains("MYDISK")') - b.wait_visible("#storage-detail") - - self.content_row_action(1, "Format") - self.dialog({"type": "ext4", - "crypto": self.default_crypto_type, - "name": "ENCRYPTED", - "mount_point": mount_point_secret, - "mount_options.auto": False, - "passphrase": "vainu-reku-toma-rolle-kaja", - "passphrase2": "vainu-reku-toma-rolle-kaja"}) - self.content_row_wait_in_col(1, 2, "Filesystem (encrypted)") - self.content_tab_wait_in_info(1, 1, "Mount point", "The filesystem is not mounted") - - self.content_tab_wait_in_info(1, 2, "Options", "none") - tab = self.content_tab_expand(1, 2) - panel = tab + " .pf-c-card:contains(Keys) " - b.wait_visible(panel) - b.wait_in_text(panel + "ul li:nth-child(1)", "Passphrase") - - # Add a key - # - b.click(panel + "[aria-label=Add]") - self.dialog_wait_open() - self.dialog_wait_apply_enabled() - self.dialog_set_val("type", "tang") - self.dialog_set_val("tang_url", "127.0.0.1") - self.dialog_set_val("passphrase", "wrong-passphrase") - self.dialog_apply() - b.wait_in_text("#dialog", "Make sure the key hash from the Tang server matches") - b.wait_in_text("#dialog", m.execute("tang-show-keys").strip()) - self.dialog_apply() - b.wait_in_text("#dialog", "No key available with this passphrase.") - self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja") - self.dialog_apply() - self.dialog_wait_close() - b.wait_visible(panel + "ul li:nth-child(2)") - b.wait_in_text(panel + "ul li:nth-child(2)", "127.0.0.1") - - # Mount it. This should succeed without passphrase. - # - self.content_row_action(1, "Mount") - self.dialog_wait_open() - self.dialog_wait_val("mount_point", mount_point_secret) - self.dialog_apply() - self.dialog_wait_close() - self.content_row_wait_in_col(1, 2, "ext4 filesystem") - - # Edit the key, without providing an existing passphrase - # - b.click(panel + "ul li:nth-child(2) [aria-label=Edit]") - self.dialog_wait_open() - self.dialog_wait_apply_enabled() - self.dialog_wait_val("tang_url", "127.0.0.1") - self.dialog_set_val("tang_url", "http://127.0.0.1/") - self.dialog_apply() - b.wait_in_text("#dialog", "Make sure the key hash from the Tang server matches") - b.wait_in_text("#dialog", m.execute("tang-show-keys").strip()) - self.dialog_apply() - self.dialog_wait_close() - b.wait_in_text(panel + "ul li:nth-child(2)", "http://127.0.0.1/") - - # Remove key on client - # - b.click(panel + "ul li:nth-child(2) button[aria-label=Remove]") - self.confirm() - b.wait_not_present(panel + "ul li:nth-child(2)") - def testLuks1Slots(self): self.allow_journal_messages("Device is not initialized.*", ".*could not be opened.") m = self.machine @@ -542,5 +470,238 @@ class TestStorageLuks(StorageCase): self.wait_mounted(1, 1) +class TestStorageNBDE(StorageCase, PackageCase): + provision = { + "0": {"address": "10.111.112.1/20", "memory_mb": 2048}, + "tang": {"address": "10.111.112.5/20"} + } + + def testBasic(self): + m = self.machine + b = self.browser + + # Only Arch gets it right... + need_fixing = (m.image != "arch") + + mount_point_secret = "/run/secret" + + tang_m = self.machines["tang"] + tang_m.execute("systemctl start tangd.socket") + tang_m.execute("firewall-cmd --add-port 80/tcp") + + if need_fixing: + self.addPackageSet("clevis") + self.enableRepo() + + self.login_and_go("/storage") + + # Add a disk and format it with luks + m.add_disk("50M", serial="MYDISK") + b.wait_in_text("#drives", "MYDISK") + b.click('.sidepanel-row:contains("MYDISK")') + b.wait_visible("#storage-detail") + + self.content_row_action(1, "Format") + self.dialog({"type": "ext4", + "crypto": self.default_crypto_type, + "name": "ENCRYPTED", + "mount_point": mount_point_secret, + "mount_options.auto": False, + "passphrase": "vainu-reku-toma-rolle-kaja", + "passphrase2": "vainu-reku-toma-rolle-kaja"}) + self.content_row_wait_in_col(1, 2, "Filesystem (encrypted)") + self.content_tab_wait_in_info(1, 1, "Mount point", "The filesystem is not mounted") + + self.content_tab_wait_in_info(1, 2, "Options", "none") + tab = self.content_tab_expand(1, 2) + panel = tab + " .pf-c-card:contains(Keys) " + b.wait_visible(panel) + b.wait_in_text(panel + "ul li:nth-child(1)", "Passphrase") + + # Add a key + # + b.click(panel + "[aria-label=Add]") + self.dialog_wait_open() + self.dialog_wait_apply_enabled() + self.dialog_set_val("type", "tang") + self.dialog_set_val("tang_url", "10.111.112.5") + self.dialog_set_val("passphrase", "wrong-passphrase") + self.dialog_apply() + b.wait_in_text("#dialog", "Make sure the key hash from the Tang server matches") + b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip()) + self.dialog_apply() + if need_fixing: + b.wait_in_text("#dialog", "Add Network Bound Disk Encryption") + with b.wait_timeout(60): + self.dialog_apply() + b.wait_in_text("#dialog", "No key available with this passphrase.") + self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja") + self.dialog_apply() + self.dialog_wait_close() + b.wait_visible(panel + "ul li:nth-child(2)") + b.wait_in_text(panel + "ul li:nth-child(2)", "10.111.112.5") + + # Adding the key should add "_netdev" options + # + self.content_tab_wait_in_info(1, 1, "Mount point", "_netdev") + self.content_tab_wait_in_info(1, 2, "Options", "_netdev") + + # Mount it. This should succeed without passphrase. + # + self.content_row_action(1, "Mount") + self.dialog_wait_open() + self.dialog_wait_val("mount_point", mount_point_secret) + self.dialog_apply() + self.dialog_wait_close() + self.content_row_wait_in_col(1, 2, "ext4 filesystem") + + # Edit the key, without providing an existing passphrase + # + b.click(panel + "ul li:nth-child(2) [aria-label=Edit]") + self.dialog_wait_open() + self.dialog_wait_apply_enabled() + self.dialog_wait_val("tang_url", "10.111.112.5") + self.dialog_set_val("tang_url", "http://10.111.112.5/") + self.dialog_apply() + b.wait_in_text("#dialog", "Make sure the key hash from the Tang server matches") + b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip()) + self.dialog_apply() + self.dialog_wait_close() + b.wait_in_text(panel + "ul li:nth-child(2)", "http://10.111.112.5/") + + # Reset the options so that we can check that they get added + # also when the filesystem is mounted. + # + self.content_tab_info_action(1, 1, "Mount point") + self.dialog({"mount_options.extra": False}) + self.content_tab_info_action(1, 2, "Options") + self.dialog({"options": ""}, expect={"options": "_netdev"}) + self.content_tab_wait_in_info(1, 2, "Options", "none") + + # Add a second key, this should not show the fixing dialog. + # + b.click(panel + "[aria-label=Add]") + self.dialog_wait_open() + self.dialog_wait_apply_enabled() + self.dialog_set_val("type", "tang") + self.dialog_set_val("tang_url", "http://10.111.112.5") + self.dialog_apply() + b.wait_in_text("#dialog", "Make sure the key hash from the Tang server matches") + b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip()) + self.dialog_apply() + self.dialog_wait_close() + self.dialog_wait_close() + b.wait_visible(panel + "ul li:nth-child(3)") + b.wait_in_text(panel + "ul li:nth-child(3)", "http://10.111.112.5") + + # This should bring the options back. + # + self.content_tab_wait_in_info(1, 1, "Mount point", "_netdev") + self.content_tab_wait_in_info(1, 2, "Options", "_netdev") + + # Reboot + # + m.reboot() + m.start_cockpit() + b.relogin() + b.enter_page("/storage") + b.wait_visible("#storage-detail") + + self.wait_mounted(1, 1) + + # Remove one key on client + # + tab = self.content_tab_expand(1, 2) + panel = tab + " .pf-c-card:contains(Keys) " + b.click(panel + 'ul li:contains("Slot 1") button[aria-label=Remove]') + self.confirm() + b.wait_not_present(panel + 'ul li:contains("Slot 1")') + + @skipImage("TODO: don't know how to encrypt the rootfs", + "debian-stable", "debian-testing", "ubuntu-stable", "ubuntu-2204", "arch") + @timeout(1200) + def testRootReboot(self): + m = self.machine + b = self.browser + + tang_m = self.machines["tang"] + tang_m.execute("systemctl start tangd.socket") + tang_m.execute("firewall-cmd --add-port 80/tcp") + + try: + self.encrypt_root("einszweidrei") + except Exception: + console_screenshot(m, "failed-encrypt.ppm") + raise + + self.assertIn("crypt", m.execute("lsblk -snlo TYPE $(findmnt -no SOURCE /)")) + + self.addPackageSet("clevis") + self.enableRepo() + + self.login_and_go("/storage") + + # Add a clevis key to the root filesystem and then reboot. + # + # We also remove the original passphrase in order to be sure + # that it was in fact clevis that has unlocked the rootfs, and + # not the magic provided by "encrypt_root" + + b.wait_visible("#mounts") + for i in range(1, 100): + if b.text(f"#mounts tbody tr:nth-child({i}) td[data-label=Mount]") == "/": + b.click(f"#mounts tbody tr:nth-child({i})") + break + + b.wait_visible("#detail-content") + for i in range(1, 100): + col = self.content_row_tbody(i) + ' tr:first-child td[data-label="Used for"]' + if b.text(col) == "/": + root_row = i + break + + tab = self.content_tab_expand(root_row, 3) + panel = tab + " .pf-c-card:contains(Keys) " + b.wait_visible(panel) + b.wait_in_text(panel + "ul li:nth-child(1)", "Passphrase") + + with b.wait_timeout(360): + b.click(panel + "[aria-label=Add]") + self.dialog_wait_open() + self.dialog_wait_apply_enabled() + self.dialog_set_val("type", "tang") + self.dialog_set_val("tang_url", "10.111.112.5") + self.dialog_set_val("passphrase", "einszweidrei") + self.dialog_apply() + b.wait_in_text("#dialog", "Make sure the key hash from the Tang server matches") + b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip()) + self.dialog_apply() + b.wait_in_text("#dialog", "Add Network Bound Disk Encryption") + self.dialog_apply() + self.dialog_wait_close() + + b.click(panel + "ul li:nth-child(1) button[aria-label=Remove]") + b.click("#force-remove-passphrase") + b.click("button:contains('Remove')") + b.wait_in_text(panel + "ul li:nth-child(1)", "Keyserver") + + # Tell the initrd to configure our special inter-machine + # network that has the "tang" machine. + # + m.execute("grubby --update-kernel=ALL --args='ip=10.111.112.1::10.111.112.1:255.255.255.0::eth1:off'") + + try: + m.reboot(timeout_sec=300) + except Exception: + console_screenshot(m, "failed-reboot.ppm") + raise + + m.start_cockpit() + b.relogin() + b.enter_page("/storage") + + self.assertIn("crypt", m.execute("lsblk -snlo TYPE $(findmnt -no SOURCE /)")) + + if __name__ == '__main__': test_main()