LCOV - code coverage report
Current view: top level - pkg/systemd/overview-cards - cryptoPolicies.jsx Coverage Total Hit
Test: cockpit Lines: 87.9 % 207 182
Test Date: 2026-06-17 06:28:00

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2022 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6           36 : import cockpit from "cockpit";
       7           36 : import React, { useState } from 'react';
       8              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
       9              : import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
      10              : import {
      11              :     Modal, ModalBody, ModalFooter, ModalHeader
      12              : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
      13              : import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
      14              : import { ExclamationTriangleIcon, ExternalLinkSquareAltIcon, HelpIcon } from '@patternfly/react-icons';
      15              : 
      16              : import { ModalError } from 'cockpit-components-inline-notification.jsx';
      17              : import { PrivilegedButton } from "cockpit-components-privileged";
      18              : import { ProfilesMenuDialogBody } from "./profiles-menu-dialog-body";
      19              : import { useDialogs } from "dialogs.jsx";
      20              : import { useInit } from "hooks";
      21              : 
      22              : import "./cryptoPolicies.scss";
      23              : 
      24           36 : const _ = cockpit.gettext;
      25              : 
      26            4 : const displayProfileText = profile => profile === "DEFAULT" ? _("Default") : profile;
      27           33 : const isInconsistentPolicy = (policy, fipsEnabled) => policy === "FIPS" !== fipsEnabled;
      28              : 
      29           36 : const getFipsConfigurable = () => cockpit.spawn(["/bin/sh", "-c", "command -v fips-mode-setup"], { error: "ignore" })
      30            0 :         .then(() => true)
      31           33 :         .catch(() => false);
      32              : 
      33           36 : export const CryptoPolicyRow = () => {
      34           36 :     const Dialogs = useDialogs();
      35           36 :     const [currentCryptoPolicy, setCurrentCryptoPolicy] = useState(null);
      36           36 :     const [fipsEnabled, setFipsEnabled] = useState(null);
      37           36 :     const [fipsConfigurable, setFipsConfigurable] = useState(null);
      38           36 :     const [shaSubPolicyAvailable, setShaSubPolicyAvailable] = useState(null);
      39              : 
      40           36 :     useInit(() => {
      41           36 :         cockpit.file("/proc/sys/crypto/fips_enabled").read()
      42            6 :                 .then(content => setFipsEnabled(content ? content.trim() === "1" : false));
      43           33 :         getFipsConfigurable().then(v => setFipsConfigurable(v));
      44           36 :         cockpit.file("/etc/crypto-policies/config")
      45           33 :                 .watch(async contents => {
      46              :                     // Ask crypto-policies to get correct FIPS state, as that dominates the configured policy
      47           33 :                     try {
      48           24 :                         setCurrentCryptoPolicy((await cockpit.spawn(["update-crypto-policies", "--show"])).trim());
      49            6 :                     } catch (error) {
      50            6 :                         console.warn("Failed to get current crypto policy:", error.toString(),
      51            6 :                                      "; falling back to /etc/crypto-policies/config");
      52            0 :                         const filteredContent = contents?.split('\n').filter(line => !line.startsWith("#")).join('\n');
      53            6 :                         setCurrentCryptoPolicy(filteredContent?.trim() ?? null);
      54            6 :                     }
      55           33 :                 });
      56              :         // RHEL-8-8 has no SHA1 subpolicy
      57           36 :         cockpit.file("/usr/share/crypto-policies/policies/modules/SHA1.pmod").read()
      58            6 :                 .then(content => setShaSubPolicyAvailable(content ? content.trim() : false));
      59           36 :     });
      60              : 
      61           24 :     if (currentCryptoPolicy === null || fipsEnabled === null || fipsConfigurable === null)
      62           36 :         return null;
      63              : 
      64            6 :     const policyRender = (currentCryptoPolicy.startsWith("FIPS") && !fipsConfigurable)
      65              :         /* read-only mode; can't switch away from FIPS without fips-mode-setup */
      66            6 :         ? <span id="crypto-policy-current">{displayProfileText(currentCryptoPolicy)}</span>
      67           24 :         : <PrivilegedButton variant="link" buttonId="crypto-policy-button" tooltipId="tip-crypto-policy"
      68           24 :                             excuse={ _("The user $0 is not permitted to change cryptographic policies") }
      69            1 :                             onClick={() => Dialogs.show(<CryptoPolicyDialog
      70            1 :                                                             currentCryptoPolicy={currentCryptoPolicy}
      71            1 :                                                             setCurrentCryptoPolicy={setCurrentCryptoPolicy}
      72            1 :                                                             fipsEnabled={fipsEnabled}
      73            1 :                                                             fipsConfigurable={fipsConfigurable}
      74            1 :                                                             shaSubPolicyAvailable={shaSubPolicyAvailable} />)}>
      75           24 :             {displayProfileText(currentCryptoPolicy)}
      76           24 :         </PrivilegedButton>;
      77              : 
      78           36 :     return (
      79           36 :         <tr className="pf-v6-c-table__tr">
      80           36 :             <th className="pf-v6-c-table__th" scope="row">{_("Cryptographic policy")}</th>
      81           36 :             <td className="pf-v6-c-table__td">{policyRender}</td>
      82           36 :         </tr>
      83              :     );
      84           36 : };
      85              : 
      86            0 : const setPolicy = async (policy, setError, setInProgress, fipsConfigurable) => {
      87            0 :     setInProgress(true);
      88              : 
      89            0 :     try {
      90            0 :         if (policy === "FIPS") {
      91            0 :             cockpit.assert(fipsConfigurable, "calling setPolicy(FIPS) without fips-mode-setup");
      92            0 :             await cockpit.spawn(["fips-mode-setup", "--enable"], { superuser: "require", err: "message" });
      93            0 :         } else {
      94            0 :             if (fipsConfigurable)
      95            0 :                 await cockpit.spawn(["fips-mode-setup", "--disable"], { superuser: "require", err: "message" });
      96            0 :             await cockpit.spawn(["update-crypto-policies", "--set", policy], { superuser: "require", err: "message" });
      97            0 :         }
      98              : 
      99            0 :         await cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require", err: "message" });
     100            0 :     } catch (error) {
     101            0 :         setError(error);
     102            0 :     } finally {
     103            0 :         setInProgress(false);
     104            0 :     }
     105            0 : };
     106              : 
     107            1 : const CryptoPolicyDialog = ({
     108            1 :     currentCryptoPolicy,
     109            1 :     fipsEnabled,
     110            1 :     fipsConfigurable,
     111            1 :     reApply,
     112            1 :     shaSubPolicyAvailable,
     113            1 : }) => {
     114            1 :     const Dialogs = useDialogs();
     115            1 :     const [error, setError] = useState();
     116            1 :     const [inProgress, setInProgress] = useState(false);
     117            1 :     const [selected, setSelected] = useState(currentCryptoPolicy);
     118              : 
     119              :     // Found in /usr/share/crypto-policies/policies/
     120            1 :     const cryptopolicies = {
     121            1 :         DEFAULT: _("Recommended, secure settings for current threat models."),
     122            1 :         "DEFAULT:SHA1": _("DEFAULT with SHA-1 signature verification allowed."),
     123            1 :         LEGACY: _("Higher interoperability at the cost of an increased attack surface."),
     124            1 :         "LEGACY:AD-SUPPORT": _("LEGACY with Active Directory interoperability."),
     125            1 :         FIPS: (<Flex alignItems={{ default: 'alignItemsCenter' }}>
     126            1 :             {_("Only use approved and allowed algorithms when booting in FIPS mode.")}
     127            1 :             <Button component='a'
     128            1 :                     rel="noopener noreferrer" target="_blank"
     129            1 :                     variant='link'
     130            1 :                     isInline
     131            1 :                     icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
     132            1 :                     href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening">
     133            1 :                 {_("Learn more")}
     134            1 :             </Button>
     135            1 :         </Flex>),
     136            1 :         "FIPS:OSPP": _("FIPS with further Common Criteria restrictions."),
     137            1 :         FUTURE: _("Protects from anticipated near-term future attacks at the expense of interoperability."),
     138            1 :     };
     139              : 
     140            1 :     const policies = Object.keys(cryptopolicies)
     141            1 :             .filter(pol => pol.endsWith(':SHA1') ? shaSubPolicyAvailable : true)
     142              :             // cannot enable fips without fips-mode-setup
     143            1 :             .filter(pol => pol.startsWith("FIPS") ? fipsConfigurable : true)
     144            1 :             .map(policy => ({
     145            1 :                 name: policy,
     146            1 :                 title: displayProfileText(policy),
     147            1 :                 description: cryptopolicies[policy],
     148            1 :                 active: !isInconsistentPolicy(policy, fipsEnabled) && policy === currentCryptoPolicy,
     149            1 :                 inconsistent: isInconsistentPolicy(policy, fipsEnabled) && policy === currentCryptoPolicy,
     150            1 :                 recommended: policy === 'DEFAULT',
     151            1 :             }));
     152              : 
     153              :     // Custom profile
     154            1 :     if (!(currentCryptoPolicy in cryptopolicies)) {
     155            1 :         policies.push({
     156            1 :             name: currentCryptoPolicy,
     157            1 :             title: displayProfileText(currentCryptoPolicy),
     158            1 :             description: _("Custom cryptographic policy"),
     159            1 :             active: !isInconsistentPolicy(currentCryptoPolicy, fipsEnabled),
     160            1 :             inconsistent: isInconsistentPolicy(currentCryptoPolicy, fipsEnabled),
     161            1 :             recommended: false,
     162            1 :         });
     163            1 :     }
     164              : 
     165            1 :     const help = (
     166            1 :         <Popover
     167            1 :             id="crypto-policies-help"
     168            1 :             bodyContent={
     169            1 :                 <div>
     170            1 :                     {_("Cryptographic Policies is a system component that configures the core cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos protocols.")}
     171            1 :                 </div>
     172              :             }
     173            1 :             footerContent={
     174            1 :                 <Button component='a'
     175            1 :                         rel="noopener noreferrer" target="_blank"
     176            1 :                         variant='link'
     177            1 :                         isInline
     178            1 :                         icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
     179            1 :                         href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening">
     180            1 :                     {_("Learn more")}
     181            1 :                 </Button>
     182              :             }
     183              :         >
     184            1 :             <Button icon={<HelpIcon />} variant="plain" aria-label={_("Help")} />
     185            1 :         </Popover>
     186              :     );
     187              : 
     188            1 :     return (
     189            1 :         <Modal position="top" variant="medium"
     190            1 :                className="ct-m-stretch-body"
     191            1 :                isOpen
     192            1 :                onClose={Dialogs.close}
     193            1 :                id="crypto-policy-dialog"
     194              :         >
     195            1 :             <ModalHeader title={_("Change cryptographic policy")}
     196            1 :                 help={help}
     197            1 :             />
     198            1 :             <ModalBody>
     199            1 :                 {error && <ModalError dialogError={typeof error == 'string' ? error : error.message} />}
     200            1 :                 {currentCryptoPolicy && <ProfilesMenuDialogBody active_profile={currentCryptoPolicy}
     201            1 :                                                          change_selected={setSelected}
     202            1 :                                                          isDisabled={inProgress}
     203            1 :                                                          profiles={policies} />}
     204            1 :             </ModalBody>
     205            1 :             <ModalFooter>
     206            1 :                 {inProgress &&
     207            1 :                 <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
     208            1 :                     {_("Applying new policy... This may take a few minutes.")}
     209            1 :                 </Flex>}
     210            1 :                 <Button id="crypto-policy-save-reboot" variant='primary'
     211            0 :                         onClick={() => setPolicy(selected, setError, setInProgress, fipsConfigurable)}
     212            1 :                         isDisabled={inProgress} isLoading={inProgress}
     213              :                 >
     214            1 :                     {reApply ? _("Reapply and reboot") : _("Apply and reboot")}
     215            1 :                 </Button>
     216            1 :                 <Button variant='link' onClick={Dialogs.close} isDisabled={inProgress}>
     217            1 :                     {_("Cancel")}
     218            1 :                 </Button>
     219            1 :             </ModalFooter>
     220            1 :         </Modal>
     221              :     );
     222            1 : };
     223              : 
     224           36 : export const CryptoPolicyStatus = () => {
     225           36 :     const Dialogs = useDialogs();
     226           36 :     const [currentCryptoPolicy, setCurrentCryptoPolicy] = useState(null);
     227           36 :     const [fipsEnabled, setFipsEnabled] = useState(null);
     228           36 :     const [fipsConfigurable, setFipsConfigurable] = useState(null);
     229              : 
     230           36 :     useInit(() => {
     231           36 :         cockpit.file("/etc/crypto-policies/state/current")
     232            6 :                 .watch(content => setCurrentCryptoPolicy(content ? content.trim().split(':', 1)[0] : undefined));
     233           33 :         getFipsConfigurable().then(v => setFipsConfigurable(v));
     234           36 :         cockpit.file("/proc/sys/crypto/fips_enabled").read()
     235            6 :                 .then(content => setFipsEnabled(content ? content.trim() === "1" : false));
     236           36 :     });
     237              : 
     238           33 :     if (currentCryptoPolicy === null || fipsConfigurable === null)
     239           36 :         return null;
     240              : 
     241            6 :     if (isInconsistentPolicy(currentCryptoPolicy, fipsEnabled)) {
     242            6 :         return (
     243            6 :             <li className="system-health-crypto-policies">
     244            6 :                 <Flex flexWrap={{ default: 'nowrap' }}>
     245            6 :                     <FlexItem><ExclamationTriangleIcon className="crypto-policies-health-card-icon" /></FlexItem>
     246            6 :                     <div>
     247            6 :                         <div id="inconsistent_crypto_policy">
     248            6 :                             {currentCryptoPolicy === "FIPS" ? _("FIPS is not properly enabled") : _("Cryptographic policy is inconsistent")}
     249            6 :                         </div>
     250            6 :                         <Button isInline variant="link" className="pf-v6-u-font-size-sm"
     251            0 :                                 onClick={() => Dialogs.show(<CryptoPolicyDialog currentCryptoPolicy={currentCryptoPolicy}
     252            0 :                                                                                 fipsEnabled={fipsEnabled}
     253            0 :                                                                                 fipsConfigurable={fipsConfigurable}
     254            0 :                                                                                 reApply />)}>
     255            6 :                             {_("Review cryptographic policy")}
     256            6 :                         </Button>
     257            6 :                     </div>
     258            6 :                 </Flex>
     259            6 :             </li>
     260              :         );
     261            6 :     }
     262              : 
     263           33 :     return null;
     264           36 : };
        

Generated by: LCOV version 2.0-1