#!/usr/bin/php
<?php
#=======================================================================
# Konfiguration
#=======================================================================
# Digitale ID des Antragstellers
# Diese PEM-Datei besteht aus vier Teilen in dieser Reihenfolge:
# 1. dem verschlüsselten privaten Schlüssel des Antragstellers
# 2. dem Zertifikat des Antragstellers
# 3. dem Zertifikat der DFN-Verein Global Issuing CA
# 4. dem Zertifikat der DFN-Verein Certification Authority 2
$pemfile = '/path/to/digitalID.pem';
# Das Passwort zum Entschlüsseln des privaten Schlüssels
$pempass = 'Wer-dies-kennt-kann-meine-Identitaet-stehlen';
# RA-ID der zuständigen Teilnehmerservice-Mitarbeiter
# 4930 = Universität
# 5540 = Kunstakademie
$raid = 4930;
# Gewünschtes Zertifikatprofil
# Für aktuelle TLS-Server wählen Sie: Webserver MustStaple
# (Aktuelle TLS-Server unterstützen OCSP Stapling)
# Für alte TLS-Server wählen Sie: Web Server
# Für TLS-Server+Clients wählen Sie: Mail Server
# Für Shibboleth-Server wählen Sie: Shibboleth IdP SP
# Bitte lassen Sie sich von der CA beraten
$profil = 'Webserver MustStaple';
# Vollqualifizierte Servernamen
# Der "Hauptname" muss als erster genannt werden
# Keine Großbuchstaben verwenden!
$fqdnlist = [
'hostname.uni-muenster.de',
'hostname2.uni-muenster.de',
'hostname.wwu.de',
'hostname2.wwu.de',
];
# Sperr-Passwort des Antragstellers für das beantragte Zertifikat
$revocationpass = 'Wer-dies-kennt-kann-sperren-lassen';
# Exakt mit diesem Text muss der Antrag bestätigt werden
$confirm = 'Hiermit unterschreibe ich den Zertifikatantrag.';
#=======================================================================
# Aktueller Zeitpunkt für Dateinamen
#=======================================================================
$now=date('Ymd\\THis');
#=======================================================================
# Subject und Subject Alternative Names zusammenbauen
#=======================================================================
# Aus der RA-ID ergibt sich die Organisationsangabe
switch($raid){
case 4930:
$org = 'Westfaelische Wilhelms-Universitaet Muenster';
$unit = 'WWU';
break;
case 5540:
$org = 'Kunstakademie Muenster - Hochschule fuer Bildende Kuenste';
$unit = 'KA';
break;
default:
die("RA-ID unbekannt\n");
}
# Syntaxkontrolle der vollqualifizierten Servernamen
if(!$fqdnlist or !is_array($fqdnlist))
die("FQDN-Liste fehlerhaft\n");
foreach($fqdnlist as $fqdn)
if(strlen($fqdn) > 253 or !preg_match(
'#^(?:(?!-)[a-z0-9-]{1,63}(?<!-)\\.)+[a-z]{2,63}$#',
$fqdn
)) die("FQDN fehlerhaft: $fqdn\n");
# Subject Alternative Names zusammenbauen
$reqsan = [];
foreach($fqdnlist as $fqdn)
$reqsan[] = 'DNS:' . $fqdn;
# Subject Distinguished Name im OpenSSL-Format zusammenbauen
# Schrägstriche sind zu maskieren
$fqdn = reset($fqdnlist);
$reqsub = '/C=DE/ST=Nordrhein-Westfalen/L=Muenster'
. '/O=' . strtr($org, ['/' => '\\/'])
. '/CN=' . strtr($fqdn, ['/' => '\\/']);
#=======================================================================
# Sperr-PIN-Hash zusammenbauen
#=======================================================================
# Um Antragsinformationen, Antragsformular und fertiges Zertifikat
# abzurufen, muss der gleiche Sperr-Passwort-Hash angegeben werden wie
# beim Absenden des Antrags.
$reqpin = sha1($revocationpass);
#=======================================================================
# Antragstellerdaten aus Nutzerzertifikat auslesen
#=======================================================================
# Digitale ID des Antragstellers einlesen
$data = file_get_contents($pemfile)
or die("Nutzerzertifikat nicht gefunden\n");
$x509 = openssl_x509_parse($data)
or die("Nutzerzertifikat fehlerhaft\n");
# Name, E-Mail und Verwendungszweck auslesen und vorprüfen
$name = @$x509['subject']['CN'];
$mail = @$x509['subject']['emailAddress'];
$exku = explode(', ',@$x509['extensions']['extendedKeyUsage']);
if( !is_string($name)
or !is_string($mail)
# Nutzerzertifikate haben Leerzeichen zwischen Vor- und Nachname
# Serverzertifikate haben hier kein Leerzeichen
or false === strpos($name, ' ')
# Verwendungszweck
or !in_array('TLS Web Client Authentication', $exku)
) die("Nutzerzertifikat unbrauchbar\n");
#=======================================================================
# Root-CA-Zertifikat und Zwischen-CA-Zertifikate herunterladen
#=======================================================================
# Root-CA-Zertifikat sowohl für die neue digitale ID
# als auch zur Verifizierung der SOAP- und HTTPS-Server
copy('https://www.uni-muenster.de/CA/rootca.pem', "$now.$fqdn.root")
or die("Wurzelzertifikat nicht abrufbar\n");
# Zwischen-CA-Zertifikate für die neue digitale ID
copy('https://www.uni-muenster.de/CA/chain.pem', "$now.$fqdn.chain")
or die("Zwischenzertifikate nicht abrufbar\n");
#=======================================================================
# Schlüsselpaar und Zertifikatantragsdatei erzeugen
#=======================================================================
# Mit den OpenSSL-Funktionen von PHP wäre das sehr viel komplizierter
passthru('/usr/bin/openssl req -new -sha256 -newkey rsa:2048 -nodes'
. ' -keyout ' . escapeshellarg( "$now.$fqdn.key" )
. ' -out ' . escapeshellarg( "$now.$fqdn.req" )
. ' -subj ' . escapeshellarg( $reqsub )
);
# Erzeugte Antragsdatei einlesen
$req = file_get_contents("$now.$fqdn.req")
or die("Fehler beim Laden von $now.$fqdn.req\n");
#=======================================================================
# SOAP-Verbindung aufbauen
#=======================================================================
# Als Funktion definieren, da mehrfach benötigt
function newsoap($rootcafile){
return new SoapClient(
'https://pki.pca.dfn.de/dfn-ca-global-g2/cgi-bin/pub/soap?wsdl=1',
[
# Dieses Skript benötigt folgende Einstellungen
'trace' => false,
'exceptions' => false,
'features' => SOAP_SINGLE_ELEMENT_ARRAYS,
'cache_wsdl' => WSDL_CACHE_NONE,
# Folgende zwei Zeilen nur,
# falls der Proxy-Server genutzt wird
'proxy_host' => 'wwwproxy.uni-muenster.de',
'proxy_port' => 3128,
'stream_context' => stream_context_create([
'http' => [
'timeout' => 120,
# Folgende zwei Zeilen nur,
# falls der Proxy-Server genutzt wird
'proxy' => 'tcp://wwwproxy.uni-muenster.de:3128',
'request_fulluri' => true,
],
'ssl' => [
# Kontrolle des Serverzertifikats
'verify_peer' => true,
'cafile' => $rootcafile,
'verify_depth' => 3, # RootCA > DFN-PCA > CA > Server
],
]),
]
);
}
# Verbindung zum DFN-PKI-Zertifizierungsserver herstellen
$soap = newsoap("$now.$fqdn.root")
or die("Fehler beim Aufbau der SOAP-Verbindung\n");
#=======================================================================
# 1. SOAP-Funktionsaufruf: Antrag absenden
#=======================================================================
$soapdata = $soap->newRequest(
$raid, # RA-ID
$req, # Antrag im PEM-Format
$reqsan, # Array mit den Subject Alternative Names
$profil, # Zertifikatprofil
$reqpin, # Sperr-PIN-Hash
$name, # Name des Antragstellers
$mail, # E-Mail des Antragstellers
$unit, # Organisationseinheit des Antragstellers
true # Zwingend: Veröffentlichen des Zertifikats
);
if($soapdata == '')
die("Fehler beim Absenden des Antrags\n");
if(is_soap_fault($soapdata))
die("Fehler beim Absenden des Antrags:\n"
. "{$soapdata->faultcode}: {$soapdata->faultstring}\n");
# Antwort ist die Antragsnummer
$reqnum = intval($soapdata);
#=======================================================================
# 2. SOAP-Funktionsaufruf: Antragsinformationen abrufen
#=======================================================================
$soapdata = $soap->getRequestInfo(
$raid, # Gleiche RA-ID wie oben
$reqnum, # Antragsnummer, wurde oben vergeben
$reqpin # Gleicher Sperr-PIN-Hash wie oben
);
if($soapdata == '')
die("Fehler beim Abrufen der Antragsinformationen\n");
if(is_soap_fault($soapdata))
die("Fehler beim Abrufen der Antragsinformationen:\n"
. "{$soapdata->faultcode}: {$soapdata->faultstring}\n");
# Benötigte Angaben aus der Antwort notieren und prüfen
# $reqsub und $reqsan werden ggf. angepasst
$status = $soapdata->Status;
$reqsub = $soapdata->Parameters->Subject;
$reqsan = $soapdata->Parameters->SubjectAltNames;
$digest = $soapdata->PublicKeyDigest;
if( $status!='NEW'
or !is_string($reqsub)
or !is_array($reqsan)
or !is_string($digest)
) die("Antragsinformationen fehlerhaft\n");
#=======================================================================
# 3. SOAP-Funktionsaufruf: Antragsformular abrufen
#=======================================================================
$soapdata = $soap->getRequestPrintout(
$raid, # Gleiche RA-ID wie oben
$reqnum, # Antragsnummer, wurde oben vergeben
'application/pdf', # Zwingend: PDF-Format
$reqpin # Gleicher Sperr-PIN-Hash wie oben
);
if($soapdata == '')
die("Fehler beim Abrufen der PDF-Datei\n");
if(is_soap_fault($soapdata))
die("Fehler beim Abrufen der PDF-Datei:\n"
. "{$soapdata->faultcode}: {$soapdata->faultstring}\n");
# Antragsformular abspeichern
file_put_contents("$now.$fqdn.pdf",$soapdata)
or die("Fehler beim Speichern der PDF-Datei nach $now.$fqdn.pdf\n");
# Dieses Formular wird nicht weiter benötigt, aber der Antragsteller
# soll wenigstens nachlesen können, welche Erklärung er unten
# bestätigt.
#=======================================================================
# SOAP-Verbindung abbauen
#=======================================================================
unset($soap);
#=======================================================================
# Kontrollausgabe
#=======================================================================
# Subject, Subject Alternative Names, Public Key Digest
echo "\nAntrag $reqnum an RA-ID $raid:\n$reqsub\n"
. implode("\n", $reqsan)
. "\n$digest\n\n";
#=======================================================================
# HTTPS-Funktionsaufruf: Bestätigung an IT-Portal übermitteln
#=======================================================================
# HTTPS-Post-Request mit JSON-Daten und TLS-Client-Authentifizierung
# absenden und JSON-Antwort entgegennehmen
$data = file_get_contents(
'https://xsso.uni-muenster.de/IT-Portal/dfnpkiconfirm/',
false,
stream_context_create([
'http' => [
'follow_location' => 0,
'ignore_errors' => true,
'method' => 'POST',
'protocol_version' => 1.1,
'timeout' => 120,
'user_agent' => 'None/0.0',
'header' => [
'Content-type: application/json',
'Accept: application/json',
],
# Folgende zwei Zeilen nur,
# falls der Proxy-Server genutzt wird
'proxy' => 'tcp://wwwproxy.uni-muenster.de:3128',
'request_fulluri' => true,
# Alle Daten im JSON-Format
'content' => json_encode([
'reqnum' => $reqnum,
'raid' => $raid,
'reqpin' => $reqpin,
'reqsub' => $reqsub,
'reqsan' => $reqsan, # Array!
'digest' => $digest,
'confirm' => $confirm,
],JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT),
],
'ssl' => [
# Kontrolle des Serverzertifikats
'verify_peer' => true,
'cafile' => "$now.$fqdn.root",
'verify_depth' => 3, # RootCA > DFN-PCA > CA > Server
# Anmeldung mit Clientzertifikat
'local_cert' => $pemfile,
'passphrase' => $pempass,
],
])
) or die("Fehler beim Absenden der Bestätigung\n$data\n");
$json = json_decode($data, true, 9999, JSON_BIGINT_AS_STRING)
or die("Antwort auf Bestätigung fehlerhaft\n");
if(!$json['success'])
die("Bestätigung nicht angenommen:\n"
. implode("\n", $json['errors']) . "\n");
#=======================================================================
# Schleife: In großen Abständen versuchen, das neue Zertifikat abzurufen
#=======================================================================
# Anfangs mindestens 10 Minuten warten, besser 20
$nextsleep = 1200;
# Schleife bis Zertifikat erfolgreich geholt
while(true){
#=======================================================================
# Vor dem Warten noch offene SOAP Verbindungen schließen
#=======================================================================
unset($soap);
#=======================================================================
# Warten
#=======================================================================
echo "Warte ...\n";
sleep($nextsleep);
# Zwischen zwei Versuchen mindestens 30 Minuten warten, besser 60
$nextsleep = 3600;
#=======================================================================
# SOAP-Verbindung aufbauen
#=======================================================================
$soap = newsoap("$now.$fqdn.root");
if(!$soap){
echo "SOAP-Server nicht erreichbar\n";
continue;
}
#=======================================================================
# 4. SOAP-Funktionsaufruf: Zertifikat abrufen
#=======================================================================
$soapdata = $soap->getCertificateByRequestSerial(
$raid, # gleiche RA-ID wie oben
$reqnum, # Antragsnummer, wurde oben vergeben
$reqpin # gleicher Sperr-PIN-Hash wie oben
);
if($soapdata == ''){
echo "Fehler beim Abrufen des Zertifikats\n";
continue;
}
if(is_soap_fault($soapdata)){
echo "Fehler beim Abrufen des Zertifikats:\n"
. "{$soapdata->faultcode}: {$soapdata->faultstring}\n";
continue;
}
# Zertifikat abspeichern
if(!file_put_contents("$now.$fqdn.cert", $soapdata)){
echo "Fehler beim Speichern des Zertifikats in $now.$fqdn.cert\n";
continue;
}
#=======================================================================
# Ende der Schleife, wir haben das Zertifikat
#=======================================================================
break;
}
#=======================================================================
# SOAP-Verbindung abbauen
#=======================================================================
unset($soap);
#=======================================================================
# Kontrollausgabe
#=======================================================================
echo "\n"
. "Die digitale ID ist vollständig, siehe folgende Dateien:\n"
. "Antragsdatei: $now.$fqdn.req\n"
. "Antragsformular: $now.$fqdn.pdf\n"
. "Privater Schlüssel: $now.$fqdn.key\n"
. "Serverzertifikat: $now.$fqdn.cert\n"
. "Zwischen-CA-Zertifikate: $now.$fqdn.chain\n"
. "Wurzel-CA-Zertifikat: $now.$fqdn.root\n"
. "\n";
#=======================================================================
# Beispiel: Neue digitale ID direkt in Apache-Webserver einbauen
#=======================================================================
### # Konfigurationsdateien ersetzen
### if( !copy("$now.$fqdn.key", '/path/to/server/key.pem')
### or !copy("$now.$fqdn.cert", '/path/to/server/cert.pem')
### or !copy("$now.$fqdn.chain", '/path/to/server/chain.pem')
### ) die("Fehler beim Aktualisieren der Serverkonfiguration\n");
### # Konfiguration neu laden
### passthru('sudo systemctl reload httpd');
?>