From 98bce1b0da675715b2e5482c0ad61ba21135f62f Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Di, 04 Aug 2026 07:00:04 +0200
Subject: [PATCH] Add a desktop client that knows what is installed locally

---
 packages/bibliothek-server/src/signatur.ts |   66 +++++++++++++++++++++++++++++++++
 1 files changed, 66 insertions(+), 0 deletions(-)

diff --git a/packages/bibliothek-server/src/signatur.ts b/packages/bibliothek-server/src/signatur.ts
new file mode 100644
index 0000000..566db02
--- /dev/null
+++ b/packages/bibliothek-server/src/signatur.ts
@@ -0,0 +1,66 @@
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+
+const execFileAsync = promisify(execFile);
+
+export interface Signaturbefund {
+  /** Windows-Status: Valid, NotSigned, UnknownError, HashMismatch … bzw. „nicht geprüft". */
+  status: string;
+  /** Anzeigename des Signaturgebers (leer, wenn unsigniert). */
+  herausgeber: string;
+}
+
+const NICHT_GEPRUEFT: Signaturbefund = { status: 'nicht geprüft', herausgeber: '' };
+
+/**
+ * Prüft die Windows-Signatur (Authenticode) einer Datei.
+ *
+ * Bewusst nur erfassend, nicht blockierend: Manche Programme werden legitim
+ * unsigniert ausgeliefert (z. B. 7-Zip). Erst ein hinterlegter Erwartungswert
+ * macht daraus eine Zusicherung — siehe `weichtHerausgeberAb`.
+ *
+ * Der Dateipfad geht über eine Umgebungsvariable an PowerShell, damit
+ * Anführungszeichen im Pfad nichts zerbrechen können.
+ */
+export async function pruefeSignatur(dateipfad: string): Promise<Signaturbefund> {
+  if (process.platform !== 'win32') {
+    return NICHT_GEPRUEFT;
+  }
+  const skript = [
+    '$befund = Get-AuthenticodeSignature -LiteralPath $env:ZU_PRUEFENDE_DATEI;',
+    '$name = if ($befund.SignerCertificate) { $befund.SignerCertificate.Subject } else { "" };',
+    'ConvertTo-Json -Compress @{ status = $befund.Status.ToString(); herausgeber = $name }',
+  ].join(' ');
+  try {
+    const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-NonInteractive', '-Command', skript], {
+      timeout: 60000,
+      windowsHide: true,
+      env: { ...process.env, ZU_PRUEFENDE_DATEI: dateipfad },
+    });
+    const roh = JSON.parse(stdout) as { status?: string; herausgeber?: string };
+    return {
+      status: roh.status ?? 'nicht geprüft',
+      herausgeber: lesbarerHerausgeber(roh.herausgeber ?? ''),
+    };
+  } catch {
+    return NICHT_GEPRUEFT; // Prüfung selbst fehlgeschlagen — kein Grund, den Abgleich zu verwerfen
+  }
+}
+
+/** Zieht aus dem Zertifikats-Betreff den sprechenden Teil (CN bzw. erstes Feld). */
+function lesbarerHerausgeber(betreff: string): string {
+  if (!betreff) {
+    return '';
+  }
+  const felder = betreff.split(',').map((feld) => feld.trim());
+  const gemeinsamerName = felder.find((feld) => feld.startsWith('CN='));
+  return (gemeinsamerName ?? felder[0]).replace(/^CN=/, '').replace(/^"|"$/g, '');
+}
+
+/** Weicht der gefundene Herausgeber von der Erwartung ab? Ohne Erwartung nie. */
+export function weichtHerausgeberAb(erwartet: string, gefunden: string): boolean {
+  if (!erwartet.trim()) {
+    return false;
+  }
+  return !gefunden.toLowerCase().includes(erwartet.trim().toLowerCase());
+}

--
Gitblit v1.9.3