Zertifikat mit PHP-Skript beantragen

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

Das folgende, für Linux-Systeme geschriebene PHP-Skript erstellt ein neues Schlüsselpaar und einen Zertifikatantrag für einen TLS-Server und verwendet diese Schnittstelle, um den Antrag an den Zertifizierungsserver zu übermitteln und die PDF-Datei abzurufen, die dann ausgedruckt, unterschrieben und beim Teilnehmerservice der CA abgegeben werden muss.

Das Skript ist bewusst simpel gehalten, damit jeder mit ausreichenden PHP-Kenntnissen es an seine Bedürfnisse anpassen kann, auch unter Windows oder Macintosh.

#!/usr/bin/php
<?php

  # Folgende Angaben fuer jeden Zertifikatsantrag neu einstellen:

  # "Erlaubte" Zeichen sind:
  #   A-Z a-z 0-9 ' ( ) + , - . / : = ?
  #   und das Leerzeichen, aber keine Umlaute

  # Name des Antragstellers (nur erlaubte Zeichen)
  $name = 'Rainer Perske';

  # E-Mail-Adresse des Antragstellers
  $mail = 'wwwadmin@uni-muenster.de';

  # Organisationseinheit des Antragstellers (nur erlaubte Zeichen)
  $unit = 'IT';

  # Pfad und Name der Datei mit dem PIN-Code des Antragsstellers
  #  (für jeden Antrag darf eine andere PIN gewählt werden)
  $code = '/path/to/file/with/the/pin';

  # RA-ID der zustaendigen 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'
  #   Lassen Sie sich bitte bei Bedarf von der CA beraten.
  $profil = 'Webserver MustStaple';

  # Vollqualifizierter (Haupt-) Name des Servers
  $fqdn = 'hostname.uni-muenster.de';

  # Gewünschter Distinguished Name des Servers im OpenSSL-Format
  #   Vorkommende "/" müssen mit Backslash maskiert werden
  $subject = '/C=DE'
    . '/ST=Nordrhein-Westfalen'
    . '/L=Muenster'
    . '/O=Westfaelische Wilhelms-Universitaet Muenster'
    . '/CN=' . strtr($fqdn, array( '/' => '\\/' ));

  # Alle ins Zertifikat aufzunehmenden Subject Alternative Names
  #   Der obige (Haupt-) Name muss immer enthalten sein
  $altnames = array(
    'dns:' . $fqdn,
    'dns:hostname2.uni-muenster.de',
    'dns:hostname.wwu.de',
    'dns:hostname2.wwu.de',
  );

  # Pfad und Name der Datei mit den Wurzelzertifikaten im PEM-Format
  #   Sie finden diese Wurzelzertifikate hier:
  #   https://www.uni-muenster.de/CA/all-rootca.pem
  $root = '/path/to/file/with/root/certificate';

  # Ende der Einstellungen

  # Schlüsselpaar und Request 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( "$fqdn.key" )
    . ' -out '    . escapeshellarg( "$fqdn.req" )
    . ' -subj '   . escapeshellarg( $subject )
  );

  # Request und PIN aus den Dateien laden
  $req = file_get_contents("$fqdn.req")
    or die("Fehler beim Laden von $fqdn.req\n");
  $pin = file_get_contents($code)
    or die("Fehler beim Laden von $code\n");

  # SOAP-Verbindung aufbauen
  #   Die Angaben mit (P) sind nur nötig, wenn der Zugriff auf den
  #   Zertifizierungsserver über den Proxy-Server erfolgen muss.
  $soap = new SoapClient(
    'https://pki.pca.dfn.de/dfn-ca-global-g2/cgi-bin/pub/soap?wsdl=1',
    array(
      'trace'          => false,
      'exceptions'     => false,
      'features'       => SOAP_SINGLE_ELEMENT_ARRAYS,
      'cache_wsdl'     => WSDL_CACHE_NONE,
      'proxy_host'     => 'wwwproxy.uni-muenster.de', # (P)
      'proxy_port'     => 3128,                       # (P)
      'typemap'        => array(),
      'stream_context' => stream_context_create(array(
        'http'  => array(
          'proxy'           => 'tcp://wwwproxy.uni-muenster.de:3128', # (P)
          'request_fulluri' => true,                                  # (P)
          'timeout'         => 60,
        ),
        'ssl'   => array(
          'verify_peer'     => true,
          'cafile'          => $root,
          'verify_depth'    => 3,    # RootCA > DFN-PCA > CA > Server
        ),            # oder: RootCA > USERTrust CA > GÉANT TCS > Server
      )),
    )
  ) or die("Fehler beim Aufbau der SOAP-Verbindung\n");

  # Request absenden
  $soapdata = $soap->newRequest(
    $raid,             # RA-ID
    $req,              # Request im PEM-Format
    $altnames,         # Array mit den Subject Alternative Names
    $profil,           # Zertifikatprofil
    sha1($pin),        # PIN
    $name,             # Name des Antragstellers
    $mail,             # E-Mail des Antragstellers
    $unit,             # Organisationseinheit des Antragstellers
    true               # Veröffentlichen des Zertifikats?
  );
  if(is_soap_fault($soapdata))
    die("Fehler beim Absenden des Antrags:\n"
      . "{$soapdata->faultcode}: {$soapdata->faultstring}\n");
  if(!$soapdata)
    die("Fehler beim Absenden des Antrags\n");
  $reqnum = intval($soapdata);

  # PDF-Datei abrufen
  $soapdata = $soap->getRequestPrintout(
    $raid,             # RA-ID
    $reqnum,           # Antragsnummer, wurde oben vergeben
    'application/pdf', # keine andere Angabe erlaubt
    sha1($pin)         # muss die gleiche PIN wie oben sein
  );
  if(is_soap_fault($soapdata))
    die("Fehler beim Abrufen der PDF-Datei:\n"
      . "{$soapdata->faultcode}: {$soapdata->faultstring}\n");
  if(!$soapdata)
    die("Fehler beim Abrufen der PDF-Datei\n");

  # PDF-Datei abspeichern
  file_put_contents("$fqdn.pdf",$soapdata)
    or die("Fehler beim Speichern der PDF-Datei nach $fqdn.pdf\n");

  # SOAP-Verbindung abbauen
  unset($soap);

