From cb25f21c44d50f3e49e2b7cececbf8334cc620d3 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Fri, 22 May 2026 21:33:21 +0000 Subject: [PATCH] feat: add SSH key signing and verification UI --- apps/web/src/api/ssh_keys.ts | 27 ++++++ apps/web/src/pages/ssh-keys.tsx | 149 +++++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/apps/web/src/api/ssh_keys.ts b/apps/web/src/api/ssh_keys.ts index 84061d7..46aa0e5 100644 --- a/apps/web/src/api/ssh_keys.ts +++ b/apps/web/src/api/ssh_keys.ts @@ -24,3 +24,30 @@ export async function createSSHKey(data: SSHKeyCreate): Promise { export async function deleteSSHKey(keyId: string): Promise { await apiClient.delete(`/ssh-keys/${keyId}`); } + +export interface SignPayloadRequest { + payload: string; +} + +export interface SignatureResponse { + signature: string; +} + +export interface VerifySignatureRequest { + payload: string; + signature: string; +} + +export interface VerifySignatureResponse { + valid: boolean; +} + +export async function signPayload(keyId: string, data: SignPayloadRequest): Promise { + const response = await apiClient.post(`/ssh-keys/${keyId}/sign`, data); + return response.data; +} + +export async function verifySignature(keyId: string, data: VerifySignatureRequest): Promise { + const response = await apiClient.post(`/ssh-keys/${keyId}/verify`, data); + return response.data; +} diff --git a/apps/web/src/pages/ssh-keys.tsx b/apps/web/src/pages/ssh-keys.tsx index 264d317..60fc57f 100644 --- a/apps/web/src/pages/ssh-keys.tsx +++ b/apps/web/src/pages/ssh-keys.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { createSSHKey, deleteSSHKey, listSSHKeys, type SSHKey } from "../api/ssh_keys"; +import { createSSHKey, deleteSSHKey, listSSHKeys, signPayload, verifySignature, type SSHKey } from "../api/ssh_keys"; import { Icon } from "../components/icon"; export const SSHKeysPage = () => { @@ -10,6 +10,13 @@ export const SSHKeysPage = () => { const [error, setError] = useState(null); const [newKeyName, setNewKeyName] = useState(""); const [generating, setGenerating] = useState(false); + const [signPayloads, setSignPayloads] = useState>({}); + const [signatures, setSignatures] = useState>({}); + const [signing, setSigning] = useState>({}); + const [verifyPayloads, setVerifyPayloads] = useState>({}); + const [verifySignatures, setVerifySignatures] = useState>({}); + const [verifyResults, setVerifyResults] = useState>({}); + const [verifying, setVerifying] = useState>({}); useEffect(() => { loadKeys(); @@ -59,6 +66,42 @@ export const SSHKeysPage = () => { navigator.clipboard.writeText(text); } + async function handleSign(keyId: string) { + const payload = signPayloads[keyId]; + if (!payload?.trim()) return; + + try { + setSigning((prev) => ({ ...prev, [keyId]: true })); + const result = await signPayload(keyId, { payload: payload.trim() }); + setSignatures((prev) => ({ ...prev, [keyId]: result.signature })); + setError(null); + } catch { + setError("Failed to sign payload"); + } finally { + setSigning((prev) => ({ ...prev, [keyId]: false })); + } + } + + async function handleVerify(keyId: string) { + const payload = verifyPayloads[keyId]; + const signature = verifySignatures[keyId]; + if (!payload?.trim() || !signature?.trim()) return; + + try { + setVerifying((prev) => ({ ...prev, [keyId]: true })); + const result = await verifySignature(keyId, { + payload: payload.trim(), + signature: signature.trim(), + }); + setVerifyResults((prev) => ({ ...prev, [keyId]: result.valid })); + setError(null); + } catch { + setError("Failed to verify signature"); + } finally { + setVerifying((prev) => ({ ...prev, [keyId]: false })); + } + } + if (loading) return
Loading...
; return ( @@ -133,6 +176,110 @@ export const SSHKeysPage = () => { Copy Full Key + +
+

Sign Payload

+
+