diff --git a/pkg/storaged/crypto-keyslots.jsx b/pkg/storaged/crypto-keyslots.jsx index e6a619f70061..e1af2e0de0cb 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 => { @@ -60,7 +60,7 @@ function get_tang_adv(url) { }); } -function tang_adv_payload(adv) { +export function tang_adv_payload(adv) { return JSON.parse(cockpit.utf8_decoder().decode(cockpit.base64_decode(adv.payload))); } @@ -92,7 +92,7 @@ function compute_thp(jwk) { }; } -function compute_sigkey_thps(adv) { +export function compute_sigkey_thps(adv) { function is_signing_key(jwk) { if (!jwk.use && !jwk.key_ops) return true; @@ -474,7 +474,7 @@ function ensure_nbde_support_dialog(steps, client, block, url, adv, old_key, exi }); } -function parse_url(url) { +export function parse_url(url) { // clevis-encrypt-tang defaults to "http://" (via curl), so we do the same here. if (!/^[a-zA-Z]+:\/\//.test(url)) url = "http://" + url; @@ -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)) diff --git a/pkg/storaged/stratis-details.jsx b/pkg/storaged/stratis-details.jsx index c2c1fd42fcdb..8414f147ca61 100644 --- a/pkg/storaged/stratis-details.jsx +++ b/pkg/storaged/stratis-details.jsx @@ -21,6 +21,7 @@ import cockpit from "cockpit"; import React from "react"; import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js'; +import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js"; import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js"; import { PlusIcon, ExclamationTriangleIcon } from "@patternfly/react-icons"; @@ -36,6 +37,7 @@ import { TextInput, PassInput, SelectOne, SelectSpaces, CheckBoxes, BlockingMessage, TeardownMessage, + Skip, init_active_usage_processes } from "./dialog.jsx"; @@ -48,6 +50,7 @@ import { } from "./utils.js"; import { fmt_to_fragments } from "utils.jsx"; import { mount_explanation } from "./format-dialog.jsx"; +import { validate_url, get_tang_adv, parse_url, tang_adv_payload, compute_sigkey_thps } from "./crypto-keyslots.jsx"; const _ = cockpit.gettext; @@ -263,6 +266,127 @@ export const StratisPoolDetails = ({ client, pool }) => { }); } + function add_tang() { + const key_desc = pool.KeyDescription[1][1]; + return client.stratis_list_keys() + .then(keys => { + if (keys.indexOf(key_desc) >= 0) + add_tang_with_keydesc(false); + else + add_tang_with_keydesc(key_desc); + }) + .catch(ex => { + console.warn("Failed fetch properties", ex.toString()); + }); + } + + function add_tang_with_keydesc(key_desc) { + dialog_open({ + Title: _("Add Tang keyserver"), + Fields: [ + TextInput("tang_url", _("Keyserver address"), + { + validate: validate_url + }), + Skip("medskip", + { + visible: () => !!key_desc + }), + PassInput("passphrase", _("Disk passphrase"), + { + visible: () => !!key_desc, + validate: val => !val.length && _("Passphrase cannot be empty"), + explanation: _("Adding a keyserver requires unlocking the pool. Please provide the existing pool passphrase.") + }) + ], + Action: { + Title: _("Add"), + action: function (vals, progress) { + return get_tang_adv(vals.tang_url) + .then(adv => confirm_tang_trust(vals.tang_url, adv, key_desc, vals.passphrase)); + } + } + }); + } + + function confirm_tang_trust(url, adv, key_desc, passphrase) { + 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)); + + dialog_open({ + Title: _("Verify key"), + Body: ( + <> +

{_("Make sure the key hash from the Tang server matches one of the following:")}

+ +

{_("SHA256")}

+ { sigkey_thps.map(s =>

{s.sha256}

) } + +

{_("SHA1")}

+ { sigkey_thps.map(s =>

{s.sha1}

) } + +

+ {_("Manually check with SSH: ")} + + {cmd} + +

+ + ), + Action: { + Title: _("Trust key"), + action: function (vals, progress) { + function bind() { + return pool.BindClevis("tang", JSON.stringify({ url, adv })) + .then((result, code, message) => { + if (code) + return Promise.reject(message); + }); + } + + if (key_desc) { + return client.stratis_store_passphrase(key_desc, passphrase) + .then(bind) + .catch(ex => { + return remove_passphrase(client, key_desc) + .then(() => Promise.reject(ex)); + }) + .then(() => { + return remove_passphrase(client, key_desc); + }); + } 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((result, code, message) => { + if (code) + return Promise.reject(message); + }); + } + } + }); + } + function rename() { dialog_open({ Title: _("Rename Stratis pool"), @@ -432,6 +556,11 @@ export const StratisPoolDetails = ({ client, pool }) => { const use = pool.TotalPhysicalUsed[0] && [Number(pool.TotalPhysicalUsed[1]), Number(pool.TotalPhysicalSize)]; + const can_tang = (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 header = ( { } + { can_tang && + + + {_("storage", "Keyserver")} + + + { tang_url == null ? "-" : tang_url } + + { tang_url == null + ? {_("Add")} + : null + } + { tang_url != null + ? {_("Remove")} + : null + } + + + + } @@ -718,20 +867,6 @@ export function start_pool(client, uuid, show_devs) { 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]; - } - function start(unlock_method) { return client.stratis_start_pool(uuid, unlock_method) .then((result, code, message) => { @@ -769,9 +904,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 => { @@ -781,6 +914,32 @@ export function start_pool(client, uuid, show_devs) { unlock_with_keydesc(key_desc); })); } + + 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]; + } + + if (!key_desc) { + // Not an encrypted pool, just start it + return start(); + } else { + if (stopped_props.clevis_info + && stopped_props.clevis_info.t == "(bv)" + && stopped_props.clevis_info.v[0]) { + return start("clevis").catch(unlock_with_keyring); + } else + return unlock_with_keyring(); + } } const StratisStoppedPoolSidebar = ({ client, uuid }) => { @@ -804,13 +963,23 @@ const StratisStoppedPoolSidebar = ({ client, uuid }) => { }; export const StratisStoppedPoolDetails = ({ client, uuid }) => { + const stopped_props = client.stratis_manager.StoppedPools[uuid]; + const clevis_info = stopped_props.clevis_info; + + const can_tang = clevis_info && clevis_info.v[0] && (!clevis_info.v[1].v[0] || clevis_info.v[1].v[1][0] == "tang"); + const tang_url = can_tang && clevis_info.v[1].v[0] ? JSON.parse(clevis_info.v[1].v[1][1]).url : null; + function start() { return start_pool(client, uuid); } const header = ( - {_("Start")} }}> + + {_("Start")} + }}> {_("Stopped Stratis pool")} @@ -819,6 +988,16 @@ export const StratisStoppedPoolDetails = ({ client, uuid }) => { {_("storage", "UUID")} { uuid } + { can_tang && + + + {_("storage", "Keyserver")} + + + { tang_url || "-" } + + + }