Fully automated certificates

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

Requests for server certificates can then be confirmed to the participant service via an HTTPS interface. The requester must identify himself with his personal user certificate. If all conditions are met, the participant service can then issue the certificate fully automatically without manual verification.

The issued certificate can be retrieved again via the SOAP interface.

The combination of these interfaces makes it possible to request, issue, retrieve, and activate certificates fully automatically. Please refer to our detailed process description.

(Please note: For security reasons, the automatisms do not run unattended around the clock. It can still take hours or a few days until the certificate is issued.)

Requirements

The part of the SOAP interface used here is publicly accessible and can be used without requirements.

The HTTPS interfaces can be used only if the following requirements are met:

  • The requester is in possession of a personal user certificate issued by WWUCA which is suitable for logging in to the central Single Sign-On of the WWU.

  • The requester is registered in the central computer database of the WWU as administrator for all requested server names.

If these requirements are not met, the request printout retrieved via the SOAP interface must still be handed over personally to a participant service staff member.

Example implementation as PHP script

#!/usr/bin/php
<?php

#=======================================================================
# Configuration
#=======================================================================

# Digital ID of the requester
# This PEM file consists of four parts in this order:
# 1st the encrypted private key of the requester
# 2nd the certificate of the requester
# 3rd the certificate of the DFN-Verein Global Issuing CA
# 4th the certificate of the DFN-Verein Certification Authority 2
$pemfile = '/path/to/digitalID.pem';

# The password for decrypting the private key
$pempass = 'Who-knows-this-can-steal-my-identity';

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

# Requested certificate profile
#   For current TLS servers choose: Webserver MustStaple
#     (Current TLS servers support OCSP Stapling)
#   For old TLS servers choose: Web Server
#   For TLS server+clients choose Mail Server
#   For Shibboleth servers choose: Shibboleth IdP SP
#   Please let the WWUCA advise you
$profil = 'Webserver MustStaple';

# Fully qualified server names
#   The "primary name" must be mentioned first
#   Do not use uppercase letters!
$fqdnlist = [
  'hostname.uni-muenster.de',
  'hostname2.uni-muenster.de',
  'hostname.wwu.de',
  'hostname2.wwu.de',
];

# Revocation password of the requester for the requested certificate
$revocationpass = 'Who-knows-this-can-request-a-revocation';

# Exactly with this German text the request must be confirmed
$confirm = 'Hiermit unterschreibe ich den Zertifikatantrag.';

#=======================================================================
# Current time stamp for file names
#=======================================================================

$now=date('Ymd\\THis');

#=======================================================================
# Compose Subject and Subject Alternative Names
#=======================================================================

# The RA-ID determines the organization name
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 unknown\n");
}

# Syntax check of the fully qualified server names
if(!$fqdnlist or !is_array($fqdnlist))
  die("FQDN list faulty\n");
foreach($fqdnlist as $fqdn)
  if(strlen($fqdn) > 253 or !preg_match(
    '#^(?:(?!-)[a-z0-9-]{1,63}(?<!-)\\.)+[a-z]{2,63}$#',
    $fqdn
  )) die("FQDN faulty: $fqdn\n");

# Compose Subject Alternative Names
$reqsan = [];
foreach($fqdnlist as $fqdn)
  $reqsan[] = 'DNS:' . $fqdn;

# Compose Subject Distinguished Name in OpenSSL format
#   Slashes must be escaped
$fqdn = reset($fqdnlist);
$reqsub = '/C=DE/ST=Nordrhein-Westfalen/L=Muenster'
  . '/O='  . strtr($org,  ['/' => '\\/'])
  . '/CN=' . strtr($fqdn, ['/' => '\\/']);

#=======================================================================
# Compose revocation password hash
#=======================================================================

# To retrieve request information, request printout and issued
# certificate, the same revocation password hash must be specified as
# when the request was submitted
$reqpin = sha1($revocationpass);

#=======================================================================
# Extract requester details from user certificate
#=======================================================================

# Read digital ID of the requester
$data = file_get_contents($pemfile)
  or die("User certificate not found\n");
$x509 = openssl_x509_parse($data)
  or die("User certificate faulty\n");

