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 : };
|