Using PHP to create self-signed X.509 Client Certificates

The following is a recipe on how to create a self-signed X.509 client certificates.
This is one of the steps to implement the FOAF + SSL protocol as outlined by Henry Story.

1. Assumptions
TOP
BACK
NEXT

The starting assumption of this recipe is that your PHP installation has the OpenSSL PHP extension installed and working.
If not the instructions are here: http://uk2.php.net/manual/en/intro.openssl.php

2. Add SubjectAltName to openssl.cnf
TOP
BACK
NEXT

The FOAF+SSL protocol requires that the URI of your FOAF file is stored in the "subjectAltName" within the X509v3 extensions section.
To do this add the following line to the openssl.cnf of your OpenSSL installation
subjectAltName=${ENV::SAN}
This directive should be added to the 'x509_extensions' section.

In our case it was within the [usr_cert] section of /usr/share/ssl/openssl.cnf as specifiec by this directive 'x509_extensions = usr_cert # The extentions to add to the cert '

Adding as an env variable allows us to set this before the openssl certificates creation calls without editting the openssl.cnf file further.

3. Create an array with the appropriate configuration for the openssl function calls
TOP
BACK
NEXT
	// Setup the contents of the subjectAltName
	if ($foafLocation)
		$SAN="URI:$foafLocation";

	if ($emailAddress) 
	{
		if ($SAN)
			$SAN.=",email:$emailAddress";
		else
			$SAN="email:$emailAddress";
	}

	// Export the subjectAltName to be picked up by the openssl.cnf file
	if ($SAN)
	{
		putenv("SAN=$SAN");
	}

	// Create the array to hold the configuration options for the openssl function calls
	// TODO - This should be more easily configured
	$config = array('config'=>'/usr/share/ssl/openssl.cnf');

	if ($SAN)
	{
		// TODO - This should be more easily configured
		$config = array_merge($config, array('x509_extensions' => 'usr_cert'));
	}
4. Create a private/public key pair
TOP
BACK
NEXT

NOTE: The private/public key pair is only held in memory at this stage.
// Generate a new private (and public) key pair
$privkey = openssl_pkey_new($config);

if ($privkey==FALSE) 
{
	while (($e = openssl_error_string()) !== false)
	{
		echo $e . "\n";
		print "<br><br>";
	}
}
5. Generate a certificate signing request
TOP
BACK
NEXT

NOTE: The signing request is only held in memory.
$dn = array(
    "countryName" => "UK",
    "stateOrProvinceName" => "Somerset",
    "localityName" => "Glastonbury",
    "organizationName" => "The Brain Room Limited",
    "organizationalUnitName" => "PHP Documentation Team",
    "commonName" => "Wez Furlong",
    "emailAddress" => "wez@example.com"
);

// Generate a certificate signing request
$csr = openssl_csr_new($dn, $privkey, $config);

if (!$csr)
{
	while (($e = openssl_error_string()) !== false) 
	{
		echo $e . "\n";
		print "<br><br>";
	}
}
6. Generate a self-signed certificate
TOP
BACK
NEXT

NOTE: The certificate is only held in memory.
// You will usually want to create a self-signed certificate at this
// point until your CA fulfills your request.
// This creates a self-signed cert that is valid for 365 days
$sscert = openssl_csr_sign($csr, null, $privkey, 365, $config);

if ($sscert==FALSE) 
{
	while (($e = openssl_error_string()) !== false)
	{
		echo $e . "\n";
		print "<br><br>";
	}
}
7. Generate a PKCS12
TOP
BACK
NEXT

NOTE: The PKCS12 certificate is only held in memory.
if (openssl_pkcs12_export($sscert, $p12Out, $privkey, $p12Password)==FALSE)
{
	// Show any errors that occurred here
	while (($e = openssl_error_string()) !== false) 
	{
		echo $e . "\n";
		print "<br><br>";
	}
}
8. function create_identity_p12()
TOP
BACK
NEXT