# Extract and check name, email, and purpose of key use
$name = @$x509['subject']['CN'];
$mail = @$x509['subject']['emailAddress'];
$exku = explode(', ',@$x509['extensions']['extendedKeyUsage']);
if(  !is_string($name)
  or !is_string($mail)
  # User certificates have blanks between first and last name
  # Server certificate no not have a blank here
  or false === strpos($name, ' ')
  # Purpose of key use
  or !in_array('TLS Web Client Authentication', $exku)
) die("User certificate not usable\n");

#=======================================================================
# Download root CA certificate and intermediate CA certificates
#=======================================================================

# Root CA certificate both for the new digital ID and for verifying
#   the SOAP and HTTPS servers
copy('https://www.uni-muenster.de/WWUCA/rootca.pem', "$now.$fqdn.root")
  or die("Root certificate not downloadable\n");

# Zwischen-CA-Zertifikate für die neue digitale ID
copy('https://www.uni-muenster.de/WWUCA/chain.pem', "$now.$fqdn.chain")
  or die("Intermediate certificates not downloadable\n");

#=======================================================================
# Generate key pair and certificate request file
#=======================================================================

# Using the OpenSSL functions of PHP this would be much more complicated
passthru('/usr/bin/openssl req -new -sha256 -newkey rsa:2048 -nodes'
  . ' -keyout ' . escapeshellarg( "$now.$fqdn.key" )
  . ' -out '    . escapeshellarg( "$now.$fqdn.req" )
  . ' -subj '   . escapeshellarg( $reqsub )
);

# Read created request file
$req = file_get_contents("$now.$fqdn.req")
  or die("Errorloading $now.$fqdn.req\n");

#=======================================================================
# Establish SOAP connection
#=======================================================================

# Define as function because it is needed several times
function newsoap($rootcafile){
  return new SoapClient(
    'https://pki.pca.dfn.de/dfn-ca-global-g2/cgi-bin/pub/soap?wsdl=1',
    [
      # This script requires these settings
      'trace'          => false,
      'exceptions'     => false,
      'features'       => SOAP_SINGLE_ELEMENT_ARRAYS,
      'cache_wsdl'     => WSDL_CACHE_NONE,
      # The next two lines only if
      # the proxy server must be used
      'proxy_host'     => 'wwwproxy.uni-muenster.de',
      'proxy_port'     => 3128,
      'stream_context' => stream_context_create([
        'http' => [
          'timeout'         => 120,
          # The next two lines only if
          # the proxy server must be used
          'proxy'           => 'tcp://wwwproxy.uni-muenster.de:3128',
          'request_fulluri' => true,
        ],
        'ssl' => [
          # Verification of the server certificate
          'verify_peer'     => true,
          'cafile'          => $rootcafile,
          'verify_depth'    => 3, # RootCA > DFN-PCA > WWUCA > Server
        ],
      ]),
    ]
  );
}

# Establish connection to the DFN-PKI certification server
$soap = newsoap("$now.$fqdn.root")
  or die("Error establishing the SOAP connection\n");

#=======================================================================
# 1st SOAP function call: Submit the request
#=======================================================================

$soapdata = $soap->newRequest(
  $raid,   # RA-ID
  $req,    # Request in PEM format
  $reqsan, # Array with Subject Alternative Names
  $profil, # Certificate profile
  $reqpin, # Revocation PIN hash
  $name,   # Name of requester
  $mail,   # Email of requester
  $unit,   # Organizational unit of requester
  true     # Mandantory: Publish the certificate
);
if($soapdata == '')
  die("Error submitting the request\n");
if(is_soap_fault($soapdata))
  die("Error submitting the request:\n"
    . "{$soapdata->faultcode}: {$soapdata->faultstring}\n");

# Response is the request number
$reqnum = intval($soapdata);

#=======================================================================
# 2nd SOAP function call: Retrieve request information
#=======================================================================

$soapdata = $soap->getRequestInfo(
  $raid,   # Same RA-ID as above
  $reqnum, # Request number, was assigned above
  $reqpin  # Same revocation PIN hash as above
);
if($soapdata == '')
  die("Error retrieving the request information\n");
if(is_soap_fault($soapdata))
  die("Error retrieving the request information:\n"
    . "{$soapdata->faultcode}: {$soapdata->faultstring}\n");

# Save and check required details from the respons
# $reqsub and $reqsan are adjusted if necessary
$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("Request information faulty\n");

#=======================================================================
# 3rd SOAP function call: Retrieve request printout
#=======================================================================

