LCOV - code coverage report
Current view: top level - pkg/storaged/crypto - tang.jsx Coverage Total Hit
Test: cockpit Lines: 82.1 % 112 92
Test Date: 2026-07-17 12:03:54

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2023 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          113 : import cockpit from "cockpit";
       7          113 : import React from "react";
       8              : 
       9              : import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
      10              : import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
      11              : 
      12              : import { useInit } from "hooks";
      13              : 
      14          113 : import stable_stringify from "json-stable-stringify-without-jsonify";
      15              : 
      16          113 : const _ = cockpit.gettext;
      17              : 
      18            1 : async function digest(text, hash) {
      19            1 :     const encoder = new TextEncoder();
      20            1 :     const data = encoder.encode(text);
      21            1 :     const digest = await window.crypto.subtle.digest(hash, data);
      22            1 :     return [...new Uint8Array(digest)];
      23            1 : }
      24              : 
      25            1 : export function validate_url(url) {
      26            1 :     if (url.length === 0)
      27            0 :         return _("Address cannot be empty");
      28            1 :     if (!parse_url(url))
      29            0 :         return _("Address is not a valid URL");
      30            1 : }
      31              : 
      32            1 : export function get_tang_adv(url) {
      33            1 :     return cockpit.spawn(["curl", "-sSf", url + "/adv"], { err: "message" })
      34            1 :             .then(JSON.parse)
      35            0 :             .catch(error => {
      36            0 :                 return Promise.reject(error.toString().replace(/^curl: \([0-9]+\) /, ""));
      37            0 :             });
      38            1 : }
      39              : 
      40            1 : function parse_url(url) {
      41              :     // clevis-encrypt-tang defaults to "http://" (via curl), so we do the same here.
      42            1 :     if (!/^[a-zA-Z]+:\/\//.test(url))
      43            1 :         url = "http://" + url;
      44            1 :     try {
      45            1 :         return new URL(url);
      46            0 :     } catch (e) {
      47            0 :         if (e instanceof TypeError)
      48            0 :             return null;
      49            0 :         throw e;
      50            0 :     }
      51            1 : }
      52              : 
      53            1 : function tang_adv_payload(adv) {
      54            1 :     return JSON.parse(window.atob(adv.payload));
      55            1 : }
      56              : 
      57            1 : function jwk_b64_encode(bytes) {
      58              :     // Use the urlsafe character set, and strip the padding.
      59            1 :     return cockpit.base64_encode(bytes).replace(/\+/g, "-")
      60            1 :             .replace(/\//g, "_")
      61            1 :             .replace(/=+$/, '');
      62            1 : }
      63              : 
      64            1 : async function compute_thp(jwk) {
      65            1 :     const REQUIRED_ATTRS = {
      66            1 :         RSA: ['kty', 'p', 'd', 'q', 'dp', 'dq', 'qi', 'oth'],
      67            1 :         EC: ['kty', 'crv', 'x', 'y'],
      68            1 :         oct: ['kty', 'k'],
      69            1 :     };
      70              : 
      71            1 :     if (!jwk.kty)
      72            0 :         return "(no key type attribute=";
      73            1 :     if (!REQUIRED_ATTRS[jwk.kty])
      74            0 :         return cockpit.format("(unknown keytype $0)", jwk.kty);
      75              : 
      76            1 :     const req = REQUIRED_ATTRS[jwk.kty];
      77            1 :     const norm = { };
      78            1 :     req.forEach(k => { if (k in jwk) norm[k] = jwk[k]; });
      79              : 
      80            1 :     const hashes = {};
      81            1 :     try {
      82            1 :         const sha256 = jwk_b64_encode(await digest(stable_stringify(norm), "SHA-256"));
      83            1 :         hashes.sha256 = sha256;
      84            0 :     } catch (err) {
      85            0 :         console.warn("Unable to create a sha256 hash", err);
      86            0 :     }
      87              : 
      88            1 :     try {
      89            1 :         const sha1 = jwk_b64_encode(await digest(stable_stringify(norm), "SHA-1"));
      90            1 :         hashes.sha1 = sha1;
      91            0 :     } catch (err) {
      92            0 :         console.warn("Unable to create a sha1 hash", err);
      93            0 :     }
      94              : 
      95            1 :     return hashes;
      96            1 : }
      97              : 
      98            1 : function compute_sigkey_thps(adv) {
      99            1 :     function is_signing_key(jwk) {
     100            1 :         if (!jwk.use && !jwk.key_ops)
     101            0 :             return true;
     102            1 :         if (jwk.use == "sig")
     103            0 :             return true;
     104            1 :         if (jwk.key_ops && jwk.key_ops.indexOf("verify") >= 0)
     105            1 :             return true;
     106            1 :         return false;
     107            1 :     }
     108              : 
     109            1 :     return adv.keys.filter(is_signing_key).map(compute_thp);
     110            1 : }
     111              : 
     112            1 : export const TangKeyVerification = ({ url, adv }) => {
     113            1 :     const parsed = parse_url(url);
     114            1 :     const cmd = cockpit.format("ssh $0 tang-show-keys $1", parsed.hostname, parsed.port);
     115            1 :     const [sigkey_thps, setSigKey] = React.useState(null);
     116              : 
     117            1 :     useInit(async () => {
     118            1 :         const sigkey = await Promise.all(compute_sigkey_thps(tang_adv_payload(adv)));
     119            1 :         setSigKey(sigkey);
     120            1 :     });
     121              : 
     122            1 :     if (sigkey_thps === null)
     123            1 :         return null;
     124              : 
     125            1 :     return (
     126            1 :         <>
     127            1 :             <Content component={ContentVariants.p}>{_("Check the key hash with the Tang server.")}</Content>
     128              : 
     129            1 :             <Content component={ContentVariants.h3}>{_("How to check")}</Content>
     130            1 :             <span>{_("In a terminal, run: ")}</span>
     131            1 :             <ClipboardCopy hoverTip={_("Copy to clipboard")}
     132            1 :                             clickTip={_("Successfully copied to clipboard!")}
     133            1 :                             variant="inline-compact"
     134            1 :                             isCode>
     135            1 :                 {cmd}
     136            1 :             </ClipboardCopy>
     137            1 :             <Content component={ContentVariants.p}>
     138            1 :                 {_("Check that the SHA-256 or SHA-1 hash from the command matches this dialog.")}
     139            1 :             </Content>
     140              : 
     141            1 :             <Content component={ContentVariants.h3}>{_("SHA-256")}</Content>
     142            1 :             { sigkey_thps.map(s => <Content key={s.sha256} component={ContentVariants.pre}>{s.sha256}</Content>) }
     143              : 
     144            1 :             <Content component={ContentVariants.h3}>{_("SHA-1")}</Content>
     145            1 :             { sigkey_thps.map(s => <Content key={s.sha1} component={ContentVariants.pre}>{s.sha1}</Content>) }
     146            1 :         </>);
     147            1 : };
        

Generated by: LCOV version 2.0-1