Ein Partner will künftig prüfen können, dass ein Webhook wirklich aus Ihrem System stammt. Ein gemeinsames Passwort scheidet aus, weil damit auch der Empfänger Nachrichten fälschen könnte. Auf dem Shared Hosting gibt es keine Shell, das Erzeugen des Schlüssels muss also PHP selbst erledigen.
Im Bild teilt sich ein Schlüssel in zwei: einer wandert in den Safe, der andere geht nach außen. Genau diese Aufteilung ist der Kern des Verfahrens.
Was PHP openssl_pkey_new() erzeugt
PHP openssl_pkey_new() erzeugt ein vollständiges Schlüsselpaar und gibt es als ein einziges Objekt zurück. Darin stecken beide Hälften: der private Schlüssel, der niemals das Haus verlässt, und der öffentliche Schlüssel, den jeder bekommen darf. Diese Doppelnatur des Rückgabewerts verwirrt am Anfang, denn ein Aufruf liefert scheinbar nur einen Wert.
<?php
$paar = openssl_pkey_new([
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
if ($paar === false) {
exit('Schluessel konnte nicht erzeugt werden');
}
var_dump($paar instanceof OpenSSLAsymmetricKey); // bool(true)
Seit PHP 8.0 kommt ein Objekt der Klasse OpenSSLAsymmetricKey zurück. Vorher war es eine Resource. Wer älteren Code übernimmt, findet dort häufig eine Prüfung mit is_resource(), und die schlägt seit dem Versionswechsel immer fehl, obwohl der Schlüssel einwandfrei erzeugt wurde. Richtig ist der Vergleich mit false.
Die Signatur im Original
Die englischen Parameternamen aus dem Handbuch tauchen in Konfigurationsdateien und Fehlermeldungen wieder auf, deshalb hier die Beschreibung im Original:
openssl_pkey_new(?array $options = null): OpenSSLAsymmetricKey|false
/* options - configuration for the key generation, e.g. private_key_bits,
private_key_type and digest_alg. Values not set here fall back
to openssl.cnf of the installed openssl version.
return - an object holding the openssl key material, both the private
key and the public part, or false on failure
The same options array is accepted by openssl_csr_new() when a csr is
built from the key. Since PHP 8.0 the return value is an object; in an
older php version it was a resource. */
Der letzte Absatz ist für die Umstellung wichtig: Wer noch mit is_resource() prüft, bekommt seit PHP 8 immer ein negatives Ergebnis.
Der vollständige Ablauf in vier Schritten
Aus dem Objekt, das PHP openssl_pkey_new() zurückgibt, müssen beide Hälften herausgeholt werden, bevor sie sich speichern lassen. Für den privaten Teil ist openssl_pkey_export() zuständig, für den öffentlichen openssl_pkey_get_details(). Beide liefern PEM, also die Textform mit den bekannten Begrenzungszeilen.
<?php
$paar = openssl_pkey_new(['private_key_bits' => 2048]);
/* privaten Teil als PEM in eine Variable schreiben */
openssl_pkey_export($paar, $privatPem);
/* oeffentlichen Teil aus den Details holen */
$details = openssl_pkey_get_details($paar);
$oeffentlichPem = $details['key'];
file_put_contents('/pfad/privat.pem', $privatPem);
file_put_contents('/pfad/oeffentlich.pem', $oeffentlichPem);
echo substr($oeffentlichPem, 0, 27);
/* -----BEGIN PUBLIC KEY----- */
Die Regel dazu ist kurz und wichtig: Nur die Datei mit dem öffentlichen Schlüssel wird weitergegeben. Wer versehentlich den privaten Teil ausliefert, kann das Verfahren sofort einstellen, denn dann kann jeder in seinem Namen unterschreiben.
Den privaten Schlüssel mit einer Passphrase schützen
Das Ergebnis von PHP openssl_pkey_new() landet ohne dritten Parameter im Klartext auf der Platte, denn openssl_pkey_export() verschlüsselt nur, wenn eine Passphrase mitkommt. Auf einem Webserver, auf dem mehrere Projekte liegen, ist das eine schlechte Idee. Die Passphrase verschlüsselt den privaten Schlüssel innerhalb der Datei.
<?php
$paar = openssl_pkey_new(['private_key_bits' => 3072]);
$passphrase = getenv('SCHLUESSEL_PASSWORT');
openssl_pkey_export($paar, $privatPem, $passphrase);
file_put_contents('/pfad/privat.pem', $privatPem);
/* spaeter wieder laden */
$geladen = openssl_pkey_get_private(
file_get_contents('/pfad/privat.pem'),
$passphrase
);
if ($geladen === false) {
exit('Passphrase falsch oder Datei beschaedigt');
}
Die Passphrase gehört nicht in den Quelltext, sondern in eine Umgebungsvariable oder eine Konfigurationsdatei außerhalb des Web-Wurzelverzeichnisses. Damit ist der Schlüssel wenigstens nicht mit einem einzigen Dateizugriff verwertbar.
Signieren und prüfen
Ein Schlüsselpaar allein tut nichts. Der Zweck von PHP openssl_pkey_new() zeigt sich erst, wenn Daten damit unterschrieben und auf der Gegenseite geprüft werden. Das erledigen openssl_sign() und openssl_verify(). Das folgende Beispiel zeigt beide Fälle, den erfolgreichen und den absichtlich fehlschlagenden.
<?php
$paar = openssl_pkey_new(['private_key_bits' => 2048]);
openssl_pkey_export($paar, $privatPem);
$oeffentlich = openssl_pkey_get_details($paar)['key'];
$nachricht = '{"auftrag":4711,"betrag":"19.90"}';
openssl_sign($nachricht, $signatur, $privatPem, OPENSSL_ALGO_SHA256);
$uebertragen = base64_encode($signatur);
/* Empfaengerseite */
$geprueft = openssl_verify(
$nachricht,
base64_decode($uebertragen),
$oeffentlich,
OPENSSL_ALGO_SHA256
);
echo $geprueft; // 1
/* eine veraenderte Nachricht faellt sofort auf */
$manipuliert = '{"auftrag":4711,"betrag":"1.90"}';
echo openssl_verify(
$manipuliert,
base64_decode($uebertragen),
$oeffentlich,
OPENSSL_ALGO_SHA256
); // 0
Der Rückgabewert kennt drei Zustände: 1 für eine gültige Signatur, 0 für eine ungültige und -1 für einen Fehler. Wer nur auf true prüft, hält einen Fehler für eine gültige Unterschrift, denn -1 ist in PHP wahr. Die Prüfung gehört deshalb strikt auf === 1.
Die Optionen im Konfigurationsarray
Das Array, das PHP openssl_pkey_new() entgegennimmt, steuert Verfahren und Stärke. In der Praxis reichen zwei bis drei Einträge, die übrigen sind Sonderfälle.
| Option | Vorgabe | Bedeutung |
private_key_bits | 2048 | Schlüssellänge bei RSA. 2048 ist das Minimum, 3072 die vorsichtigere Wahl. Jede Verdopplung kostet spürbar Rechenzeit beim Erzeugen. |
private_key_type | RSA | OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_EC, OPENSSL_KEYTYPE_DSA oder OPENSSL_KEYTYPE_DH. |
curve_name | keine | Nur bei EC. Übliche Werte sind prime256v1 und secp384r1. |
config | Systempfad | Pfad zu einer eigenen openssl.cnf. Unter Windows häufig nötig. |
Ein Aufruf ganz ohne Array funktioniert und nimmt die Vorgaben. Für Produktivsysteme sollte die Schlüssellänge trotzdem ausdrücklich dastehen, damit sie beim nächsten Serverumzug nicht stillschweigend eine andere wird.
RSA oder EC?
Wer PHP openssl_pkey_new() aufruft, ohne den Typ zu setzen, bekommt RSA. Elliptische Kurven sind die modernere Alternative und in vielen Fällen die bessere Wahl.
| Merkmal | RSA 3072 | EC prime256v1 |
| Schlüsselgröße | groß, mehrere Kilobyte als PEM | klein, wenige hundert Byte |
| Erzeugungsdauer | spürbar, teils mehrere Sekunden | sehr kurz |
| Verbreitung | überall, auch in Altsystemen | modern, gelegentlich nicht unterstützt |
| Direkt verschlüsseln | möglich, aber größenbegrenzt | nicht vorgesehen, nur signieren |
<?php
$ec = openssl_pkey_new([
'private_key_type' => OPENSSL_KEYTYPE_EC,
'curve_name' => 'prime256v1',
]);
openssl_pkey_export($ec, $pem);
echo strlen($pem); // ungefaehr 300 statt mehrerer tausend Zeichen
Für Signaturen an einer Schnittstelle ist EC in aller Regel die bessere Wahl. Wer dagegen mit älteren Partnersystemen sprechen muss, bleibt bei RSA, weil dort die Unterstützung sicher vorhanden ist.
Wenn PHP openssl_pkey_new() false liefert
Der Aufruf schlägt still fehl und gibt false zurück. Die Ursache steht nicht in der PHP-Fehlermeldung, sondern in der Fehlerliste von OpenSSL, und die muss aktiv ausgelesen werden.
<?php
$paar = openssl_pkey_new(['private_key_bits' => 2048]);
if ($paar === false) {
$meldungen = [];
while (($fehler = openssl_error_string()) !== false) {
$meldungen[] = $fehler;
}
echo implode(PHP_EOL, $meldungen);
/* z.B. error:02001002:system library:fopen:No such file or directory */
}
Die Schleife ist wichtig, weil OpenSSL mehrere Meldungen stapelt. Ein einzelner Aufruf holt nur die älteste und lässt die aussagekräftige stehen. Der häufigste Fall auf Entwicklungsrechnern unter Windows ist eine fehlende openssl.cnf. Dann hilft die Option config mit einem eigenen Pfad, oder die Umgebungsvariable OPENSSL_CONF zeigt auf die mitgelieferte Datei.
Was asymmetrische Schlüssel nicht können
Ein Schlüssel aus PHP openssl_pkey_new() verschlüsselt bei RSA nur wenig Daten auf einmal, bei 2048 Bit gut 200 Byte. Eine PDF-Datei damit direkt zu verschlüsseln, funktioniert nicht. In der Praxis wird deshalb ein zufälliger symmetrischer Schlüssel erzeugt, die Nutzdaten damit verschlüsselt und nur dieser kurze Schlüssel asymmetrisch geschützt. Wie die symmetrische Hälfte aussieht, zeigt das Tutorial zu PHP openssl_encrypt() und dem sicheren Verschlüsseln von Daten. Wer wissen will, was in einem fertigen Zertifikat steht, findet das im Tutorial zu PHP openssl_x509_parse().
<?php
/* Hybridverfahren: symmetrisch fuer die Daten, asymmetrisch fuer den Schluessel */
$sitzungsschluessel = random_bytes(32);
$iv = random_bytes(openssl_cipher_iv_length('aes-256-cbc'));
$chiffrat = openssl_encrypt($grosseDaten, 'aes-256-cbc',
$sitzungsschluessel, OPENSSL_RAW_DATA, $iv);
/* nur die 32 Byte gehen asymmetrisch an den Empfaenger */
openssl_public_encrypt($sitzungsschluessel, $geschuetzt, $oeffentlichPem);
$paket = base64_encode($iv . $geschuetzt . $chiffrat);
Nur der kurze Sitzungsschlüssel wird asymmetrisch geschützt, die eigentlichen Nutzdaten laufen über das schnellere symmetrische Verfahren. So arbeiten auch TLS und verschlüsselte E-Mail.
Ebenso wenig gehören Anmeldedaten in dieses Verfahren. Passwörter werden nicht verschlüsselt, sondern gehasht, und dafür ist PHP password_hash() zuständig. Wer beides vermischt, baut sich eine Schwachstelle, die sich später nur schwer wieder ausbauen lässt.
Der folgende Ablauf fasst zusammen, welcher Aufruf welchen Teil liefert und wo die beiden Wege wieder zusammenkommen.
flowchart TD
A[openssl_pkey_new] --> B[Schluesselobjekt]
B --> C[openssl_pkey_export]
B --> D[openssl_pkey_get_details]
C --> E[privat.pem mit Passphrase]
D --> F[oeffentlich.pem]
E --> G[openssl_sign]
F --> H[openssl_verify]
G --> H
Der private Zweig bleibt im eigenen System, der öffentliche geht zur Gegenstelle. Erst bei der Prüfung treffen sich beide wieder.
Ein vollständiges Beispiel für signierte Webhooks
Damit steht alles zusammen, was für den Fall aus der Einleitung nötig ist. Der Aufruf von PHP openssl_pkey_new() passiert genau einmal, alles Weitere arbeitet mit den beiden Dateien. Der Sender hängt die Signatur als Header an, der Empfänger prüft sie gegen den öffentlichen Schlüssel.
<?php
/* einmalig: Paar erzeugen und ablegen */
function schluesselErzeugen(string $verzeichnis, string $passphrase): void
{
$paar = openssl_pkey_new([
'private_key_type' => OPENSSL_KEYTYPE_EC,
'curve_name' => 'prime256v1',
]);
if ($paar === false) {
throw new RuntimeException('Schluessel nicht erzeugbar');
}
openssl_pkey_export($paar, $privat, $passphrase);
$oeffentlich = openssl_pkey_get_details($paar)['key'];
file_put_contents($verzeichnis . '/privat.pem', $privat);
file_put_contents($verzeichnis . '/oeffentlich.pem', $oeffentlich);
chmod($verzeichnis . '/privat.pem', 0600);
}
/* bei jedem Versand */
function signatur(string $rumpf, string $pfad, string $passphrase): string
{
$key = openssl_pkey_get_private(file_get_contents($pfad), $passphrase);
if ($key === false) {
throw new RuntimeException('Privater Schluessel nicht ladbar');
}
openssl_sign($rumpf, $roh, $key, OPENSSL_ALGO_SHA256);
return base64_encode($roh);
}
Die Zeile mit chmod() ist kein Beiwerk. Auf einem Server mit mehreren Konten entscheidet sie darüber, ob der private Schlüssel für andere lesbar ist. Wer HTTP-Header für die Übertragung braucht, findet die Grundlagen im Tutorial zu HTTP-Headern mit PHP.
Fazit
PHP openssl_pkey_new() erzeugt in einem Aufruf ein Paar, dessen Hälften getrennte Wege gehen. Der private Teil bleibt mit Passphrase und engen Dateirechten im eigenen System, der öffentliche darf frei weitergegeben werden. Erst zusammen mit openssl_sign() und openssl_verify() entsteht daraus ein nutzbares Verfahren, und die strikte Prüfung auf === 1 entscheidet darüber, ob es auch hält.
Wer heute neu anfängt, wählt EC statt RSA, setzt die Optionen ausdrücklich und liest im Fehlerfall openssl_error_string() in einer Schleife aus. Damit sind die drei Stellen abgedeckt, an denen die Arbeit mit PHP openssl_pkey_new() üblicherweise stecken bleibt.