$soapdata = $soap->getRequestPrintout(
  $raid,             # Same RA-ID as above
  $reqnum,           # Request number, was assigned above
  'application/pdf', # Mandantory: PDF format
  $reqpin            # Same revocation PIN hash as above
);
if($soapdata == '')
  die("Error retrieving the PDF file\n");
if(is_soap_fault($soapdata))
  die("Error retrieving the PDF file:\n"
    . "{$soapdata->faultcode}: {$soapdata->faultstring}\n");

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

# This printout is no longer required, but the requester should at least
# be able to read which statement he confirms below.

#=======================================================================
# Terminate SOAP connection
#=======================================================================

unset($soap);

#=======================================================================
# Control output
#=======================================================================

# Subject, Subject Alternative Names, Public Key Digest
echo "\nRequest $reqnum to RA-ID $raid:\n$reqsub\n"
  . implode("\n", $reqsan)
  . "\n$digest\n\n";

#=======================================================================
# HTTPS function call: Submit confirmation to WWU IT portal
#=======================================================================

# Submit HTTPS post request with JSON data and TLS client
# authentification and retrieve JSON response
$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',
      ],
      # The next two lines only if
      # the proxy server must be used
      'proxy'            => 'tcp://wwwproxy.uni-muenster.de:3128',
      'request_fulluri'  => true,
      # All data in 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' => [
      # Verification of the server certificate
      'verify_peer'  => true,
      'cafile'       => "$now.$fqdn.root",
      'verify_depth' => 3, # RootCA > DFN-PCA > WWUCA > Server
      # Identify the requester with client certificate
      'local_cert'   => $pemfile,
      'passphrase'   => $pempass,
    ],
  ])
) or die("Error submitting the confirmation\n$data\n");
$json = json_decode($data, true, 9999, JSON_BIGINT_AS_STRING)
  or die("Response to confirmation faulty\n");
if(!$json['success'])
  die("Confirmation rejected:\n"
    . implode("\n", $json['errors']) . "\n");


#=======================================================================
# Loop: At large intervals try to retrieve the new certificate
#=======================================================================

# Initially wait at least 10 minutes, better 20
$nextsleep = 1200;

# Loop until the certificate is successfully fetched
while(true){

#=======================================================================
# Before waiting terminate any open SOAP connection
#=======================================================================

  unset($soap);

#=======================================================================
# Wait
#=======================================================================

  echo "Wait ...\n";
  sleep($nextsleep);

  # Between two tries wait at least 30 minutes, better 60
  $nextsleep = 3600;

#=======================================================================
# Establish SOAP connection
#=======================================================================

  $soap = newsoap("$now.$fqdn.root");
  if(!$soap){
    echo "SOAP server not reached\n";
    continue;
  }      

#=======================================================================
# 4th SOAP function call: Retrieve certificate
#=======================================================================

  $soapdata = $soap->getCertificateByRequestSerial(
    $raid,   # Same RA-ID as above
    $reqnum, # Request number, was assigned above
    $reqpin  # Same revocation PIN hash as above
  );
  if($soapdata == ''){
    echo "Error retrieving the certificate\n";
    continue;
  }
  if(is_soap_fault($soapdata)){
    echo "Error retrieving the certificate:\n"
      . "{$soapdata->faultcode}: {$soapdata->faultstring}\n";
    continue;
  }

  # Save certificate
  if(!file_put_contents("$now.$fqdn.cert", $soapdata)){
    echo "Error saving the certificate to $now.$fqdn.cert\n";
    continue;
  }

#=======================================================================
# End of loop: We have got the certicate
#=======================================================================

  break;
}

#=======================================================================
# Terminate SOAP connection
#=======================================================================

unset($soap);

#=======================================================================
# Control output
#=======================================================================

echo "\n"
  . "The digital ID is complete, see the following files:\n"
  . "Request file:                 $now.$fqdn.req\n"
  . "Request printout:             $now.$fqdn.pdf\n"
  . "Private key:                  $now.$fqdn.key\n"
  . "Server certificate:           $now.$fqdn.cert\n"
  . "Intermediate CA certificates: $now.$fqdn.chain\n"
  . "Root CA certificate:          $now.$fqdn.root\n"
  . "\n";

#=======================================================================
# Example: Install new digital ID into an Apache webserver
#=======================================================================

### # Replace configuration files
### 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("Error updating the server configuration\n");

### # Reload server configuration
### passthru('sudo systemctl reload httpd');

?>