mailwolt/app/Services/DkimService.php

416 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Services;
use App\Models\DkimKey;
use App\Models\Domain;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use RuntimeException;
class DkimService
{
/** Erzeugt Keypair & gibt den TXT-Record (ohne Host) zurück. */
// public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array
// {
// // 1) Selector zentral aus der Config (Fallback 'mwl1')
// $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
//
// $dirKey = $this->safeKey($domain);
// $selKey = $this->safeKey($selector, 32);
//
// $disk = Storage::disk('local');
// $baseRel = "dkim/{$dirKey}";
// $privRel = "{$baseRel}/{$selKey}.pem";
// $pubRel = "{$baseRel}/{$selKey}.pub";
//
// // 2) Idempotent: existiert das Paar schon? -> nur lesen & zurückgeben
// if ($disk->exists($privRel) && $disk->exists($pubRel)) {
// $privateKey = $disk->get($privRel);
// $publicKeyPem = $disk->get($pubRel);
// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem);
// if (strlen($publicKeyBase) < 300) {
// throw new \RuntimeException('DKIM: Public Key zu kurz vermutlich Parsing-Fehler.');
// }
// return [
// 'selector' => $selKey,
// 'priv_path' => storage_path("app/{$privRel}"),
// 'pub_path' => storage_path("app/{$pubRel}"),
// 'public_pem' => $publicKeyPem,
// 'private_pem' => $privateKey,
// 'dns_name' => "{$selKey}._domainkey",
// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}",
// 'bits' => $bits,
// ];
// }
//
// // 3) Sonst neu generieren
// $disk->makeDirectory($baseRel);
//
// $res = openssl_pkey_new([
// 'private_key_type' => OPENSSL_KEYTYPE_RSA,
// 'private_key_bits' => $bits,
// ]);
// if ($res === false) {
// throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
// }
//
// $privateKey = '';
// if (!openssl_pkey_export($res, $privateKey)) {
// throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
// }
//
// $details = openssl_pkey_get_details($res);
// if ($details === false || empty($details['key'])) {
// throw new \RuntimeException('DKIM: Public Key konnte nicht gelesen werden.');
// }
// $publicKeyPem = $details['key'];
// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem);
// if (strlen($publicKeyBase) < 300) {
// throw new \RuntimeException('DKIM: Public Key zu kurz vermutlich Parsing-Fehler.');
// }
//
// if (!$disk->put($privRel, $privateKey)) {
// throw new \RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privRel}");
// }
// if (!$disk->put($pubRel, $publicKeyPem)) {
// throw new \RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubRel}");
// }
//
// // 4) Rückgabe
// return [
// 'selector' => $selKey,
// 'priv_path' => storage_path("app/{$privRel}"),
// 'pub_path' => storage_path("app/{$pubRel}"),
// 'public_pem' => $publicKeyPem,
// 'private_pem' => $privateKey,
// 'dns_name' => "{$selKey}._domainkey",
// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}",
// 'bits' => $bits,
// ];
// }
// public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array
// {
// // 1) Selector zentral aus der Config (Fallback 'mwl1')
// $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
//
// $dirKey = $this->safeKey($domain);
// $selKey = $this->safeKey($selector, 32);
//
// $disk = Storage::disk('local');
// $baseRel = "dkim/{$dirKey}";
// $privRel = "{$baseRel}/{$selKey}.pem";
// $pubRel = "{$baseRel}/{$selKey}.pub";
//
// $privAbs = method_exists($disk, 'path')
// ? $disk->path($privRel) // -> /var/www/mailwolt/storage/app/private/dkim/<id>/<selector>.pem
// : storage_path('app/private/'.$privRel); // Fallback falls 'path' nicht existiert
//
// $pubAbs = method_exists($disk, 'path')
// ? $disk->path($pubRel)
// : storage_path('app/private/'.$pubRel);
//
// // 2) Idempotent: existiert das Paar schon?
// if ($disk->exists($privRel) && $disk->exists($pubRel)) {
// $privateKey = $disk->get($privRel);
// $publicKeyPem = $disk->get($pubRel);
// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem);
// if (strlen($publicKeyBase) < 300) {
// throw new \RuntimeException('DKIM: Public Key zu kurz vermutlich Parsing-Fehler.');
// }
// return [
// 'selector' => $selKey,
// 'priv_path' => $privAbs,
// 'pub_path' => $pubAbs,
// 'public_pem' => $publicKeyPem,
// 'private_pem' => $privateKey,
// 'dns_name' => "{$selKey}._domainkey",
// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}",
// 'bits' => $bits,
// ];
// }
//
// // 3) Neu generieren
// $disk->makeDirectory($baseRel);
//
// $res = openssl_pkey_new([
// 'private_key_type' => OPENSSL_KEYTYPE_RSA,
// 'private_key_bits' => $bits,
// ]);
// if ($res === false) {
// throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
// }
//
// $privateKey = '';
// if (!openssl_pkey_export($res, $privateKey)) {
// throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
// }
//
// $details = openssl_pkey_get_details($res);
// if ($details === false || empty($details['key'])) {
// throw new \RuntimeException('DKIM: Public Key konnte nicht gelesen werden.');
// }
// $publicKeyPem = $details['key'];
// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem);
// if (strlen($publicKeyBase) < 300) {
// throw new \RuntimeException('DKIM: Public Key zu kurz vermutlich Parsing-Fehler.');
// }
//
// if (!$disk->put($privRel, $privateKey)) {
// throw new \RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privRel}");
// }
// if (!$disk->put($pubRel, $publicKeyPem)) {
// throw new \RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubRel}");
// }
//
// return [
// 'selector' => $selKey,
// 'priv_path' => $privAbs,
// 'pub_path' => $pubAbs,
// 'public_pem' => $publicKeyPem,
// 'private_pem' => $privateKey,
// 'dns_name' => "{$selKey}._domainkey",
// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}",
// 'bits' => $bits,
// ];
// }
// public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array
// {
// // 1) Selector (Fallback mwl1)
// $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
//
// $dirKey = $this->safeKey($domain);
// $selKey = $this->safeKey($selector, 32);
//
// $disk = Storage::disk('local'); // root: /var/www/mailwolt/storage/app/private
// $baseRel = "dkim/{$dirKey}";
// $privRel = "{$baseRel}/{$selKey}.pem";
// $pubRel = "{$baseRel}/{$selKey}.pub";
//
// // Absolute Pfade (robust gegen geändertes Disk-Root)
// $privAbs = method_exists($disk, 'path') ? $disk->path($privRel) : storage_path('app/private/'.$privRel);
// $pubAbs = method_exists($disk, 'path') ? $disk->path($pubRel) : storage_path('app/private/'.$pubRel);
//
// // 2) Idempotent: existiert das Paar schon?
// if ($disk->exists($privRel) && $disk->exists($pubRel)) {
// $privateKey = $disk->get($privRel); // ← Inhalte laden, nicht Pfade!
// $publicKeyPem = $disk->get($pubRel);
// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem);
// if (strlen($publicKeyBase) < 300) {
// throw new \RuntimeException('DKIM: Public Key zu kurz vermutlich Parsing-Fehler.');
// }
// return [
// 'selector' => $selKey,
// 'priv_path' => $privAbs,
// 'pub_path' => $pubAbs,
// 'public_pem' => $publicKeyPem,
// 'private_pem' => $privateKey,
// 'dns_name' => "{$selKey}._domainkey",
// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}",
// 'bits' => $bits,
// ];
// }
//
// // 3) Neu generieren
// $disk->makeDirectory($baseRel);
//
// $res = openssl_pkey_new([
// 'private_key_type' => OPENSSL_KEYTYPE_RSA,
// 'private_key_bits' => $bits,
// ]);
// if ($res === false) {
// throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
// }
//
// $privateKey = '';
// if (!openssl_pkey_export($res, $privateKey)) {
// throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
// }
//
// $details = openssl_pkey_get_details($res);
// if ($details === false || empty($details['key'])) {
// throw new \RuntimeException('DKIM: Public Key konnte nicht gelesen werden.');
// }
// $publicKeyPem = $details['key'];
// $publicKeyBase = self::extractPublicKeyBase64($publicKeyPem);
// if (strlen($publicKeyBase) < 300) {
// throw new \RuntimeException('DKIM: Public Key zu kurz vermutlich Parsing-Fehler.');
// }
//
// if (!$disk->put($privRel, $privateKey)) {
// throw new \RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privRel}");
// }
// if (!$disk->put($pubRel, $publicKeyPem)) {
// throw new \RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubRel}");
// }
//
// return [
// 'selector' => $selKey,
// 'priv_path' => $privAbs,
// 'pub_path' => $pubAbs,
// 'public_pem' => $publicKeyPem,
// 'private_pem' => $privateKey,
// 'dns_name' => "{$selKey}._domainkey",
// 'dns_txt' => "v=DKIM1; k=rsa; p={$publicKeyBase}",
// 'bits' => $bits,
// ];
// }
public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array
{
$selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
$dirKey = $this->safeKey($domain);
$selKey = $this->safeKey($selector, 32);
// Disk "local" zeigt bei dir auf storage/app/private (siehe Kommentar in deinem Code)
$disk = Storage::disk('local');
$baseRel = "dkim/{$dirKey}";
// unsere Dateien (alle Varianten, damit sowohl App als auch OpenDKIM glücklich sind)
$privPemRel = "{$baseRel}/{$selKey}.pem"; // PKCS#1/PKCS#8 (App-Backup)
$pubPemRel = "{$baseRel}/{$selKey}.pub"; // Public PEM (App-Backup)
$privOKRel = "{$baseRel}/{$selKey}.private"; // <-- OpenDKIM erwartet *genau das*
$dnsTxtRel = "{$baseRel}/{$selKey}.txt"; // TXT-String "v=DKIM1; k=rsa; p=..."
// Absolute Pfade
$privPemAbs = method_exists($disk, 'path') ? $disk->path($privPemRel) : storage_path('app/private/'.$privPemRel);
$pubPemAbs = method_exists($disk, 'path') ? $disk->path($pubPemRel) : storage_path('app/private/'.$pubPemRel);
$privOKAbs = method_exists($disk, 'path') ? $disk->path($privOKRel) : storage_path('app/private/'.$privOKRel);
$dnsTxtAbs = method_exists($disk, 'path') ? $disk->path($dnsTxtRel) : storage_path('app/private/'.$dnsTxtRel);
// Wenn schon vollständig vorhanden → direkt zurück
if ($disk->exists($privOKRel) && $disk->exists($dnsTxtRel)) {
$privatePem = $disk->exists($privPemRel) ? $disk->get($privPemRel) : $disk->get($privOKRel);
$publicPem = $disk->exists($pubPemRel) ? $disk->get($pubPemRel) : null;
$dnsTxt = $disk->get($dnsTxtRel);
return [
'selector' => $selKey,
'priv_path' => $privOKAbs,
'pub_path' => $pubPemAbs,
'private_pem' => $privatePem,
'public_pem' => $publicPem,
'dns_name' => "{$selKey}._domainkey",
'dns_txt' => $dnsTxt,
'bits' => $bits,
];
}
// Neu generieren
$disk->makeDirectory($baseRel);
$res = openssl_pkey_new([
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'private_key_bits' => $bits,
]);
if ($res === false) {
throw new \RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
}
$privatePem = '';
if (!openssl_pkey_export($res, $privatePem)) {
throw new \RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
}
$details = openssl_pkey_get_details($res);
if ($details === false || empty($details['key'])) {
throw new \RuntimeException('DKIM: Public Key konnte nicht gelesen werden.');
}
$publicPem = $details['key'];
$publicBase64 = trim(preg_replace('/-----(BEGIN|END)\s+PUBLIC KEY-----|\s+/', '', $publicPem));
if (strlen($publicBase64) < 300) {
throw new \RuntimeException('DKIM: Public Key zu kurz vermutlich Parsing-Fehler.');
}
$dnsTxt = "v=DKIM1; k=rsa; p={$publicBase64}";
// Dateien schreiben (App + OpenDKIM)
if (!$disk->put($privPemRel, $privatePem)) throw new \RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privPemRel}");
if (!$disk->put($pubPemRel, $publicPem)) throw new \RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubPemRel}");
if (!$disk->put($privOKRel, $privatePem)) throw new \RuntimeException("DKIM: OpenDKIM-Key schreiben fehlgeschlagen: {$privOKRel}");
if (!$disk->put($dnsTxtRel, $dnsTxt)) throw new \RuntimeException("DKIM: DNS-TXT schreiben fehlgeschlagen: {$dnsTxtRel}");
// Dateirechte (best effort)
@chmod($privPemAbs, 0600);
@chmod($privOKAbs, 0600);
@chmod($pubPemAbs, 0640);
@chmod($dnsTxtAbs, 0644);
// DB pflegen
DkimKey::updateOrCreate(
['domain_id' => $domain->id, 'selector' => $selKey],
[
'private_key_pem' => $privatePem,
'public_key_txt' => $publicBase64,
'is_active' => true,
]
);
// OpenDKIM einhängen (wenn Helper existiert)
if (is_executable('/usr/local/sbin/mailwolt-install-dkim')) {
$res = Process::run([
'/usr/local/sbin/mailwolt-install-dkim',
$domain->domain,
$selKey,
$privOKAbs, // <-- .private (genau das will der Helper)
$dnsTxtAbs // optional: TXT-Datei, falls dein Helper das nutzt
]);
if ($res->failed()) {
throw new \RuntimeException("OpenDKIM-Install fehlgeschlagen: {$res->error()}");
}
// OpenDKIM neu laden
Process::run(['systemctl', 'reload', 'opendkim']);
}
return [
'selector' => $selKey,
'priv_path' => $privOKAbs,
'pub_path' => $pubPemAbs,
'private_pem' => $privatePem,
'public_pem' => $publicPem,
'dns_name' => "{$selKey}._domainkey",
'dns_txt' => $dnsTxt,
'bits' => $bits,
];
}
protected function safeKey($value, int $max = 64): string
{
if (is_object($value)) {
if (isset($value->id)) $value = $value->id;
elseif (method_exists($value, 'getKey')) $value = $value->getKey();
else $value = json_encode($value);
}
$raw = (string) $value;
$san = preg_replace('/[^A-Za-z0-9._-]/', '_', $raw);
if ($san === '' ) $san = 'unknown';
if (strlen($san) > $max) {
$san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12);
}
return $san;
}
protected static function extractPublicKeyBase64(string $pem): string
{
// Hole den Body zwischen den Headern (multiline, dotall)
if (!preg_match('/^-+BEGIN PUBLIC KEY-+\r?\n(.+?)\r?\n-+END PUBLIC KEY-+\s*$/ms', $pem, $m)) {
throw new \RuntimeException('DKIM: Ungültiges Public-Key-PEM (Header/Footers nicht gefunden).');
}
// Whitespace entfernen → reines Base64
$b64 = preg_replace('/\s+/', '', $m[1]);
if ($b64 === '' || base64_decode($b64, true) === false) {
throw new \RuntimeException('DKIM: Public Key Base64 ist leer/ungültig.');
}
return $b64;
}
}