commit 63e795da8146a1d482efcd49d6a2e58ebd9b1b8b Author: LD-Reborn Date: Sun Jun 28 02:34:50 2026 +0200 initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..42877c5 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# ── PEM files (recommended for Let's Encrypt / certbot) ── +CERT_FILE=fullchain6.pem +PRIVKEY_FILE=privkey.pem +# Leave empty if the private key has no passphrase +PRIVKEY_PASSWORD= + +# ── Alternative: PKCS#12 ── +# PKCS12_FILE=certificate.p12 +# PKCS12_PASSWORD=changeit + +# ── Signature metadata ── +SIGNATURE_NAME=John Doe +SIGNATURE_REASON=Approved +SIGNATURE_LOCATION=Berlin, Germany +SIGNATURE_CONTACT=john@example.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24c7f08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +vendor/ +.env +*.p12 +*.key +*.crt +*.pem +signature.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..11ff579 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# DropSign + +Drag & Drop – PDFs cryptographically signieren. + +Ein PHP-Einzeldatei-Tool, das per Drag & Drop hochgeladene PDFs mit einem X.509-Zertifikat digital signiert und das signierte PDF zum Download bereitstellt. + +## Voraussetzungen + +- PHP ≥ 7.4 mit den Extensions `openssl`, `gd`, `mbstring` +- Composer +- Ein gültiges Signaturzertifikat (PKCS#12 `.p12` oder separiert als PEM) + +## Installation + +```bash +git clone dropsign +cd dropsign +composer install +``` + +## Konfiguration + +`.env` anlegen (oder `.env.example` kopieren und anpassen): + +### Variante A – PEM (getrennt) + +```env +CERT_FILE=fullchain6.pem +PRIVKEY_FILE=privkey.pem +PRIVKEY_PASSWORD= +``` + +### Variante B – PKCS#12 (z. B. von Let's Encrypt / Hausverwaltung) + +```env +PKCS12_FILE=certificate.p12 +PKCS12_PASSWORD=dein-password +``` + +### Signatur-Metadaten (optional) + +```env +SIGNATURE_NAME=John Doe +SIGNATURE_REASON=Approved +SIGNATURE_LOCATION=Berlin, Germany +SIGNATURE_CONTACT=john@example.com +``` + +## Verwendung + +### Entwicklung + +```bash +php -S localhost:8000 +``` + +→ Browser öffnen, PDF hineinziehen – signiertes PDF wird heruntergeladen. + +### Produktion (nginx) + +Dokumenten-Wurzel auf das Projektverzeichnis zeigen lassen, **nur `index.php` und `.pdf`-Dateien** freigeben: + +```nginx +server { + listen 443 ssl; + server_name dropsign.example.com; + + ssl_certificate /etc/letsencrypt/live/…/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/…/privkey.pem; + + root /pfad/zu/dropsign; + index index.php; + + location = / { + rewrite ^ /index.php last; + } + + location = /index.php { + try_files $uri =404; + include fastcgi_params; + fastcgi_pass unix:/var/run/php/php8.x-fpm.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location ~* \.pdf$ { + try_files $uri =404; + } + + location / { + deny all; + return 404; + } +} +``` + +> `.env`, `composer.json`, `vendor/`, `*.pem`, `*.key`, `*.p12` etc. sind damit automatisch geschützt. + +## Funktionsweise + +1. Der Benutzer zieht eine PDF-Datei per Drag & Drop in die Weboberfläche. +2. Die PDF wird per `fetch`-POST an `index.php` gesendet. +3. Das Script importiert jede Seite der Original-PDF via FPDI in TCPDF. +4. TCPDF signiert das neue PDF mit dem hinterlegten Zertifikat (PEM oder PKCS#12). +5. Das signierte PDF wird als `signed_.pdf` heruntergeladen. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..bac3f42 --- /dev/null +++ b/composer.json @@ -0,0 +1,10 @@ +{ + "name": "dropsign/dropsign", + "description": "Drag-and-drop PDF cryptographic signer", + "require": { + "php": ">=7.4", + "tecnickcom/tcpdf": "^6.7", + "setasign/fpdi-tcpdf": "^2.3", + "vlucas/phpdotenv": "^5.6" + } +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..d96e27e --- /dev/null +++ b/index.php @@ -0,0 +1,232 @@ +safeLoad(); + +$basePath = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/'); + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + ?> + + + + +DropSign + + + +

DropSign

+

Drop a PDF — get it cryptographically signed

+
+
🔐
+
Drag & drop a PDF here
+
or click to browse
+ +
+
+
Configure certificate in .env
+ + + + 'Upload failed']); + exit; +} + +$env = $_ENV; + +$certPem = $privKeyPem = $privKeyPass = ''; + +$certFile = $env['CERT_FILE'] ?? ''; +$keyFile = $env['PRIVKEY_FILE'] ?? ''; + +if ($certFile && $keyFile) { + $certPath = __DIR__ . '/' . $certFile; + $keyPath = __DIR__ . '/' . $keyFile; + $privKeyPass = $env['PRIVKEY_PASSWORD'] ?? ''; + + if (!file_exists($certPath)) { + http_response_code(500); + echo json_encode(['error' => 'Certificate not found: ' . $certFile]); + exit; + } + if (!file_exists($keyPath)) { + http_response_code(500); + echo json_encode(['error' => 'Private key not found: ' . $keyFile]); + exit; + } + + $certPem = file_get_contents($certPath); + $privKeyPem = file_get_contents($keyPath); +} else { + $p12Path = __DIR__ . '/' . ($env['PKCS12_FILE'] ?? 'certificate.p12'); + $p12Pass = $env['PKCS12_PASSWORD'] ?? ''; + + if (!file_exists($p12Path)) { + http_response_code(500); + echo json_encode(['error' => 'Certificate file not found. Set CERT_FILE+PRIVKEY_FILE or PKCS12_FILE in .env']); + exit; + } + + $p12Content = file_get_contents($p12Path); + if (!openssl_pkcs12_read($p12Content, $certs, $p12Pass)) { + http_response_code(500); + echo json_encode(['error' => 'Failed to read PKCS#12 certificate. Check password.']); + exit; + } + + $certPem = $certs['cert']; + $privKeyPem = $certs['pkey']; + $privKeyPass = $p12Pass; +} + +try { + $pdf = new Fpdi(); + $pageCount = $pdf->setSourceFile($_FILES['pdf']['tmp_name']); + + for ($i = 1; $i <= $pageCount; $i++) { + $tplId = $pdf->importPage($i); + $size = $pdf->getTemplateSize($tplId); + $orientation = ($size['width'] > $size['height']) ? 'L' : 'P'; + $pdf->AddPage($orientation, [$size['width'], $size['height']]); + $pdf->useTemplate($tplId); + } + + $pdf->setSignature( + $certPem, + $privKeyPem, + $privKeyPass, + '', // extracerts (chain already in fullchain6.pem) + 2, // cert_type (CMS) + [ + 'Name' => $env['SIGNATURE_NAME'] ?? '', + 'Location' => $env['SIGNATURE_LOCATION'] ?? '', + 'Reason' => $env['SIGNATURE_REASON'] ?? '', + 'ContactInfo' => $env['SIGNATURE_CONTACT'] ?? '', + ], + '' // approval + ); + + $outPath = tempnam(sys_get_temp_dir(), 'dropsign_') . '.pdf'; + $pdf->Output($outPath, 'F'); + + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; filename="signed_' . basename($_FILES['pdf']['name']) . '"'); + header('Content-Length: ' . filesize($outPath)); + readfile($outPath); + unlink($outPath); +} catch (\Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +}