Navigation
 Startseite
 Fachbücher
 Anzeigenmarkt
 Forum
 Webmaster News
 Script Newsletter
 Push Meldungen
 Kontakt
 Script Installation
 Php
 Php Tutorials
 Lernpfade
 Webhoster Vergleich
 Impressum

Community-Bereich
 kostenlos Registrieren
 Anmelden
 Benutzerliste

Script Datenbank
 Script Archiv
 Script Top 20
 Screenshots
 Testberichte

Suche
 

Unsere Php Scripts
 Counter Script
 Umfrage Script
 Bilder Upload Script
 Terminverwaltung
 Simple PHP Forum
 RSS Grabber

Tools und Generatoren
 .htpasswd Generator
 md5 Generator
 base64 Generator
 Markdown to HTML
 Colorpicker
 Unix timestamp Tool
 Unit Test Generator
 TLD Liste
 Webkatalog‑Verzeichnis

Webspace-Mieten.d...
Webhosting mit Plesk aus Erfurt: Vier...
https://www.Webspace-Mieten.de
Hosterplus.de
Bekommen Sie Speicherplatz (Webspace), Domains...
https://www.Hosterplus.de
 
 
 

PHP openssl_csr_new(): CSR erzeugen und selbst signieren

Sie befinden sich: Home > Php Tutorial > PHP openssl_csr_new(): CSR ...

PHP openssl_csr_new(): CSR erzeugen und selbst signieren
Veröffentlicht am:
11.11.2026
Hits / Besucher:
13
Sprache:
  Deutsch
Tutorial Art:
eigenes
Eingetragen von:
Merkliste:
 
Beschreibung

Für das interne Kundenportal soll ein Zertifikat her, und die Zertifizierungsstelle verlangt dafür eine CSR. Auf dem Server gibt es keine Konsole, also muss PHP die Anfrage selbst bauen. Wie ein Schlüsselpaar entsteht, zeigt das Tutorial zu openssl_pkey_new(), was in einem fertigen Zertifikat steht, das Tutorial zu openssl_x509_parse(). Hier geht es um den Schritt dazwischen: die Anfrage, mit der aus einem Schlüssel ein Zertifikat wird, gebaut mit PHP openssl_csr_new().

Illustration zum Tutorial: PHP openssl_csr_new(): CSR erzeugen und selbst signieren

Die Beispiele zu PHP openssl_csr_new() liefen unter Windows mit OpenSSL 3.0 und unter Linux mit OpenSSL 3.5, beide mit PHP 8.4. Geprüft wurden die erzeugten Dateien zusätzlich mit dem Kommandozeilenwerkzeug openssl.

Was PHP openssl_csr_new() erzeugt

Eine CSR (Certificate Signing Request) ist ein Antrag, noch kein Zertifikat. Sie enthält den öffentlichen Schlüssel, den Namen des Antragstellers und auf Wunsch Erweiterungen wie die Liste der Hostnamen. Signiert wird sie mit dem privaten Schlüssel. Diese Signatur beweist der Zertifizierungsstelle, dass der Antragsteller den passenden privaten Schlüssel besitzt, ohne dass dieser das Haus verlässt. Den Namen beschreibt ein Distinguished Name (DN) aus Feldern wie Land, Organisation und Common Name, ein Format, das aus X.500 und LDAP stammt. PHP openssl_csr_new() nimmt diese Felder als assoziatives Array entgegen.

Wer die Anfrage am Ende unterschreibt, entscheidet über den weiteren Weg:

flowchart TD
    A[Privater Schlüssel] --> C[openssl_csr_new]
    B[DN-Array und Konfiguration] --> C
    C --> D[CSR als PEM]
    D --> E{Wer signiert?}
    E -->|öffentliche CA| F[Zertifikat der CA]
    E -->|selbst| G[openssl_csr_sign ohne CA]
    E -->|eigene CA| H[openssl_csr_sign mit CA]
    F --> I[Zertifikat prüfen]
    G --> I
    H --> I

