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