?>

Dieses Skript erzeugt folgende Dateien:

hostname.uni-muenster.de.key

der private Schlüssel im PEM-Format (ungeschützt)
passend für die Apache-Anweisung SSLCertificateKeyFile

hostname.uni-muenster.de.req

der hochgeladene Zertifikatantrag im PEM-Format

hostname.uni-muenster.de.pdf

die PDF-Datei

Diese PDF-Datei muss jetzt ausgedruckt, unterschrieben und dem Teilnehmerservice-Mitarbeiter übergeben werden.

Sie werden dann das Zertifikat per E-Mail erhalten. Sie sollten die Anlage abspeichern:

hostname.uni-muenster.de.cert

das ausgestellte Zertifikat aus der E-Mail im PEM-Format
passend für die Apache-Anweisung SSLCertificateFile

Ebenfalls abspeichern sollten Sie diese Zertifikate der Zwischenzertifizierungsstellen:

hostname.uni-muenster.de.chain

Zwischen-CA-Zertifikate im PEM-Format
passend für die Apache-Anweisung SSLCertificateChainFile

Das ausgestellte Zertifikat kann auch per SOAP abgeholt werden. Dazu müssten Sie große Teile des obigen Skripts mit folgendem PHP-Fragment verbinden:

  # Zertifikat abrufen
  $soapdata = $soap->getCertificateByRequestSerial(
    $raid,             # RA-ID
    $reqnum,           # Antragsnummer, wurde oben vergeben
    sha1($pin)         # muss die gleiche PIN wie oben sein
  );
  if(is_soap_fault($soapdata))
    die("Fehler beim Abrufen des Zertifikats:\n"
      . "{$soapdata->faultcode}: {$soapdata->faultstring}\n");
  if(!$soapdata)
    die("Fehler beim Abrufen des Zertifikats\n");

  # Zertifikat abspeichern
  file_put_contents("$fqdn.cert",$soapdata)
    or die("Fehler beim Speichern des Zertifikats nach $fqdn.cert\n");

PHP-SOAP und Zertifikat-Seriennummern

Leider ist der PHP-Bug 48171 (Integer-Overflow bei großen Zahlen in SOAP-Daten) auch nach über acht Jahren immer noch nicht behoben, daher muss für Funktionen, die mit Zertifikatseriennummern arbeiten, getrickst werden.

Dies betrifft die Funktion zum Stellen von Sperranträgen. Um diese zu verwenden, müssen vorher zwei PHP-Funktionen definiert werden, muss beim Aufbauen der SOAP-Verbindung der typemap-Parameter gesetzt werden und muss beim Aufruf der Funktion die Seriennummer als Array mit zwei Elementen übergeben werden:

  # Abbildungen für: integer positiveInteger negativeInteger
  #  nonPositiveInteger nonNegativeInteger
  function soap_from_xml_integer($x){
    $x = simplexml_load_string($x);
    return array(
      $x->getName(),
      $x->__toString()
    );
  }
  function soap_to_xml_integer($x){
    return '<' . $x[0] . '>'
      . htmlentities($x[1], ENT_NOQUOTES|ENT_XML1)
      . '</' . $x[0] . '>';
  }

  $soap = new SoapClient(
    'https://pki.pca.dfn.de/dfn-ca-global-g2/cgi-bin/pub/soap?wsdl=1',
    array(
      ...
      'typemap' => array(
        array(
          'type_ns'=>'http://www.w3.org/2001/XMLSchema',
          'type_name'=>'integer',
          'from_xml'=>'soap_from_xml_integer',
          'to_xml'=>'soap_to_xml_integer',
        ),
        ...

   # Es geht leider nicht ohne das Wissen, dass intern der
   # Name "Serial" für die Seriennummer verwendet wird:
   $soapdata = $soap->newRevocationRequest(
     $raid,                      # RA-ID
     array( 'Serial', $serial ), # speziell: Zertifikat-Seriennummer
     'user request',             # Begründung
     sha1($pin)                  # muss die gleiche PIN wie oben sein
   );
   ...

Glücklicherweise werden kaum jemals so viele Sperranträge zu stellen sein, dass sich das Schreiben eines PHP-Skripts dafür lohnt.