Die Signatur im Original: Parameter und Rückgabewert

PHP openssl_csr_new() hat zwei Pflichtparameter und zwei Arrays für Feinheiten:

openssl_csr_new(

array $distinguished_names,
OpenSSLAsymmetricKey &$private_key,
?array $options = null,
?array $extra_attributes = null
): OpenSSLCertificateSigningRequest|bool

/* distinguished_names - associative array with
the fields of the subject, e.g. commonName
private_key - the private key that signs the
request; if null, a new key is generated
options - config, digest_alg, req_extensions;
missing values are read from the
openssl.cnf configuration file
extra_attributes - additional attributes of
the request, e.g. challengePassword
return - the csr as an object, true if the
request was created but signing failed,
false on failure.
Sign the csr with openssl_csr_sign() to
generate a certificate, e.g. for an ssl
server. */

Das Ergebnis von PHP openssl_csr_new() ist seit PHP 8 ein Objekt der Klasse OpenSSLCertificateSigningRequest. Ungewöhnlich ist der Rückgabetyp mit bool: Neben false für einen Fehler gibt es true, wenn die Anfrage gebaut, aber nicht signiert werden konnte. Im Test passierte das mit einem Ed25519-Schlüssel und dem voreingestellten Hash-Verfahren. Eine Prüfung mit !== false reicht deshalb nicht, is_object() ist der sichere Weg. Ist $private_key vor dem Aufruf null, erzeugt PHP openssl_csr_new() selbst einen Schlüssel und schreibt ihn in die Variable, im Test ein RSA-Schlüssel mit 2048 Bit.

Der vollständige Ablauf: vom Schlüssel zur PEM-Datei

Mit einem vorhandenen Schlüssel, dem DN-Array und einer kleinen Konfigurationsdatei baut PHP openssl_csr_new() die Anfrage in wenigen Zeilen. Den Inhalt von csr.cnf zeigt der übernächste Abschnitt.

<?php

$cnf = __DIR__ . '/csr.cnf';

$schluessel = openssl_pkey_new([
'private_key_bits' => 2048,
'config' => $cnf,
]);
$dn = [
'countryName' => 'DE',
'stateOrProvinceName' => 'Nordrhein-Westfalen',
'localityName' => 'Köln',
'organizationName' => 'Beispiel GmbH',
'commonName' => 'www.beispiel.de',
];
$csr = openssl_csr_new($dn, $schluessel, [
'config' => $cnf,
'req_extensions' => 'san',
]);
if (!is_object($csr)) {
exit('CSR fehlgeschlagen');
}
openssl_csr_export($csr, $pem);
file_put_contents(__DIR__ . '/anfrage.csr', $pem);
echo strtok($pem, "\n");
/* -----BEGIN CERTIFICATE REQUEST----- */

openssl_csr_export() schreibt den PEM-Block in die Variable, openssl_csr_export_to_file() direkt in eine Datei. Die Anfrage war im Test 1090 Bytes groß. Mit false als drittem Parameter kommt eine lesbare Zusammenfassung dazu, die mit Certificate Request: beginnt. Diese Datei geht an die Zertifizierungsstelle, der private Schlüssel bleibt auf dem Server. Wie er mit einer Passphrase gesichert wird, steht im Tutorial zu openssl_pkey_new().

Das DN-Array: Felder, Kurznamen und Grenzen

Für die Felder des DN-Arrays akzeptiert PHP openssl_csr_new() Langnamen und Kurznamen, beide funktionierten im Test. openssl_csr_get_subject() liefert die Kurzform zurück, mit false als zweitem Parameter die Langform. Umlaute in UTF-8 bleiben erhalten, Köln stand auch in der Ausgabe von openssl req korrekt da. Mehrere Werte für ein Feld, etwa zwei Abteilungen, gehen als Array.

