* Copyright (C) 2022 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
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, ModalBody, ModalFooter, ModalHeader
} from '@patternfly/react-core/dist/esm/components/Modal/index.js';
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
import { ExclamationTriangleIcon, ExternalLinkSquareAltIcon, HelpIcon } from '@patternfly/react-icons';
import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { PrivilegedButton } from "cockpit-components-privileged";
import { ProfilesMenuDialogBody } from "./profiles-menu-dialog-body";
import { useDialogs } from "dialogs.jsx";
import { useInit } from "hooks";
import "./cryptoPolicies.scss";
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);
useInit(() => {
cockpit.file("/proc/sys/crypto/fips_enabled").read()
.then(content => setFipsEnabled(content ? content.trim() === "1" : false));
getFipsConfigurable().then(v => setFipsConfigurable(v));
cockpit.file("/etc/crypto-policies/config")
.watch(async contents => {
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");
const filteredContent = contents?.split('\n').filter(line => !line.startsWith("#")).join('\n');
setCurrentCryptoPolicy(filteredContent?.trim() ?? null);
}
});
cockpit.file("/usr/share/crypto-policies/policies/modules/SHA1.pmod").read()
.then(content => setShaSubPolicyAvailable(content ? content.trim() : false));
});
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 */
? <span id="crypto-policy-current">{displayProfileText(currentCryptoPolicy)}</span>
: <PrivilegedButton variant="link" buttonId="crypto-policy-button" tooltipId="tip-crypto-policy"
excuse={ _("The user $0 is not permitted to change cryptographic policies") }
onClick={() => Dialogs.show(<CryptoPolicyDialog
currentCryptoPolicy={currentCryptoPolicy}
setCurrentCryptoPolicy={setCurrentCryptoPolicy}
fipsEnabled={fipsEnabled}
fipsConfigurable={fipsConfigurable}
shaSubPolicyAvailable={shaSubPolicyAvailable} />)}>
{displayProfileText(currentCryptoPolicy)}
</PrivilegedButton>;
return (
<tr className="pf-v6-c-table__tr">
<th className="pf-v6-c-table__th" scope="row">{_("Cryptographic policy")}</th>
<td className="pf-v6-c-table__td">{policyRender}</td>
</tr>
);
};
const setPolicy = async (policy, setError, setInProgress, fipsConfigurable) => {
setInProgress(true);
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" });
}
await cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require", err: "message" });
} catch (error) {
setError(error);
} finally {
setInProgress(false);
}
};
const CryptoPolicyDialog = ({
currentCryptoPolicy,
fipsEnabled,
fipsConfigurable,
reApply,
shaSubPolicyAvailable,
}) => {
const Dialogs = useDialogs();
const [error, setError] = useState();
const [inProgress, setInProgress] = useState(false);
const [selected, setSelected] = useState(currentCryptoPolicy);
const cryptopolicies = {
DEFAULT: _("Recommended, secure settings for current threat models."),
"DEFAULT:SHA1": _("DEFAULT with SHA-1 signature verification allowed."),
LEGACY: _("Higher interoperability at the cost of an increased attack surface."),
"LEGACY:AD-SUPPORT": _("LEGACY with Active Directory interoperability."),
FIPS: (<Flex alignItems={{ default: 'alignItemsCenter' }}>
{_("Only use approved and allowed algorithms when booting in FIPS mode.")}
<Button component='a'
rel="noopener noreferrer" target="_blank"
variant='link'
isInline
icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening">
{_("Learn more")}
</Button>
</Flex>),
"FIPS:OSPP": _("FIPS with further Common Criteria restrictions."),
FUTURE: _("Protects from anticipated near-term future attacks at the expense of interoperability."),
};
const policies = Object.keys(cryptopolicies)
.filter(pol => pol.endsWith(':SHA1') ? shaSubPolicyAvailable : true)
.filter(pol => pol.startsWith("FIPS") ? fipsConfigurable : true)
.map(policy => ({
name: policy,
title: displayProfileText(policy),
description: cryptopolicies[policy],
active: !isInconsistentPolicy(policy, fipsEnabled) && policy === currentCryptoPolicy,
inconsistent: isInconsistentPolicy(policy, fipsEnabled) && policy === currentCryptoPolicy,
recommended: policy === 'DEFAULT',
}));
if (!(currentCryptoPolicy in cryptopolicies)) {
policies.push({
name: currentCryptoPolicy,
title: displayProfileText(currentCryptoPolicy),
description: _("Custom cryptographic policy"),
active: !isInconsistentPolicy(currentCryptoPolicy, fipsEnabled),
inconsistent: isInconsistentPolicy(currentCryptoPolicy, fipsEnabled),
recommended: false,
});
}
const help = (
<Popover
id="crypto-policies-help"
bodyContent={
<div>
{_("Cryptographic Policies is a system component that configures the core cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos protocols.")}
</div>
}
footerContent={
<Button component='a'
rel="noopener noreferrer" target="_blank"
variant='link'
isInline
icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening">
{_("Learn more")}
</Button>
}
>
<Button icon={<HelpIcon />} variant="plain" aria-label={_("Help")} />
</Popover>
);
return (
<Modal position="top" variant="medium"
className="ct-m-stretch-body"
isOpen
onClose={Dialogs.close}
id="crypto-policy-dialog"
>
<ModalHeader title={_("Change cryptographic policy")}
help={help}
/>
<ModalBody>
{error && <ModalError dialogError={typeof error == 'string' ? error : error.message} />}
{currentCryptoPolicy && <ProfilesMenuDialogBody active_profile={currentCryptoPolicy}
change_selected={setSelected}
isDisabled={inProgress}
profiles={policies} />}
</ModalBody>
<ModalFooter>
{inProgress &&
<Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
{_("Applying new policy... This may take a few minutes.")}
</Flex>}
<Button id="crypto-policy-save-reboot" variant='primary'
onClick={() => setPolicy(selected, setError, setInProgress, fipsConfigurable)}
isDisabled={inProgress} isLoading={inProgress}
>
{reApply ? _("Reapply and reboot") : _("Apply and reboot")}
</Button>
<Button variant='link' onClick={Dialogs.close} isDisabled={inProgress}>
{_("Cancel")}
</Button>
</ModalFooter>
</Modal>
);
};
export const CryptoPolicyStatus = () => {
const Dialogs = useDialogs();
const [currentCryptoPolicy, setCurrentCryptoPolicy] = useState(null);
const [fipsEnabled, setFipsEnabled] = useState(null);
const [fipsConfigurable, setFipsConfigurable] = useState(null);
useInit(() => {
cockpit.file("/etc/crypto-policies/state/current")
.watch(content => setCurrentCryptoPolicy(content ? content.trim().split(':', 1)[0] : undefined));
getFipsConfigurable().then(v => setFipsConfigurable(v));
cockpit.file("/proc/sys/crypto/fips_enabled").read()
.then(content => setFipsEnabled(content ? content.trim() === "1" : false));
});
if (currentCryptoPolicy === null || fipsConfigurable === null)
return null;
if (isInconsistentPolicy(currentCryptoPolicy, fipsEnabled)) {
return (
<li className="system-health-crypto-policies">
<Flex flexWrap={{ default: 'nowrap' }}>
<FlexItem><ExclamationTriangleIcon className="crypto-policies-health-card-icon" /></FlexItem>
<div>
<div id="inconsistent_crypto_policy">
{currentCryptoPolicy === "FIPS" ? _("FIPS is not properly enabled") : _("Cryptographic policy is inconsistent")}
</div>
<Button isInline variant="link" className="pf-v6-u-font-size-sm"
onClick={() => Dialogs.show(<CryptoPolicyDialog currentCryptoPolicy={currentCryptoPolicy}
fipsEnabled={fipsEnabled}
fipsConfigurable={fipsConfigurable}
reApply />)}>
{_("Review cryptographic policy")}
</Button>
</div>
</Flex>
</li>
);
}
return null;
};