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.


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


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.

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

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>";
	}
}

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>";
	}
}

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>";
	}
}

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>";
	}
}

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;
}
// 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();
	}

}
// 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);
}

Simple Create Client Certificate Form




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