diff --git a/pkg/systemd/overview-cards/cryptoPolicies.jsx b/pkg/systemd/overview-cards/cryptoPolicies.jsx index dc8b996ec785..1db1e19b16a7 100644 --- a/pkg/systemd/overview-cards/cryptoPolicies.jsx +++ b/pkg/systemd/overview-cards/cryptoPolicies.jsx @@ -18,7 +18,7 @@ */ import cockpit from "cockpit"; -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js"; @@ -29,6 +29,7 @@ import { ModalError } from 'cockpit-components-inline-notification.jsx'; import { PrivilegedButton } from "cockpit-components-privileged.jsx"; import { ProfilesMenuDialogBody } from "./profiles-menu-dialog-body.jsx"; import { useDialogs } from "dialogs.jsx"; +import { useInit } from "hooks"; import "./cryptoPolicies.scss"; @@ -37,63 +38,87 @@ const _ = cockpit.gettext; const displayProfileText = profile => profile === "DEFAULT" ? _("Default") : profile; const isInconsistentPolicy = (policy, fipsEnabled) => policy === "FIPS" !== fipsEnabled; +const getFipsConfigurable = () => cockpit.spawn(["/bin/sh", "-c", "command -v fips-mode-setup"], { error: "ignore" }) + .then(() => true) + .catch(() => false); + export const CryptoPolicyRow = () => { const Dialogs = useDialogs(); const [currentCryptoPolicy, setCurrentCryptoPolicy] = useState(null); const [fipsEnabled, setFipsEnabled] = useState(null); + const [fipsConfigurable, setFipsConfigurable] = useState(null); const [shaSubPolicyAvailable, setShaSubPolicyAvailable] = useState(null); - useEffect(() => { + useInit(() => { cockpit.file("/proc/sys/crypto/fips_enabled").read() .then(content => setFipsEnabled(content ? content.trim() === "1" : false)); - cockpit.file("/etc/crypto-policies/state/current") - .watch(content => setCurrentCryptoPolicy(content ? content.trim() : null)); + getFipsConfigurable().then(v => setFipsConfigurable(v)); + cockpit.file("/etc/crypto-policies/config") + .watch(async contents => { + // Ask crypto-policies to get correct FIPS state, as that dominates the configured policy + try { + setCurrentCryptoPolicy((await cockpit.spawn(["update-crypto-policies", "--show"])).trim()); + } catch (error) { + console.warn("Failed to get current crypto policy:", error.toString(), + "; falling back to /etc/crypto-policies/config"); + setCurrentCryptoPolicy(contents.trim()); + } + }); // RHEL-8-8 has no SHA1 subpolicy cockpit.file("/usr/share/crypto-policies/policies/modules/SHA1.pmod").read() .then(content => setShaSubPolicyAvailable(content ? content.trim() : false)); - }, []); + }); - if (!currentCryptoPolicy) { + if (currentCryptoPolicy === null || fipsEnabled === null || fipsConfigurable === null) return null; - } + + const policyRender = (currentCryptoPolicy.startsWith("FIPS") && !fipsConfigurable) + /* read-only mode; can't switch away from FIPS without fips-mode-setup */ + ? {displayProfileText(currentCryptoPolicy)} + : Dialogs.show()}> + {displayProfileText(currentCryptoPolicy)} + ; return ( {_("Cryptographic policy")} - - Dialogs.show()}> - {displayProfileText(currentCryptoPolicy)} - - + {policyRender} ); }; -const setPolicy = (policy, setError, setInProgress) => { +const setPolicy = async (policy, setError, setInProgress, fipsConfigurable) => { setInProgress(true); - let promise; - if (policy === "FIPS") { - promise = cockpit.spawn(["fips-mode-setup", "--enable"], { superuser: "require", err: "message" }); - } else { - promise = cockpit.spawn(["fips-mode-setup", "--disable"], { superuser: "require", err: "message" }).then(() => - cockpit.spawn(["update-crypto-policies", "--set", policy], { superuser: "require", err: "message" })); - } + try { + if (policy === "FIPS") { + cockpit.assert(fipsConfigurable, "calling setPolicy(FIPS) without fips-mode-setup"); + await cockpit.spawn(["fips-mode-setup", "--enable"], { superuser: "require", err: "message" }); + } else { + if (fipsConfigurable) + await cockpit.spawn(["fips-mode-setup", "--disable"], { superuser: "require", err: "message" }); + await cockpit.spawn(["update-crypto-policies", "--set", policy], { superuser: "require", err: "message" }); + } - promise.then(() => cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require", err: "message" })) - .catch(error => setError(error)) - .finally(() => setInProgress(false)); + await cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require", err: "message" }); + } catch (error) { + setError(error); + } finally { + setInProgress(false); + } }; const CryptoPolicyDialog = ({ currentCryptoPolicy, fipsEnabled, + fipsConfigurable, reApply, shaSubPolicyAvailable, }) => { @@ -125,6 +150,8 @@ const CryptoPolicyDialog = ({ const policies = Object.keys(cryptopolicies) .filter(pol => pol.endsWith(':SHA1') ? shaSubPolicyAvailable : true) + // cannot enable fips without fips-mode-setup + .filter(pol => pol.startsWith("FIPS") ? fipsConfigurable : true) .map(policy => ({ name: policy, title: displayProfileText(policy), @@ -185,7 +212,8 @@ const CryptoPolicyDialog = ({ {_("Applying new policy... This may take a few minutes.")} } - diff --git a/test/verify/check-system-info b/test/verify/check-system-info index 097187dcdbf8..5ce5000a0abe 100755 --- a/test/verify/check-system-info +++ b/test/verify/check-system-info @@ -61,6 +61,14 @@ class TestSystemInfo(testlib.MachineCase): # Most OSes don't set nosmt by default, but there are some exceptions self.expect_smt_default = self.machine.image in ["fedora-coreos"] + self.supportsFIPS = re.match('fedora|((centos|rhel)-(8|9)).*', self.machine.image) + # HACK: temporary: we are in the middle of transitioning centos/rhel-10 images away from fips-mode-setup; + # until https://github.com/cockpit-project/bots/pull/7095 and https://github.com/cockpit-project/bots/pull/7091 + # land, do runtime detection + if self.machine.image in ["centos-10", "rhel-10-0"]: + if '/bin' in self.machine.execute("command -v fips-mode-setup", check=False): + self.supportsFIPS = True + def testBasic(self): m = self.machine b = self.browser @@ -811,8 +819,6 @@ password=foobar profile = m.execute(cmd + " --show").strip() self.assertEqual(profile, new_profile) b.wait_text("#crypto-policy-button", shown_profile_text(profile)) - new_profile = "FIPS" - change_profile(profile, new_profile) # Select a custom policy (non-selectable option) profile = "EMPTY" @@ -824,6 +830,24 @@ password=foobar b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected", "Custom cryptographic policy") b.click("#crypto-policy-dialog button.pf-v5-c-button.pf-m-link") + # FIPS mode + if self.supportsFIPS: + change_profile(profile, "FIPS") + else: + # FIPS is not an available choice + b.click("#crypto-policy-button") + b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected", shown_profile_text(profile)) + self.assertNotIn("FIPS", b.text("#crypto-policy-dialog")) + b.click("#crypto-policy-dialog button:contains('Cancel')") + b.wait_not_present("#crypto-policy-dialog") + + # pretend the machine already has FIPS enabled, then it can't be disabled + m.execute(cmd + " --set FIPS") # this is known incomplete/invalid, but suffices to set the UI state + b.wait_text("#crypto-policy-current", "FIPS") + self.assertFalse(b.is_present("#crypto-policy-button")) + m.execute(cmd + f" --set {profile}") # this is known incomplete/invalid, but suffices to set the UI state + b.wait_text("#crypto-policy-button", shown_profile_text(profile)) + @testlib.skipImage("crypto-policies not available", "debian-*", "ubuntu-*", "arch") @testlib.skipOstree("crypto-policies not available") def testInconsistentCryptoPolicy(self): @@ -837,14 +861,24 @@ password=foobar self.login_and_go("/system") b.wait_text("#inconsistent_crypto_policy", "FIPS is not properly enabled") b.click(".system-health-crypto-policies button.pf-v5-c-button.pf-m-link") - b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected .pf-v5-c-label.pf-m-orange", "inconsistent") + if self.supportsFIPS: + # fix the FIPS policy + b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected .pf-v5-c-label.pf-m-orange", "inconsistent") + else: + # pick any valid non-FIPS policy + b.click("#crypto-policy-dialog .pf-v5-c-menu__list-item[data-value='DEFAULT'] button") b.click("#crypto-policy-save-reboot") # Initramfs re-generation takes a while m.wait_reboot(timeout_sec=600) m.start_cockpit() self.login_and_go("/system") - b.wait_text("#crypto-policy-button", "FIPS") - self.assertEqual(m.execute("cat /proc/sys/crypto/fips_enabled").strip(), "1") + if self.supportsFIPS: + b.wait_text("#crypto-policy-button", "FIPS") + self.assertEqual(m.execute("cat /proc/sys/crypto/fips_enabled").strip(), "1") + else: + b.wait_text("#crypto-policy-button", "Default") + # rest of the test does not apply any more + return m.execute(cmd + " --set DEFAULT") b.wait_text("#inconsistent_crypto_policy", "Cryptographic policy is inconsistent")