Feld (Kurzname) Verhalten im Test
countryName (C) genau zwei Zeichen, DEU scheitert, kleines de geht durch
commonName (CN) 64 Zeichen gehen, 65 scheitern
organizationName (O) ein leerer String scheitert, besser weglassen
organizationalUnitName (OU) mehrere Werte als Array erlaubt
emailAddress (ohne Kurzname) angenommen, ein Umlaut in der Adresse scheitert
unbekannter Name, etwa firmenname Warnung, das Feld fehlt, die Anfrage entsteht trotzdem

Scheitert ein Feld, meldet PHP eine Warnung wie openssl_csr_new(): dn: add_entry_by_NID 14 -> DEU (failed; check error queue ...). Die Zahl ist die interne Kennung des Feldes, 14 steht für das Land, 13 für den Common Name. In der Fehlerliste von OpenSSL steht dazu string too long oder bei einem leeren Wert string too short. Wer Felder aus einem Formular übernimmt, prüft Länge und Leerwerte also vorher.

Das Feld emailAddress heißt auch in der Kurzform so und ist laut RFC 5280 im Namen nur noch geduldet. Ein S/MIME-Zertifikat für signierte E-Mails trägt die Adresse stattdessen im SAN. In der Konfigurationsdatei aus dem nächsten Abschnitt genügt dafür eine zusätzliche Zeile email.1 = mia@beispiel.de im Abschnitt namen. Im Test stand sie danach als email:mia@beispiel.de neben den Hostnamen in der Anfrage. Weil der Abschnitt server weiter unten dieselbe Liste namen verwendet, landet die Adresse dann auch im SAN des Serverzertifikats. Wer das nicht will, legt für die E-Mail einen eigenen Abschnitt an.

Vorgaben aus der openssl.cnf: AU, Some-State und SHA-1

Ohne eigene Konfiguration greift PHP openssl_csr_new() auf die openssl.cnf des Systems zurück, und die hält zwei Überraschungen bereit. Die erste betrifft fehlende Felder, gemessen unter Linux mit der Standarddatei:

<?php

$nurCn = openssl_csr_new(
['commonName' => 'nur-cn.de'],
$schluessel
);
print_r(openssl_csr_get_subject($nurCn));
/* [CN] => nur-cn.de
[C] => AU
[ST] => Some-State
[O] => Internet Widgits Pty Ltd */

Die Werte stammen aus dem Abschnitt [ req_distinguished_name ] der Standarddatei, der für das Land AU, für das Bundesland Some-State und für die Firma Internet Widgits Pty Ltd vorgibt. PHP übernimmt jeden dieser Vorgabewerte, dessen Feld im Array fehlt. Die zweite Überraschung ist das Hash-Verfahren. Ohne digest_alg signierte PHP openssl_csr_new() die Anfrage auf beiden Systemen mit sha1WithRSAEncryption, denn der Standarddatei fehlt die Angabe default_md. Ein Tippfehler wie 'digest_alg' => 'sha265' fällt nicht auf, ein unbekannter Name landete im Test ohne Warnung ebenfalls bei SHA-1. SHA-1 gilt seit Jahren als veraltet.

Beides erledigt eine eigene, kleine Konfigurationsdatei. Ihr DN-Abschnitt ist leer, default_md legt SHA-256 fest, und die übrigen Abschnitte werden für die Hostnamen und das Signieren gebraucht:

[ req ]

distinguished_name = req_dn
default_md = sha256
default_bits = 2048
string_mask = utf8only

[ req_dn ]

[ san ]
subjectAltName = @namen

[ namen ]
DNS.1 = beispiel.de
DNS.2 = www.beispiel.de

