Request a certificate using a PHP script

Our certification servers accept certification requests not only via WWW but also via a SOAP interface.

The PHP script below generates a new key pair and a certification request for a TLS server and uses this interface to submit the request to the certification server and to download the PDF file that has to be printed, signed, and handed over to the participant service of the CA.

The script is deliberately kept simple so that everybody with sufficient knowledge of PHP can adopt it to his needs, even when using Windows or Macintosh computers.

#!/usr/bin/php
<?php

  # adopt the following data for each certificate request:

  # "Permitted" characters are:
  #   A-Z a-z 0-9 ' ( ) + , - . / : = ?
  #   and the space character, but no umlauts

  # Name of applicant (permitted characters only)
  $name = 'Rainer Perske';

  # email address of applicant
  $mail = 'wwwadmin@uni-muenster.de';

  # organizational unit of applicant (permitted characters only)
  $unit = 'IT';

  # path and name of the file with the applicant's PIN code
  #   (for each request, another PIN may be chosen)
  $code = '/path/to/file/with/the/pin';

  # RA-ID of the responsible participant service staff member
  #   4930=university
  #   5540=academy of fine arts
  $raid = 4930;

  # requested certificate profile
  #   for modern TLS servers choose 'Webserver MustStaple'
  #     (modern TLS servers support OCSP Stapling)
  #   for old TLS servers choose 'Web Server'
  #   for TLS servers and clients choose 'Mail Server'
  #   please get advice from the CA when needed
  $profil = 'Webserver MustStaple';

  # main fully qualified domain name of the server
  $fqdn = 'hostname.uni-muenster.de';

  # requested distinguished name of the server in OpenSSL format
  #   possibly occurring "/" must be escaped with backslash
  $subject = '/C=DE'
    . '/ST=Nordrhein-Westfalen'
    . '/L=Muenster'
    . '/O=Westfaelische Wilhelms-Universitaet Muenster'
    . '/CN=' . strtr($fqdn, array( '/' => '\\/' ));

  # all Subject Alternative Names to be included into the certificate
  #   the main name above must always be included
  $altnames = array(
    'dns:' . $fqdn,
    'dns:hostname2.uni-muenster.de',
    'dns:hostname.wwu.de',
    'dns:hostname2.wwu.de',
  );

  # path and name of the file with the root certificates in PEM format
  #   you can find the root certificates here:
  #   https://www.uni-muenster.de/CA/all-rootca.pem
  $root = '/path/to/file/with/root/certificate';

  # end of settings

  # generate key pair and request
  #   (using the OpenSSL functions of PHP would be much more
  #   complicated)
  passthru('/usr/bin/openssl req -new -sha256 -newkey rsa:2048 -nodes'
    . ' -keyout ' . escapeshellarg( "$fqdn.key" )
    . ' -out '    . escapeshellarg( "$fqdn.req" )
    . ' -subj '   . escapeshellarg( $subject )
  );

  # load request and PIN from the files
  $req = file_get_contents("$fqdn.req")
    or die("Error loading $fqdn.req\n");
  $pin = file_get_contents($code)
    or die("Error loading $code\n");

  # establish SOAP connection
  #   the details with (P) are necessary only if access to the
  #   certification server must be made via the proxy server
  $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
        ),              # or: RootCA > USERTrust CA > GÉANT TCS > Server
      )),
    )
  ) or die("Error establishing the SOAP connection\n");

  # submit request
  $soapdata = $soap->newRequest(
    $raid,             # RA-ID
    $req,              # Request in PEM format
    $altnames,         # Array with the Subject Alternative Names
    $profil,           # certification profile
    sha1($pin),        # PIN
    $name,             # name of applicant
    $mail,             # email of applicant
    $unit,             # organizational unit of applicant
    true               # publish the certificate?
  );
  if(is_soap_fault($soapdata))
    die("Error submitting the request:\n"
      . "{$soapdata->faultcode}: {$soapdata->faultstring}\n");
  if(!$soapdata)
    die("Error submitting the request\n");
  $reqnum = intval($soapdata);

  # download PDF file
  $soapdata = $soap->getRequestPrintout(
    $raid,             # RA-ID
    $reqnum,           # request number, has been assigned above
    'application/pdf', # no other setting allowed
    sha1($pin)         # must be the same PIN as above
  );
  if(is_soap_fault($soapdata))
    die("Error downloading the PDF file:\n"
      . "{$soapdata->faultcode}: {$soapdata->faultstring}\n");
  if(!$soapdata)
    die("Error downloading the PDF file\n");

  # save the PDF file
  file_put_contents("$fqdn.pdf",$soapdata)
    or die("Error saving the PDF file to $fqdn.pdf\n");

  # terminate SOAP connection
  unset($soap);

?>

This script creates these files:

hostname.uni-muenster.de.key

the private key in PEM format (unprotected)
suitable for the Apache statement SSLCertificateKeyFile

hostname.uni-muenster.de.req

the uploaded certification request in PEM format

hostname.uni-muenster.de.pdf

the PDF file

This PDF file must now be printed, signed, and handed over to a participant service staff member.

You will later receive the certificate by email. Then you should save the attachment into a file:

hostname.uni-muenster.de.cert

the issued certificate from the email in PEM format
suitable for the Apache statement SSLCertificateFile

You should also save these certificates of the intermediate certification authorities:

hostname.uni-muenster.de.chain

Intermediate CA certificates in PEM format
suitable for the Apache statement SSLCertificateChainFile

The issued certificate can be fetched via SOAP, too. To do so, you have to combine large parts of the script above with the PHP fragment below:

  # Download certificate
  $soapdata = $soap->getCertificateByRequestSerial(
    $raid,     # RA-ID
    $reqnum,   # request number, has been assigned above
    sha1($pin) # must be the same PIN as above
  );
  if(is_soap_fault($soapdata))
    die("Error downloading the certificate:\n"
      . "{$soapdata->faultcode}: {$soapdata->faultstring}\n");
  if(!$soapdata) die("Error downloading the certificate\n");

  # Save certificate
  file_put_contents("$fqdn.cert",$soapdata)
    or die("Error saving the certificate to $fqdn.cert\n");

PHP SOAP and certificate serial numbers

Unfortunately PHP bug 48171 (integer overflow with huge numbers in SOAP data) is not yet fixed even after more than eight years. Thus you have to use some tricks when using functions working with serial numbers of certificates.

This affects the function for requesting certificate revocations. To use it, you have to define two PHP functions, to pass the typemap parameter when establishing the SOAP connection, and to pass the serial number as an array with two elements:

  # Mappings for: 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',
        ),
        ...

   # Unfortunately you have to know that the internal name
   # for the serial number is "Serial":
   $soapdata = $soap->newRevocationRequest(
     $raid,                      # RA-ID
     array( 'Serial', $serial ), # specially: serial number of certificate
     'user request',             # reason
     sha1($pin)                  # must be the same PIN as above
   );
   ...

Luckily you will hardly ever need to submit so many revocation requests that it is worth writing a PHP script for it.