From 4133eb93dc20f00db996d1fefdd5fcbfd1d6b320 Mon Sep 17 00:00:00 2001 From: Marius Vollmer Date: Tue, 8 Aug 2023 10:28:03 +0300 Subject: [PATCH] storage: Tang keyserver and passphrase management for Stratis --- pkg/storaged/client.js | 23 ++- pkg/storaged/crypto-keyslots.jsx | 60 +++---- pkg/storaged/stratis-details.jsx | 263 +++++++++++++++++++++++++++--- pkg/storaged/stratis-panel.jsx | 74 ++++++--- pkg/storaged/stratis-utils.js | 19 +++ test/reference | 2 +- test/verify/check-storage-stratis | 199 +++++++++++++++++++++- 7 files changed, 554 insertions(+), 86 deletions(-) diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js index 9874d3b575e1..3122a905a92b 100644 --- a/pkg/storaged/client.js +++ b/pkg/storaged/client.js @@ -379,6 +379,8 @@ function update_indices() { } client.blocks_stratis_stopped_pool = { }; + client.stratis_stopped_pool_key_description = { }; + client.stratis_stopped_pool_clevis_info = { }; for (const uuid in client.stratis_manager.StoppedPools) { const devs = client.stratis_manager.StoppedPools[uuid].devs.v; for (const d of devs) { @@ -386,6 +388,22 @@ function update_indices() { if (block) client.blocks_stratis_stopped_pool[block.path] = uuid; } + const kinfo = client.stratis_manager.StoppedPools[uuid].key_description; + if (kinfo && + kinfo.t == "(bv)" && + kinfo.v[0] && + kinfo.v[1].t == "(bs)" && + kinfo.v[1].v[0]) { + client.stratis_stopped_pool_key_description[uuid] = kinfo.v[1].v[1]; + } + const cinfo = client.stratis_manager.StoppedPools[uuid].clevis_info; + if (cinfo && + cinfo.t == "(bv)" && + cinfo.v[0] && + cinfo.v[1].t == "(b(ss))" && + cinfo.v[1].v[0]) { + client.stratis_stopped_pool_clevis_info[uuid] = cinfo.v[1].v[1]; + } } client.blocks_cleartext = { }; @@ -970,11 +988,11 @@ function stratis3_start() { return client.stratis_manager.StartPool(uuid, [!!unlock_method, unlock_method || ""]); }; - client.stratis_create_pool = (name, devs, key_desc) => { + client.stratis_create_pool = (name, devs, key_desc, clevis_info) => { return client.stratis_manager.CreatePool(name, [false, 0], devs, key_desc ? [true, key_desc] : [false, ""], - [false, ["", ""]]); + clevis_info ? [true, clevis_info] : [false, ["", ""]]); }; client.stratis_list_keys = () => { @@ -986,6 +1004,7 @@ function stratis3_start() { }; client.features.stratis = true; + client.features.stratis_crypto_binding = true; client.stratis_pools = client.stratis_manager.client.proxies("org.storage.stratis3.pool." + stratis3_interface_revision, "/org/storage/stratis3", diff --git a/pkg/storaged/crypto-keyslots.jsx b/pkg/storaged/crypto-keyslots.jsx index fbf6afb7f6ee..f8e86f5f7d62 100644 --- a/pkg/storaged/crypto-keyslots.jsx +++ b/pkg/storaged/crypto-keyslots.jsx @@ -52,7 +52,7 @@ const _ = cockpit.gettext; /* Tang advertisement utilities */ -function get_tang_adv(url) { +export function get_tang_adv(url) { return cockpit.spawn(["curl", "-sSf", url + "/adv"], { err: "message" }) .then(JSON.parse) .catch(error => { @@ -487,7 +487,7 @@ function parse_url(url) { } } -function validate_url(url) { +export function validate_url(url) { if (url.length === 0) return _("Address cannot be empty"); if (!parse_url(url)) @@ -614,39 +614,41 @@ function add_or_update_tang(dlg, vals, block, url, adv, old_key, passphrase) { .catch(request_passphrase_on_error_handler(dlg, vals, passphrase, block)); } -function edit_tang_adv(client, block, key, url, adv, passphrase) { +export const TangKeyVerification = ({ url, adv }) => { const parsed = parse_url(url); const cmd = cockpit.format("ssh $0 tang-show-keys $1", parsed.hostname, parsed.port); - const sigkey_thps = compute_sigkey_thps(tang_adv_payload(adv)); + return ( + + {_("Check the key hash with the Tang server.")} + + {_("How to check")} + + {_("In a terminal, run: ")} + + {cmd} + + + + {_("Check that the SHA-256 or SHA-1 hash from the command matches this dialog.")} + + + {_("SHA-256")} + { sigkey_thps.map(s => {s.sha256}) } + + {_("SHA-1")} + { sigkey_thps.map(s => {s.sha1}) } + ); +}; + +function edit_tang_adv(client, block, key, url, adv, passphrase) { const dlg = dialog_open({ Title: _("Verify key"), - Body: ( - - {_("Check the key hash with the Tang server.")} - - {_("How to check")} - - {_("In a terminal, run: ")} - - {cmd} - - - - {_("Check that the SHA-256 or SHA-1 hash from the command matches this dialog.")} - - - {_("SHA-256")} - { sigkey_thps.map(s => {s.sha256}) } - - {_("SHA-1")} - { sigkey_thps.map(s => {s.sha1}) } - - ), + Body: , Fields: existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase.")), Action: { Title: _("Trust key"), diff --git a/pkg/storaged/stratis-details.jsx b/pkg/storaged/stratis-details.jsx index 7ff56069137f..834a1620471a 100644 --- a/pkg/storaged/stratis-details.jsx +++ b/pkg/storaged/stratis-details.jsx @@ -22,6 +22,7 @@ import React from "react"; import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js'; import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; +import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js"; import { PlusIcon, ExclamationTriangleIcon } from "@patternfly/react-icons"; @@ -29,7 +30,7 @@ import { FilesystemTab, mounting_dialog, is_mounted, is_valid_mount_point, get_f import { ListingTable } from "cockpit-components-table.jsx"; import { ListingPanel } from 'cockpit-components-listing-panel.jsx'; import { StdDetailsLayout } from "./details.jsx"; -import { StorageButton, StorageBarMenu, StorageMenuItem, StorageUsageBar } from "./storage-controls.jsx"; +import { StorageButton, StorageLink, StorageBarMenu, StorageMenuItem, StorageUsageBar } from "./storage-controls.jsx"; import { SidePanel } from "./side-panel.jsx"; import { dialog_open, @@ -48,8 +49,9 @@ import { } from "./utils.js"; import { fmt_to_fragments } from "utils.jsx"; import { mount_explanation } from "./format-dialog.jsx"; +import { validate_url, get_tang_adv } from "./crypto-keyslots.jsx"; -import { std_reply, with_keydesc, with_stored_passphrase } from "./stratis-utils.js"; +import { std_reply, with_keydesc, with_stored_passphrase, confirm_tang_trust, get_unused_keydesc } from "./stratis-utils.js"; const _ = cockpit.gettext; @@ -171,6 +173,14 @@ export function validate_pool_name(client, pool, name) { export const StratisPoolDetails = ({ client, pool }) => { const filesystems = client.stratis_pool_filesystems[pool.path]; + const key_desc = (pool.Encrypted && + pool.KeyDescription[0] && + pool.KeyDescription[1][1]); + const can_tang = (client.features.stratis_crypto_binding && + pool.Encrypted && + pool.ClevisInfo[0] && // pool has consistent clevis config + (!pool.ClevisInfo[1][0] || pool.ClevisInfo[1][1][0] == "tang")); // not bound or bound to "tang" + const tang_url = can_tang && pool.ClevisInfo[1][0] ? JSON.parse(pool.ClevisInfo[1][1][1]).url : null; const forced_options = ["x-systemd.requires=stratis-fstab-setup@" + pool.Uuid + ".service"]; @@ -207,6 +217,140 @@ export const StratisPoolDetails = ({ client, pool }) => { }); } + function add_passphrase() { + dialog_open({ + Title: _("Add passphrase"), + Fields: [ + PassInput("passphrase", _("Passphrase"), + { validate: val => !val.length && _("Passphrase cannot be empty") }), + PassInput("passphrase2", _("Confirm"), + { validate: (val, vals) => vals.passphrase.length && vals.passphrase != val && _("Passphrases do not match") }) + ], + Action: { + Title: _("Save"), + action: vals => { + return get_unused_keydesc(client, pool.Name) + .then(keydesc => { + return with_stored_passphrase(client, keydesc, vals.passphrase, + () => pool.BindKeyring(keydesc)) + .then(std_reply); + }); + } + } + }); + } + + function change_passphrase() { + with_keydesc(client, pool, (keydesc, keydesc_set) => { + dialog_open({ + Title: _("Change passphrase"), + Fields: [ + PassInput("old_passphrase", _("Old passphrase"), + { + visible: vals => !keydesc_set, + validate: val => !val.length && _("Passphrase cannot be empty") + }), + PassInput("new_passphrase", _("New passphrase"), + { validate: val => !val.length && _("Passphrase cannot be empty") }), + PassInput("new_passphrase2", _("Confirm"), + { validate: (val, vals) => vals.new_passphrase.length && vals.new_passphrase != val && _("Passphrases do not match") }) + ], + Action: { + Title: _("Save"), + action: vals => { + function rebind() { + return get_unused_keydesc(client, pool.Name) + .then(new_keydesc => { + return with_stored_passphrase(client, new_keydesc, vals.new_passphrase, + () => pool.RebindKeyring(new_keydesc)) + .then(std_reply); + }); + } + + if (vals.old_passphrase) { + return with_stored_passphrase(client, keydesc, vals.old_passphrase, rebind); + } else { + return rebind(); + } + } + } + }); + }); + } + + function remove_passphrase() { + dialog_open({ + Title: _("Remove passphrase?"), + Body:
+

{ fmt_to_fragments(_("Passphrase removal may prevent unlocking $0."), {pool.Name}) }

+
, + Action: { + DangerButton: true, + Title: _("Remove"), + action: function (vals) { + return pool.UnbindKeyring().then(std_reply); + } + } + }); + } + + function add_tang() { + return with_keydesc(client, pool, (keydesc, keydesc_set) => { + dialog_open({ + Title: _("Add Tang keyserver"), + Fields: [ + TextInput("tang_url", _("Keyserver address"), + { + validate: validate_url + }), + PassInput("passphrase", _("Pool passphrase"), + { + visible: () => !keydesc_set, + validate: val => !val.length && _("Passphrase cannot be empty"), + explanation: _("Adding a keyserver requires unlocking the pool. Please provide the existing pool passphrase.") + }) + ], + Action: { + Title: _("Save"), + action: function (vals, progress) { + return get_tang_adv(vals.tang_url) + .then(adv => { + function bind() { + return pool.BindClevis("tang", JSON.stringify({ url: vals.tang_url, adv })) + .then(std_reply); + } + confirm_tang_trust(vals.tang_url, adv, + () => { + if (vals.passphrase) + return with_stored_passphrase(client, keydesc, + vals.passphrase, bind); + else + return bind(); + }); + }); + } + } + }); + }); + } + + function remove_tang() { + dialog_open({ + Title: _("Remove Tang keyserver?"), + Body:
+

{ fmt_to_fragments(_("Remove $0?"), {tang_url}) }

+

{ fmt_to_fragments(_("Keyserver removal may prevent unlocking $0."), {pool.Name}) }

+
, + Action: { + DangerButton: true, + Title: _("Remove"), + action: function (vals) { + return pool.UnbindClevis().then(std_reply); + } + } + }); + } + function rename() { dialog_open({ Title: _("Rename Stratis pool"), @@ -398,6 +542,52 @@ export const StratisPoolDetails = ({ client, pool }) => { } + { pool.Encrypted && client.features.stratis_crypto_binding && + + + {_("storage", "Passphrase")} + + + + { !key_desc + ? {_("Add passphrase")} + : <> + {_("Change")} + + + {_("Remove")} + + + + } + + + + } + { can_tang && + + + {_("storage", "Keyserver")} + + + + { tang_url == null + ? {_("Add keyserver")} + : <> + { tang_url } + + + {_("Remove")} + + + + } + + + + } @@ -646,24 +836,9 @@ export const StratisPoolDetails = ({ client, pool }) => { }; export function start_pool(client, uuid, show_devs) { - const manager = client.stratis_manager; - const stopped_props = manager.StoppedPools[uuid]; - const devs = stopped_props.devs.v.map(d => d.devnode).sort(); - let key_desc = null; - - if (stopped_props.key_description && - stopped_props.key_description.t == "(bv)" && - stopped_props.key_description.v[0]) { - if (stopped_props.key_description.v[1].t != "(bs)" || - !stopped_props.key_description.v[1].v[0]) { - dialog_open({ - Title: _("Error"), - Body: _("This pool can not be unlocked here because its key description is not in the expected format.") - }); - return; - } - key_desc = stopped_props.key_description.v[1].v[1]; - } + const devs = client.stratis_manager.StoppedPools[uuid].devs.v.map(d => d.devnode).sort(); + const key_desc = client.stratis_stopped_pool_key_description[uuid]; + const clevis_info = client.stratis_stopped_pool_clevis_info[uuid]; function start(unlock_method) { return client.stratis_start_pool(uuid, unlock_method).then(std_reply); @@ -691,9 +866,7 @@ export function start_pool(client, uuid, show_devs) { }); } - if (!key_desc) { - return start(); - } else { + function unlock_with_keyring() { return (client.stratis_list_keys() .catch(() => [{ }]) .then(keys => { @@ -703,11 +876,21 @@ export function start_pool(client, uuid, show_devs) { unlock_with_keydesc(key_desc); })); } + + if (!key_desc && !clevis_info) { + // Not an encrypted pool, just start it + return start(); + } else if (key_desc && clevis_info) { + return start("clevis").catch(unlock_with_keyring); + } else if (!key_desc && clevis_info) { + return start("clevis"); + } else if (key_desc && !clevis_info) { + return unlock_with_keyring(); + } } const StratisStoppedPoolSidebar = ({ client, uuid }) => { - const stopped_props = client.stratis_manager.StoppedPools[uuid]; - const devs = stopped_props.devs.v.map(d => d.devnode).sort(); + const devs = client.stratis_manager.StoppedPools[uuid].devs.v.map(d => d.devnode).sort(); function render_dev(dev) { const block = client.slashdevs_block[dev]; @@ -726,13 +909,21 @@ const StratisStoppedPoolSidebar = ({ client, uuid }) => { }; export const StratisStoppedPoolDetails = ({ client, uuid }) => { + const key_desc = client.stratis_stopped_pool_key_description[uuid]; + const clevis_info = client.stratis_stopped_pool_clevis_info[uuid]; + + const encrypted = key_desc || clevis_info; + const can_tang = encrypted && (!clevis_info || clevis_info[0] == "tang"); + const tang_url = (can_tang && clevis_info) ? JSON.parse(clevis_info[1]).url : null; + function start() { return start_pool(client, uuid); } + const actions = {_("Start")}; const header = ( - {_("Start")} }}> + {_("Stopped Stratis pool")} @@ -741,6 +932,26 @@ export const StratisStoppedPoolDetails = ({ client, uuid }) => { {_("storage", "UUID")} { uuid } + { encrypted && client.features.stratis_crypto_binding && + + + {_("storage", "Passphrase")} + + + { key_desc ? cockpit.format(_("using key description $0"), key_desc) : _("none") } + + + } + { can_tang && client.features.stratis_crypto_binding && + + + {_("storage", "Keyserver")} + + + { tang_url || _("none") } + + + } diff --git a/pkg/storaged/stratis-panel.jsx b/pkg/storaged/stratis-panel.jsx index 18b4b2b46a5a..9ffcde691787 100644 --- a/pkg/storaged/stratis-panel.jsx +++ b/pkg/storaged/stratis-panel.jsx @@ -29,7 +29,8 @@ import { validate_pool_name, start_pool } from "./stratis-details.jsx"; import { StorageButton } from "./storage-controls.jsx"; import { PlayIcon } from "@patternfly/react-icons"; -import { std_reply, get_unused_keydesc, with_stored_passphrase } from "./stratis-utils.js"; +import { std_reply, get_unused_keydesc, with_stored_passphrase, confirm_tang_trust } from "./stratis-utils.js"; +import { validate_url, get_tang_adv } from "./crypto-keyslots.jsx"; const _ = cockpit.gettext; @@ -119,10 +120,24 @@ export function create_stratis_pool(client) { value: name, validate: name => validate_pool_name(client, null, name) }), - CheckBoxes("encrypt", "", + SelectSpaces("disks", _("Block devices"), + { + empty_warning: _("No block devices are available."), + validate: function (disks) { + if (disks.length === 0) + return _("At least one block device is needed."); + }, + spaces: get_available_spaces(client) + }), + CheckBoxes("encrypt_pass", client.features.stratis_crypto_binding ? _("Encryption") : "", { fields: [ - { tag: "on", title: _("Encrypt data") } + { + tag: "on", + title: (client.features.stratis_crypto_binding + ? _("Use a passphrase") + : _("Encrypt data")) + } ], nested_fields: [ PassInput("passphrase", _("Passphrase"), @@ -131,7 +146,7 @@ export function create_stratis_pool(client) { if (phrase === "") return _("Passphrase cannot be empty"); }, - visible: vals => vals.encrypt.on, + visible: vals => vals.encrypt_pass.on, new_password: true }), PassInput("passphrase2", _("Confirm"), @@ -140,20 +155,25 @@ export function create_stratis_pool(client) { if (phrase2 != vals.passphrase) return _("Passphrases do not match"); }, - visible: vals => vals.encrypt.on, + visible: vals => vals.encrypt_pass.on, new_password: true }) ] }), - SelectSpaces("disks", _("Block devices"), - { - empty_warning: _("No block devices are available."), - validate: function (disks) { - if (disks.length === 0) - return _("At least one block device is needed."); - }, - spaces: get_available_spaces(client) - }) + CheckBoxes("encrypt_tang", "", + { + visible: () => client.features.stratis_crypto_binding, + fields: [ + { tag: "on", title: _("Use a Tang keyserver") } + ], + nested_fields: [ + TextInput("tang_url", _("Keyserver address"), + { + validate: validate_url, + visible: vals => vals.encrypt_tang && vals.encrypt_tang.on + }), + ] + }) ], Action: { Title: _("Create"), @@ -161,16 +181,28 @@ export function create_stratis_pool(client) { return prepare_available_spaces(client, vals.disks).then(function (paths) { const devs = paths.map(p => decode_filename(client.blocks[p].PreferredDevice)); - function create(key_desc) { - return client.stratis_create_pool(vals.name, devs, key_desc).then(std_reply); + function create(key_desc, adv) { + let clevis_info = null; + if (adv) + clevis_info = ["tang", JSON.stringify({ url: vals.tang_url, adv })]; + return client.stratis_create_pool(vals.name, devs, key_desc, clevis_info).then(std_reply); + } + + function create2(adv) { + if (vals.encrypt_pass.on) { + return get_unused_keydesc(client, vals.name) + .then(keydesc => with_stored_passphrase(client, keydesc, vals.passphrase, + () => create(keydesc, adv))); + } else { + return create(false, adv); + } } - if (vals.encrypt.on) { - return get_unused_keydesc(client, vals.name) - .then(keydesc => with_stored_passphrase(client, keydesc, vals.passphrase, - () => create(keydesc))); + if (vals.encrypt_tang && vals.encrypt_tang.on) { + return get_tang_adv(vals.tang_url) + .then(adv => confirm_tang_trust(vals.tang_url, adv, () => create2(adv))); } else { - return create(false); + return create2(false); } }); } diff --git a/pkg/storaged/stratis-utils.js b/pkg/storaged/stratis-utils.js index 380d7733f5fa..98590a47e5de 100644 --- a/pkg/storaged/stratis-utils.js +++ b/pkg/storaged/stratis-utils.js @@ -17,6 +17,14 @@ * along with Cockpit; If not, see . */ +import cockpit from "cockpit"; +import React from "react"; + +import { dialog_open } from "./dialog.jsx"; +import { TangKeyVerification } from "./crypto-keyslots.jsx"; + +const _ = cockpit.gettext; + export function std_reply(result, code, message) { if (code) return Promise.reject(message); @@ -60,3 +68,14 @@ export function get_unused_keydesc(client, desc_prefix) { return desc; }); } + +export function confirm_tang_trust(url, adv, action) { + dialog_open({ + Title: _("Verify key"), + Body: , + Action: { + Title: _("Trust key"), + action + } + }); +} diff --git a/test/reference b/test/reference index 1f6b16a7bcd1..c12af9a76bea 160000 --- a/test/reference +++ b/test/reference @@ -1 +1 @@ -Subproject commit 1f6b16a7bcd1817c84c85a96f7c9ec906f52d678 +Subproject commit c12af9a76bea24c6602e35f7ee77f74e3264cfd8 diff --git a/test/verify/check-storage-stratis b/test/verify/check-storage-stratis index abd5961a5f98..90c45f6c7539 100755 --- a/test/verify/check-storage-stratis +++ b/test/verify/check-storage-stratis @@ -32,6 +32,8 @@ class TestStorageStratis(storagelib.StorageCase): self.machine.execute("systemctl enable --now stratisd") self.addCleanup(self.machine.execute, "systemctl disable --now stratisd") + self.stratis_v2 = self.image.startswith("rhel-8-9") or self.image == "centos-8-stream" + def testBasic(self): m = self.machine b = self.browser @@ -81,7 +83,7 @@ class TestStorageStratis(storagelib.StorageCase): self.dialog_cancel() self.dialog_wait_close() - if not m.image.startswith("rhel-8-9") and m.image != "centos-8-stream": + if not self.stratis_v2: # Stop the pool (only works with Stratis 3) pool_uuid = m.execute("stratis --unhyphenated-uuids pool list --name pool0 | grep ^UUID | cut -d' ' -f2").strip() m.execute("stratis pool stop pool0") @@ -275,6 +277,8 @@ class TestStorageStratis(storagelib.StorageCase): m.add_disk("4G", serial="DISK2") b.wait_in_text("#drives", dev_2) + passphrase = "foodeeboodeebar" + # Create an encrypted pool with a filesystem, but don't mount # it. Cockpit will chose a key description for the pool and # we occupy its first choice in order to force Cockpit to use @@ -283,9 +287,9 @@ class TestStorageStratis(storagelib.StorageCase): self.dialog_open_with_retry(trigger=lambda: self.devices_dropdown("Create Stratis pool"), expect=lambda: (self.dialog_is_present('disks', dev_1) and self.dialog_check({"name": "pool0"}))) - self.dialog_set_val("encrypt.on", val=True) - self.dialog_set_val("passphrase", "foodeeboodeebar") - self.dialog_set_val("passphrase2", "foodeeboodeebar") + self.dialog_set_val("encrypt_pass.on", val=True) + self.dialog_set_val("passphrase", passphrase) + self.dialog_set_val("passphrase2", passphrase) self.dialog_set_val("disks", {dev_1: True}) b.assert_pixels("#dialog", "create-encrypted-pool") self.dialog_apply() @@ -296,6 +300,7 @@ class TestStorageStratis(storagelib.StorageCase): b.click('.sidepanel-row:contains("pool0")') b.wait_visible('#storage-detail') b.wait_in_text('#detail-header', "Encrypted Stratis pool pool0") + b.click("button:contains(Create new filesystem)") self.dialog({'name': 'fsys1', 'mount_point': '/run/fsys1', @@ -312,12 +317,26 @@ class TestStorageStratis(storagelib.StorageCase): self.dialog_set_val('disks', {dev_2: True}) self.dialog_apply() self.dialog_wait_error("passphrase", "Passphrase cannot be empty") - self.dialog_set_val('passphrase', "foodeeboodeebar") + self.dialog_set_val('passphrase', passphrase) self.dialog_apply() self.dialog_wait_close() b.wait_in_text('#detail-sidebar', dev_2) b.wait_in_text(f'#detail-sidebar .sidepanel-row:contains({dev_2})', "data") + # Change the passphrase (if supported) + if not self.stratis_v2: + b.wait_visible('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Remove)[aria-disabled=true]') + b.click('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Change)') + self.dialog({'old_passphrase': passphrase, + 'new_passphrase': "boodeefoodeebar", + 'new_passphrase2': "boodeefoodeebar"}) + # do it again, with the old passphrase in the keyring + m.execute("echo boodeefoodeebar | stratis key set pool0 --capture-key") + b.click('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Change)') + self.dialog({'new_passphrase': passphrase, + 'new_passphrase2': passphrase}) + m.execute("stratis key unset pool0") + m.reboot() m.start_cockpit() b.relogin() @@ -334,7 +353,7 @@ class TestStorageStratis(storagelib.StorageCase): self.dialog_set_val('passphrase', "wrong-passphrase") self.dialog_apply() b.wait_visible("#dialog .pf-v5-c-alert.pf-m-danger") - self.dialog_set_val('passphrase', "foodeeboodeebar") + self.dialog_set_val('passphrase', passphrase) self.dialog_apply() self.dialog_wait_close() b.wait_not_in_text('#detail-header', "Stopped") @@ -346,7 +365,7 @@ class TestStorageStratis(storagelib.StorageCase): self.wait_mounted(1, 1) # Reboot (this requires the passphrase) - self.setup_systemd_password_agent("foodeeboodeebar") + self.setup_systemd_password_agent(passphrase) m.reboot() m.start_cockpit() b.relogin() @@ -548,5 +567,171 @@ class TestStoragePackagesStratis(packagelib.PackageCase, storagelib.StorageCase) b.wait_not_present("#devices .pf-v5-c-dropdown a:contains('Create Stratis pool')") +@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*") +@testlib.skipImage("Stratis too old", "rhel-8-*", "centos-8-*") +class TestStorageStratisNBDE(packagelib.PackageCase, storagelib.StorageCase): + provision = { + "0": {"address": "10.111.112.1/20", "memory_mb": 2048}, + "tang": {"address": "10.111.112.5/20"} + } + + def setUp(self): + super().setUp() + + if self.image == "arch": + # Arch Linux does not enable systemd units by default + self.machine.execute("systemctl enable --now stratisd") + self.addCleanup(self.machine.execute, "systemctl disable --now stratisd") + + def testBasic(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") + + self.login_and_go("/storage") + + dev_1 = "/dev/sda" + m.add_disk("4G", serial="DISK1") + b.wait_in_text("#drives", dev_1) + + dev_2 = "/dev/sdb" + m.add_disk("5G", serial="DISK2") + b.wait_in_text("#drives", dev_2) + + # Create an encrypted pool with both a passphrase and a keyserver + self.dialog_open_with_retry(trigger=lambda: self.devices_dropdown("Create Stratis pool"), + expect=lambda: (self.dialog_is_present('disks', dev_1) and + self.dialog_check({"name": "pool0"}))) + self.dialog_set_val("encrypt_pass.on", val=True) + self.dialog_set_val("passphrase", "foodeeboodeebar") + self.dialog_set_val("passphrase2", "foodeeboodeebar") + self.dialog_set_val("encrypt_tang.on", val=True) + self.dialog_set_val("tang_url", "10.111.112.5") + self.dialog_set_val("disks", {dev_1: True}) + self.dialog_apply() + b.wait_in_text("#dialog", "Check the key hash") + b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip()) + self.dialog_apply() + with b.wait_timeout(60): + self.dialog_wait_close() + + b.wait_in_text("#devices", "pool0") + b.click('.sidepanel-row:contains("pool0")') + b.wait_visible('#storage-detail') + b.wait_in_text('#detail-header', "Encrypted Stratis pool pool0") + b.wait_in_text('#detail-header', "Passphrase") + b.wait_in_text('#detail-header', "Keyserver") + b.wait_in_text('#detail-header', "10.111.112.5") + + b.assert_pixels('#detail-header', "header", + ignore=['.pf-v5-c-description-list__group:contains(UUID)']) + + # Remove passphrase + b.click('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Remove)') + self.confirm() + b.wait_in_text('#detail-header .pf-v5-c-description-list__group:contains(Passphrase)', "Add passphrase") + b.wait_visible('#detail-header .pf-v5-c-description-list__group:contains(Keyserver) button:contains(Remove)[aria-disabled=true]') + + # Stop the pool and start it again. This should not ask + # for the passphrase (since there isn't any) + m.execute("stratis pool stop pool0") + b.wait_in_text('#detail-header', "Stopped Stratis pool") + tang_m.execute("systemctl stop tangd.socket") + b.click('#detail-header button:contains(Start)') + self.dialog_wait_open() + b.wait_in_text("#dialog", "Error communicating") + self.dialog_cancel() + self.dialog_wait_close() + + tang_m.execute("systemctl start tangd.socket") + b.click('#detail-header button:contains(Start)') + b.wait_not_in_text('#detail-header', "Stopped") + b.wait_in_text('#detail-header', "Encrypted Stratis pool pool0") + + # Put passphrase back and do the stopping starting again, + # but without tang. This should try clevis but then fall + # back to asking for a passphrase. + + b.click('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Add passphrase)') + self.dialog({'passphrase': "foodeeboodeebar", + 'passphrase2': "foodeeboodeebar"}) + b.wait_visible('#detail-header .pf-v5-c-description-list__group:contains(Keyserver) button:contains(Remove):not([aria-disabled=true])') + m.execute("stratis pool stop pool0") + tang_m.execute("systemctl stop tangd.socket") + b.click('#detail-header button:contains(Start)') + self.dialog_wait_open() + self.dialog_set_val("passphrase", "foobar") + self.dialog_cancel() + self.dialog_wait_close() + + # Finally start tang + tang_m.execute("systemctl start tangd.socket") + b.click('#detail-header button:contains(Start)') + b.wait_not_in_text('#detail-header', "Stopped") + b.wait_in_text('#detail-header', "Encrypted Stratis pool pool0") + + # Add a blockdevice. This requires the passphrase. + + b.click('#detail-sidebar .pf-v5-c-card__actions button') + self.dialog({'disks': {dev_2: True}, 'passphrase': "foodeeboodeebar"}) + + # Remove the keyserver and add it back + + b.click('#detail-header .pf-v5-c-description-list__group:contains(Keyserver) button:contains(Remove)') + self.confirm() + + b.click('#detail-header button:contains(Add keyserver)') + self.dialog_wait_open() + self.dialog_set_val("tang_url", "10.111.112.5") + self.dialog_set_val("passphrase", "foodeeboodeebar") + self.dialog_apply() + b.wait_in_text("#dialog", "Check the key hash") + b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip()) + self.dialog_apply() + with b.wait_timeout(60): + self.dialog_wait_close() + b.wait_in_text('#detail-header', "10.111.112.5") + + # Remove the keyserver and add it back a second time, but try + # first with the wrong passphrase already in the keyring + + b.click('#detail-header .pf-v5-c-description-list__group:contains(Keyserver) button:contains(Remove)') + self.confirm() + + m.execute("echo foobar | stratis key set pool0 --capture-key") + b.click('#detail-header button:contains(Add keyserver)') + self.dialog_wait_open() + self.dialog_set_val("tang_url", "10.111.112.5") + self.dialog_apply() + b.wait_in_text("#dialog", "Check the key hash") + b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip()) + self.dialog_apply() + with b.wait_timeout(60): + b.wait_in_text('#dialog', "Command failed") + m.execute("stratis key unset pool0") + m.execute("echo foodeeboodeebar | stratis key set pool0 --capture-key") + self.dialog_apply() + with b.wait_timeout(60): + self.dialog_wait_close() + b.wait_in_text('#detail-header', "10.111.112.5") + m.execute("stratis key unset pool0") + + # Create a mounted filesystem and reboot. + + b.click("button:contains(Create new filesystem)") + self.dialog({'name': 'fsys1', + 'mount_point': '/run/fsys1'}) + b.wait_in_text("#detail-content", "fsys1") + m.reboot() + m.start_cockpit() + b.relogin() + b.enter_page("/storage") + b.wait_visible("#storage-detail") + self.wait_mounted(1, 1) # should be mounted after boot + + if __name__ == '__main__': testlib.test_main()