[ server ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @namen

[ ca ]
basicConstraints = critical, CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash

Mit dieser Datei enthielt die Anfrage nur die übergebenen Felder und war mit sha256WithRSAEncryption signiert. Die Zeile default_bits hat einen Grund: Ohne sie scheiterte openssl_pkey_new() ohne eigene Bitangabe mit Private key length must be at least 384 bits, configured to 0, denn die Standarddatei liefert diesen Wert sonst mit. Unter Windows ist eine eigene Konfigurationsdatei sogar Pflicht. PHP sucht dort nach C:\Program Files\Common Files\SSL\openssl.cnf, eine Datei, die es auf dem Testrechner nicht gab. Ohne Option config lieferten openssl_pkey_new() und openssl_csr_new() nur false mit der Meldung configuration file routines::no such file. Wer die Umgebungsvariable OPENSSL_CONF vor dem Start von PHP setzt, hat das Problem ebenfalls nicht. Ein putenv() im Skript kam im Test dagegen zu spät.

Subject Alternative Names mit PHP openssl_csr_new()

Browser prüfen den Hostnamen gegen die Liste der Subject Alternative Names (SAN), nicht mehr gegen den Common Name. Eine eigene Option dafür kennt PHP openssl_csr_new() nicht. Wer 'subjectAltName' => 'DNS:beispiel.de' ins Optionsarray schreibt, bekommt keine Fehlermeldung und keinen SAN, denn unbekannte Schlüssel ignoriert PHP still. Der SAN gehört in einen eigenen Abschnitt der Konfiguration, und req_extensions nennt dessen Namen, im Ablauf oben san. Ein falscher Name wie 'gibtsnicht' brach mit Error loading request_extensions_section section gibtsnicht ab.

Wechseln die Hostnamen je Anfrage, schreibt eine Funktion die Konfiguration als temporäre Datei. Weil die Namen dabei als Zeilen in einer Datei landen, werden sie vorher geprüft. Sonst könnte ein Wert mit Zeilenumbruch eigene Abschnitte einschleusen:

<?php

function csr_config(array $hosts): string
{
$zeilen = [];
foreach (array_values($hosts) as $i => $host) {
$ok = filter_var(
$host,
FILTER_VALIDATE_DOMAIN,
FILTER_FLAG_HOSTNAME
);
if ($ok === false) {
throw new InvalidArgumentException($host);
}
$zeilen[] = 'DNS.' . ($i + 1) . ' = ' . $host;
}
$namen = implode(PHP_EOL, $zeilen);
$cnf = <<<CNF
[ req ]
distinguished_name = req_dn
default_md = sha256
[ req_dn ]
[ san ]
subjectAltName = @namen
[ namen ]
$namen
CNF;
$datei = tempnam(sys_get_temp_dir(), 'csr');
file_put_contents($datei, $cnf);
return $datei;
}

$tmp = csr_config(['shop.beispiel.de', 'beispiel.de']);
$csr2 = openssl_csr_new(
['commonName' => 'shop.beispiel.de'],
$schluessel,
['config' => $tmp, 'req_extensions' => 'san']
);
unlink($tmp);
echo is_object($csr2) ? 'CSR mit SAN' : 'Fehler';
/* CSR mit SAN */

Der Filter lehnte im Test einen Wert mit Zeilenumbruch ab, allerdings auch einen Platzhalter wie *.beispiel.de. Wildcard-Anfragen brauchen deshalb eine eigene Prüfung. Umlautdomains gehören in der Punycode-Form in die Anfrage: OpenSSL nahm DNS:müller.de zwar klaglos an, eine Zertifizierungsstelle erwartet aber xn--mller-kva.de. Statt einer temporären Datei geht auch ein fester Abschnitt mit subjectAltName = $ENV::SAN und ein putenv('SAN=DNS:beispiel.de') vor dem Aufruf. Fehlt die Variable, scheitert die Anfrage mit variable has no value.

Wenn PHP openssl_csr_new() false liefert

Fehler meldet PHP openssl_csr_new() auf zwei Kanälen. PHP selbst gibt Warnungen aus, etwa zu einem Feld des DN. Die Ursache aus Sicht von OpenSSL steht in einer Fehlerliste, die openssl_error_string() Meldung für Meldung ausliest:

<?php

while (openssl_error_string() !== false) {
}
$falsch = openssl_csr_new(
['countryName' => 'DEU', 'commonName' => 'x.de'],
$schluessel,
['config' => $cnf]
);
if ($falsch === false) {
while (($f = openssl_error_string()) !== false) {
echo $f, PHP_EOL;
}
}
/* error:06800097:asn1 encoding routines::
string too long */

Die Liste füllt sich auch dann, wenn alles klappt. Unter Linux stand nach einem erfolgreichen Schlüssel random number generator::Cannot open file darin, unter Windows mit der Option config bei jedem Aufruf drei Meldungen über die fehlende Standarddatei, sogar wenn die Liste direkt vorher geleert wurde. Ausgewertet wird sie deshalb nur, wenn PHP openssl_csr_new() false geliefert hat, und die aussagekräftige Meldung steht am Ende. Die ausführliche Schleife samt Hintergrund steht im Tutorial zu openssl_pkey_new().

Selbstsigniert mit openssl_csr_sign()

Für Testsysteme und lokale TLS-Server braucht es keine Zertifizierungsstelle. openssl_csr_sign() mit null als CA-Zertifikat signiert die Anfrage mit dem eigenen Schlüssel:

<?php

$zert = openssl_csr_sign(
$csr,
null,
$schluessel,
365,
['config' => $cnf, 'x509_extensions' => 'server'],
random_int(1, PHP_INT_MAX)
);
openssl_x509_export_to_file($zert, 'server.pem');
openssl_pkey_export_to_file(
$schluessel,
'server.key',
null,
['config' => $cnf]
);
$info = openssl_x509_parse($zert);
echo $info['extensions']['subjectAltName'];
/* DNS:beispiel.de, DNS:www.beispiel.de */

Die Schlüsseldatei ist so unverschlüsselt und war im Test unter Linux mit der üblichen umask für alle lesbar (0644). Ein chmod('server.key', 0600) direkt danach schränkt das auf den Eigentümer ein. Unter Windows änderte der Aufruf an den gemeldeten Rechten nichts, dort stand vorher und nachher 0666. Ein solches Zertifikat kann ein Testserver mit stream_socket_server() direkt ausliefern, im Test stand die Verbindung mit TLS 1.3. Die Parameter von openssl_csr_sign() haben ihre eigenen Fallen:

Fall Ergebnis im Test
keine Seriennummer angegeben Seriennummer 0
Anfrage mit SAN, aber ohne x509_extensions Zertifikat ohne jede Erweiterung, der SAN fehlt
Standardkonfiguration ohne Optionen SHA-1 und CA:TRUE, also ein CA-Zertifikat
Laufzeit 0 Tage Beginn gleich Ende, nie gültig
Laufzeit -1 Tag oder zu groß false, höchstens 24855 Tage unter Windows
negative Seriennummer ohne Warnung angenommen

Die Grenze von 24855 Tagen, rund 68 Jahre, gilt nur unter Windows. Linux nannte in der Warnung Days must be between 0 and 106751991167300. Für Seriennummern, die nicht in einen PHP-Integer passen, also mehr als 63 Bit brauchen, gibt es seit PHP 8.4 den Parameter serial_hex, der eine Hexzahl als String nimmt. Zwei Zertifikate derselben CA dürfen nie dieselbe Nummer tragen, random_int() ist deshalb besser als ein fester Wert.

Eine eigene Test-CA

Sollen mehrere Dienste einander vertrauen, lohnt eine eigene kleine Zertifizierungsstelle. Ihr Zertifikat bekommt den Abschnitt ca aus der Konfiguration, danach signiert sie die Anfrage des Servers:

<?php

$caKey = openssl_pkey_new(['config' => $cnf]);
$caCsr = openssl_csr_new(
['commonName' => 'Beispiel Test-CA'],
$caKey,
['config' => $cnf]
);
$caZert = openssl_csr_sign(
$caCsr,
null,
$caKey,
3650,
['config' => $cnf, 'x509_extensions' => 'ca'],
1
);
$srv = openssl_csr_sign(
$csr,
$caZert,
$caKey,
397,
['config' => $cnf, 'x509_extensions' => 'server'],
random_int(1, PHP_INT_MAX)
);
$caPub = openssl_pkey_get_public($caZert);
echo openssl_x509_verify($srv, $caPub);
/* 1 */

Gegen den eigenen öffentlichen Schlüssel geprüft lieferte openssl_x509_verify() 0, gegen den der CA 1. Mit einem falschen CA-Schlüssel brach das Signieren ab: Private key does not correspond to signing cert. Ganz ohne Meldung blieb dagegen ein anderer Fehler. Wer beim selbstsignierten Zertifikat versehentlich einen fremden Schlüssel übergibt, bekommt ein Zertifikat, dessen öffentlicher Schlüssel nicht zu seiner Signatur passt. Beim Selbstsignieren gehört also derselbe Schlüssel in beide Aufrufe. Die 397 Tage aus dem Beispiel gelten nur für die eigene Test-CA, öffentliche Zertifizierungsstellen stellen inzwischen deutlich kürzer aus.

Die Anfrage vor dem Versand prüfen

Bevor die Anfrage an die Zertifizierungsstelle geht, gehört ein Abgleich mit dem eigenen Schlüssel dazu. Ging einmal der falsche hinaus, stellt die CA ein Zertifikat aus, zu dem kein privater Schlüssel passt:

<?php

$pub = openssl_csr_get_public_key(
'file://' . __DIR__ . '/anfrage.csr'
);
$a = openssl_pkey_get_details($pub)['key'];
$b = openssl_pkey_get_details($schluessel)['key'];
var_dump($a === $b);
/* bool(true) */

Das Präfix file:// ist nötig. Ein Pfad ohne Präfix lieferte false, weil die Funktion den Pfad als PEM-Text liest. Mit einem fremden Schlüssel ergab der Vergleich false. Wo doch eine Konsole erreichbar ist, zeigt dieser Befehl Name, SAN und Hash-Verfahren und prüft die Signatur:

openssl req -in anfrage.csr -noout -text -verify

Die erste Zeile der Ausgabe lautete Certificate request self-signature verify OK. Die Entsprechungen sind übersichtlich: Das DN-Array ersetzt den Schalter -subj, der Abschnitt mit req_extensions das -addext, digest_alg das -sha256, und openssl_csr_sign() ohne CA das -x509 -days.

Fazit

PHP openssl_csr_new() baut aus Schlüssel und DN-Array eine Zertifikatsanfrage, die openssl_csr_export() als PEM ausgibt. Die Standardkonfiguration des Systems taugt dafür wenig: Sie ergänzt fehlende Felder mit Beispielwerten, signiert mit SHA-1 und fehlt unter Windows ganz. Eine eigene openssl.cnf mit leerem DN-Abschnitt und default_md = sha256 löst das und trägt zugleich die Subject Alternative Names.

Beim Signieren mit openssl_csr_sign() gehören eine zufällige Seriennummer und x509_extensions in jeden Aufruf, sonst fehlt der SAN im Zertifikat. Erfolg prüft is_object(), die Fehlerliste wird nur nach false gelesen. Wer die Anfrage vor dem Versand mit dem eigenen Schlüssel abgleicht, verschickt nie eine, zu der der private Schlüssel fehlt.

 


 

Kommentare (0)

Noch keine Kommentare. Sei der Erste!

Melde dich an, um einen Kommentar zu schreiben.
Bücherregal mit drei Büchern: 'PHP 4 - Grundlagen und Profiwissen' von Hanser Verlag, 'Webdesign in a Nutshell' von O'Reilly Verlag, und 'Webgestaltung' von Galileo Computing.