Putting it all in a single function.
// Returns a p12 encoded SSL certificate
function create_identity_p12( 
	$countryName,  $stateOrProvinceName, $localityName, $organizationName, $organizationalUnitName, $commonName, $emailAddress,
	$foafLocation, $p12Password)
{
	// Create the DN array for the openssl function calls
	if ($countryName)
		$dn = array("countryName" => $countryName);

	if ($stateOrProvinceName)
	{	
		if ($dn)
			$dn = array_merge($dn, array("stateOrProvinceName" => $stateOrProvinceName));
		else
			$dn = array("stateOrProvinceName" => $stateOrProvinceName);
	}

	if ($localityName)
	{
		if ($dn)
			$dn = array_merge($dn, array("localityName" => $localityName));
		else
			$dn = array("localityName" => $localityName);
	}

	if ($organizationName)
	{
		if ($dn)
			$dn = array_merge($dn, array("organizationName" => $organizationName));
		else
			$dn = array("organizationName" => $organizationName);
	}

	if ($organizationalUnitName)
	{
		if ($dn)
			$dn = array_merge($dn, array("organizationalUnitName" => $organizationalUnitName));
		else
			$dn = array("organizationalUnitName" => $organizationalUnitName);
	}

	if ($commonName)
	{
		if ($dn)
			$dn = array_merge($dn, array("commonName" => $commonName));
		else
			$dn = array("commonName" => $commonName);
	}

	if ($emailAddress) 
	{
		if ($dn)
			$dn = array_merge($dn, array("emailAddress" => $emailAddress));
		else
			$dn = array("emailAddress" => $emailAddress);
	}

	// if the $dn array is NULL at this point set country name to the default of GB
	if (!$dn)
		$dn = array("countryName" => "GB");

	// Setup the contents of the subjectAltName
	if ($foafLocation)
		$SAN="URI:$foafLocation";

	if ($emailAddress) 
	{
		if ($SAN)
			$SAN.=",email:$emailAddress";
		else
			$SAN="email:$emailAddress";
	}

	// Export the subjectAltName to be picked up by the openssl.cnf file
	if ($SAN)
	{
		putenv("SAN=$SAN");
	}

	// Create the array to hold the configuration options for the openssl function calls
	// TODO - This should be more easily configured
	$config = array('config'=>'/usr/share/ssl/openssl.cnf');

	if ($SAN)
	{
		// TODO - This should be more easily configured
		$config = array_merge($config, array('x509_extensions' => 'usr_cert'));
	}

	// Generate a new private (and public) key pair
	$privkey = openssl_pkey_new($config);

	if ($privkey==FALSE) 
	{
		// Show any errors that occurred here
		while (($e = openssl_error_string()) !== false)
		{
			echo $e . "\n";
			print "<br><br>";
		}
	}

	// Generate a certificate signing request
	$csr = openssl_csr_new($dn, $privkey, $config);

	if (!$csr)
	{
		// Show any errors that occurred here
		while (($e = openssl_error_string()) !== false) 
		{
			echo $e . "\n";
			print "<br><br>";
		}
	}

	// You will usually want to create a self-signed certificate at this
	// point until your CA fulfills your request.
	// This creates a self-signed cert that is valid for 365 days
	$sscert = openssl_csr_sign($csr, null, $privkey, 365, $config);

	if ($sscert==FALSE) 
	{
		// Show any errors that occurred here
		while (($e = openssl_error_string()) !== false)
		{
			echo $e . "\n";
			print "<br><br>";
		}
	}

	if (openssl_pkcs12_export($sscert, $p12Out, $privkey, $p12Password)==FALSE)
	{
		// Show any errors that occurred here
		while (($e = openssl_error_string()) !== false) 
		{
			echo $e . "\n";
			print "<br><br>";
		}
	}

	return $p12Out;
}
9. function download_identity_p12()
TOP
BACK
NEXT
// Send the p12 encoded SSL certificate as a file transfer
function download_identity_p12($p12, $foafLocation)
{
	// set headers
	header("Pragma: private");
	header("Expires: 0");
	header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
	header("Cache-Control: private");
	header("Content-Description: File Transfer");
	header("Content-Type: application/x-pkcs12");
	
	$file = basename($foafLocation);

	header("Content-Disposition: attachment; filename=\"$file.p12\"");
	header("Content-Transfer-Encoding: binary");
	header("Content-Length: " . strlen($p12));

	print($p12);

	flush();

	if (connection_status()!=0) 
	{
		@fclose($file);
		die();
	}

}
10. cert.php
TOP
BACK
NEXT
// Check if the foaf loaction is specified in the script call
$foafLocation = $_GET[foaf];
if (!$foafLocation)
{
	if (array_key_exists('foaf', $_GET))
		$query = $_SERVER[QUERY_STRING];
	else
		$query = ($_SERVER[QUERY_STRING]?$_SERVER[QUERY_STRING]."&":"") . "foaf=";

	print "Please specify the location of your foaf file. <a href='https://foaf.me/cert.php?" . $query . 
	"'>https://foaf.me/cert.php?foaf=</a><font color='red'><b>http://foaf.me/nickname</b></font><br><br>
	The FOAF location is added to the SubjectAltName within the SSL Client Certificate<br>";

	exit();
}

// Check that script is called using the HTTPS protocol
if ($_SERVER[HTTPS] == NULL)
{
	print "Please use the following secure uri to download the Identity P12. <a href='https://foaf.me/cert.php?" . $_SERVER[QUERY_STRING] . 
	"'>https://foaf.me/cert.php?" . $_SERVER[QUERY_STRING] . "</a><br>";

	exit();
}

// Get the rest of the script parameters
$countryName			= $_GET[countryName];
$stateOrProvinceName		= $_GET[stateOrProvinceName];
$localityName			= $_GET[localityName];
$organizationName		= $_GET[organizationName];
$organizationalUnitName	= $_GET[organizationalUnitName];
$commonName			= $_GET[commonName];
$emailAddress			= $_GET[emailAddress];
$p12Password			= $_GET[password];

// Create a p12 encoded SSL certificate
if ( $p12 = create_identity_p12(
			$countryName, $stateOrProvinceName, $localityName, $organizationName, $organizationalUnitName, $commonName, $emailAddress,
			$foafLocation, $p12Password ) )
{	
	// Send the p12 encoded SSL certificate to the script caller as a file transfer
	download_identity_p12($p12, $foafLocation);
}
11. Example Form
TOP
BACK
NEXT

Simple Create Client Certificate Form


12. Downloads
TOP
BACK
NEXT

13. Security Considerations
TOP
BACK
NEXT

The server script has access to the private/public key pair. A malicious script could farm this information for later use.

14. See Also
TOP
BACK
NEXT
15. External Links
TOP
BACK
NEXT