mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
feat: add CA-signed certificate authentication for SSH credentials
Support OpenSSH certificate-based authentication (-cert.pub files) in the Credentials manager. When a CA-signed certificate is stored alongside a private key, Termix uses it during SSH connection establishment so that servers relying on certificate-based authorization work out of the box. Changes: - db/schema.ts: add cert_public_key column to ssh_credentials table - db/index.ts: auto-migration via addColumnIfNotExists - routes/credentials.ts: expose certPublicKey in create/update/get endpoints - ssh/auth-manager.ts: include certPublicKey in ResolvedCredentials - ssh/host-resolver.ts: propagate certPublicKey when resolving credentials - ssh/opkssh-cert-auth.ts: refactor shared logic into _applyCertToConnection; export new setupCACertAuth() with optional passphrase support - ssh/terminal.ts: call setupCACertAuth() when a certificate is present - utils/ssh-key-utils.ts: detect all OpenSSH cert types in public key parser - types/index.ts: add certPublicKey to Credential, CredentialBackend, CredentialData interfaces - CredentialAuthenticationTab.tsx: new CA Certificate section with file upload, paste editor and automatic cert-type badge - CredentialEditor.tsx: certPublicKey wired into form schema and submit - CredentialViewer.tsx: show certificate status in security tab - locales/en.json: add i18n strings for new UI elements
This commit is contained in:
@@ -4932,11 +4932,13 @@ function CredentialEditorView({
|
||||
value: credential?.value ?? "",
|
||||
publicKey: credential?.publicKey ?? "",
|
||||
passphrase: credential?.passphrase ?? "",
|
||||
certPublicKey: (credential as any)?.certPublicKey ?? "",
|
||||
}));
|
||||
const { t } = useTranslation();
|
||||
const [generatingKey, setGeneratingKey] = useState(false);
|
||||
const [generatingPublicKey, setGeneratingPublicKey] = useState(false);
|
||||
const credFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const certFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const setCredField = <K extends keyof typeof credForm>(
|
||||
k: K,
|
||||
v: (typeof credForm)[K],
|
||||
@@ -4961,6 +4963,8 @@ function CredentialEditorView({
|
||||
: credForm.value || null
|
||||
: null,
|
||||
publicKey: credForm.type === "key" ? credForm.publicKey : null,
|
||||
certPublicKey:
|
||||
credForm.type === "key" ? credForm.certPublicKey || null : null,
|
||||
keyPassword:
|
||||
credForm.type === "key"
|
||||
? credForm.passphrase === "existing_key_password"
|
||||
@@ -5103,6 +5107,7 @@ function CredentialEditorView({
|
||||
type: m as any,
|
||||
value: "",
|
||||
publicKey: "",
|
||||
certPublicKey: "",
|
||||
passphrase: "",
|
||||
}))
|
||||
}
|
||||
@@ -5329,6 +5334,52 @@ function CredentialEditorView({
|
||||
className="w-full px-3 py-2 text-[10px] bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 p-3 border border-border bg-muted/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("credentials.caCertificate")}
|
||||
</label>
|
||||
{credForm.certPublicKey && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-[10px] text-destructive hover:text-destructive/80"
|
||||
onClick={() => setCredField("certPublicKey", "")}
|
||||
>
|
||||
{t("credentials.clearCert")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{t("credentials.caCertificateDescription")}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-[10px] text-accent-brand hover:text-accent-brand/80 flex items-center gap-1 self-start"
|
||||
onClick={() => certFileInputRef.current?.click()}
|
||||
>
|
||||
<Upload className="size-3" /> {t("credentials.uploadCertFile")}
|
||||
</button>
|
||||
<input
|
||||
ref={certFileInputRef}
|
||||
type="file"
|
||||
accept=".pub,.txt"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const text = await file.text();
|
||||
setCredField("certPublicKey", text.trim());
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<textarea
|
||||
placeholder={t("credentials.pasteOrUploadCert")}
|
||||
rows={2}
|
||||
value={credForm.certPublicKey}
|
||||
onChange={(e) => setCredField("certPublicKey", e.target.value)}
|
||||
className="w-full px-3 py-2 text-[10px] bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -6922,6 +6973,8 @@ export function HostManager({
|
||||
passphrase: (full as any).hasKeyPassword
|
||||
? "existing_key_password"
|
||||
: "",
|
||||
certPublicKey:
|
||||
(full as any).certPublicKey ?? "",
|
||||
});
|
||||
} catch {
|
||||
setEditingCredential(cred);
|
||||
|
||||
Reference in New Issue
Block a user