Vollautomatische Zertifikate

Unsere Zertifizierungsserver nehmen Zertifikatanträge nicht nur per WWW, sondern auch über eine SOAP-Schnittstelle entgegen.

Anträge für Serverzertifikate können dann über eine HTTPS-Schnittstelle gegenüber dem Teilnehmerservice bestätigt werden. Dabei muss sich der Antragsteller mit seinem persönlichen Nutzerzertifikat ausweisen. Falls alle Bedingungen erfüllt sind, kann der Teilnehmerservice dann ohne manuelle Prüfung das Zertifikat vollautomatisch ausstellen.

Das ausgestellte Zertifikat kann dann wiederum über die SOAP-Schnittstelle abgerufen werden.

Die Kombination dieser Schnittstellen ermöglicht es, Zertifikate vollautomatisch zu beantragen, auszustellen, abzuholen und zu aktivieren. Beachten Sie dazu bitte unsere detaillierte Prozessbeschreibung.

(Bitte beachten Sie: Aus Sicherheitsgründen laufen die Automatismen nicht unbeaufsichtigt rund um die Uhr. Es kann trotzdem noch Stunden oder wenige Tage dauern, bis das Zertifikat ausgestellt ist.)

Voraussetzungen

Der hier benutzte Teil der SOAP-Schnittstelle ist öffentlich zugänglich und kann ohne Voraussetzungen genutzt werden.

Die HTTPS-Schnittstelle kann nur unter folgenden Voraussetzungen genutzt werden:

  • Der Antragsteller verfügt über ein von der CA der Universität Münster ausgestelltes persönliches Nutzerzertifikat, welches zur Anmeldung am zentralen Single Sign-On der Universität Münster geeignet ist.

  • Der Antragsteller ist für alle beantragten Servernamen als Administrator in der zentralen Rechnerdatenbank der Universität eingetragen.

Falls diese Voraussetzungen nicht erfüllt sind, muss das über die SOAP-Schnittstelle abgerufene Antragsformular weiterhin persönlich bei einem Teilnehmerservice-Mitarbeiter abgegeben werden.

Beispielrealisierung als PHP-Skript

#!/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');

?>