Dealing with Trusted Timestamps in PHP (RFC 3161)

This article won’t be pariculary interesting for the most readers. My aim is to help the billions of developers that are confused about dealing with trusted timestamping. If you don’t know what Trusted Timestamps are, carry on.

Explanation of the concept behind Trusted Timestamps

I can’t put it better as Wikipedia (Trusted timestamping) does:

Trusted timestamping is the process of securely keeping track of the creation and modification time of a document. Security here means that no one — not even the owner of the document — should be able to change it once it has been recorded provided that the timestamper’s integrity is never compromised.

The workflow is as follows:

  1. You create a hash of a file / database-columns / whatever in sha1 or md5
  2. You create a timestamp-requestfile from your hash via the openssl ts-command
  3. You send the requestfile to a Timestamp Authority (TSA). I will use the DFN timestamp-service but you are free to use any TSA that follows the RFC 3161 in which Trusted Timestamping is defined
  4. The TSA responds with a confusing human-unreadable binary string (Timestamp-Response) that you can store in a file or in a database. This response is signed with a certificate of the TSA so that you can’t manipulate the string or simply switch the TSA afterwards.
  5. You are now able to check if someone has changed the signed data when you recalculate the hash (step 1) and validate it against the Timestamp-Response.

Why has this to be so complicated?

Trusted Timestamping is particulary interesting for applications which deal with very critical data. You can’t simply store a hash in your database when the attacker might have access to all your systems and comes from within your company (e.g. the admin itself). He would be able to simply manipulate the hashed data and overwrite the old hash in your database. Another point is to prove in court that there has no manipulation happened – definitely necessary when dealing with money.

Preconditions

As mentioned above, this requires the openssl ts-command which is availible in openssl versions newer than 0.99 (check with command openssl version).

  • Linux: The version that ships with Debian is older than that and does not support the openssl ts-command. In this case, you have to use the Debian Experimentals.
  • When you’re under Windows (e.g. local testing environment), you have to look here (The light package will do. If needed, the Visual C++ 2008 Redistributables have to be installed in advance. Also don’t forget to add X:\path\to\openssl\bin to your PATH).

Furthermore CURL is required. Ah, and you have to be able to use the exec-function of PHP, which might be disabled at some shared hosts.

Let’s go!

We’ll be creating a Trusted Timestamp-Class that is able to handle anything from creating requestfiles over signing them at the TSA to verifying the integrity. Let’s start.

Create the Requestfile


public static function createRequestfile ($hash)
{
	if (strlen($hash) !== 40)
		throw new Exception("Invalid hash.");
		
	$outfilepath = self::createTempFile();
	$cmd = "openssl ts -query -digest ".escapeshellarg($hash)." -cert -out ".escapeshellarg($outfilepath);

	$retarray = array();
	exec($cmd." 2>&1", $retarray, $retcode);
	
	if ($retcode !== 0)
		throw new Exception("OpenSSL does not seem to be installed: ".implode(", ", $retarray));
	
	if (stripos($retarray[0], "openssl:Error") !== false)
		throw new Exception("There was an error with OpenSSL. Is version >= 0.99 installed?: ".implode(", ", $retarray));

	return $outfilepath;
}

You throw a hash in and the method returns the path to the newly created Timestamp Requestfile. If you’re curious what the 2>&1 appendix to the executed command is for: This redirects the error-stream STDERR to STDOUT, so exec will be able to also return the error description if something goes wrong.

Sign Requestfile

public static function signRequestfile ($requestfile_path, $tsa_url)
{
	if (!file_exists($requestfile_path))
		throw new Exception("The Requestfile was not found");

	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $tsa_url);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_TIMEOUT, 10);
	curl_setopt($ch, CURLOPT_POST, 1);
	curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
	curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents($requestfile_path));
	curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/timestamp-query'));
	curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)"); 
	$binary_response_string = curl_exec($ch);
	$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
	curl_close($ch);
	
	if ($status != 200 || !strlen($binary_response_string))
		throw new Exception("The request failed");
	
	$base64_response_string = base64_encode($binary_response_string);
	
	$response_time = self::getTimestampFromAnswer ($base64_response_string);
	
	return array("response_string" => $base64_response_string,
				 "response_time" => $response_time);
}

Now that we’ve created our Requestfile, we want to send it to the TSA in order to get our Timestamp-Response binary certificate signed string. For that, we use CURL to transmit our Requestfile. After we get our response, we base64-encode it because handling with binary strings can be a pain in the ass, especially when you want to write it to the database. We return the base64-encoded responsestring and the extracted unix timestamp (will be shown in the next method).

Get the Unix-Timestamp from the Timestamp Response

public static function getTimestampFromAnswer ($base64_response_string)
{
	$binary_response_string = base64_decode($base64_response_string);

	$responsefile = self::createTempFile($binary_response_string);

	$cmd = "openssl ts -reply -in ".escapeshellarg($responsefile)." -text";
	
	$retarray = array();
	exec($cmd." 2>&1", $retarray, $retcode);
	
	if ($retcode !== 0)
		throw new Exception("The reply failed: ".implode(", ", $retarray));
	
	$matches = array();
	$response_time = 0;

	/*
	 * Format of answer:
	 * 
	 * Foobar: some stuff
	 * Time stamp: 21.08.2010 blabla GMT
	 * Somestuff: Yayayayaya
	 */
	foreach ($retarray as $retline)
	{
		if (preg_match("~^Time\sstamp\:\s(.*)~", $retline, $matches))
		{
			$response_time = strtotime($matches[1]);
			break;		
		}
	}

	if (!$response_time)
		throw new Exception("The Timestamp was not found");	
		
	return $response_time;
}

We want to be sure that an attacker can’t simply make a new TSA request to sign his manipulated data and delete the old TSA request with the real data. So we have to pull the exact unix timestamp from the Timestamp Response, when our Request has been signed.

The Validation

public static function validate ($hash, $base64_response_string, $response_time, $tsa_cert_file)
{
	if (strlen($hash) !== 40)
		throw new Exception("Invalid Hash");
	
	$binary_response_string = base64_decode($base64_response_string);
	
	if (!strlen($binary_response_string))
		throw new Exception("There was no response-string");	
		
	if (!intval($response_time))
		throw new Exception("There is no valid response-time given");
	
	if (!file_exists($tsa_cert_file))
		throw new Exception("The TSA-Certificate could not be found");
	
	$responsefile = self::createTempFile($binary_response_string);

	$cmd = "openssl ts -verify -digest ".escapeshellarg($hash)." -in ".escapeshellarg($responsefile)." -CAfile ".escapeshellarg($tsa_cert_file);
	
	$retarray = array();
	exec($cmd." 2>&1", $retarray, $retcode);
	
	/*
	 * just 2 "normal" cases: 
	 * 	1) Everything okay -> retcode 0 + retarray[0] == "Verification: OK"
	 *  2) Hash is wrong -> retcode 1 + strpos(retarray[somewhere], "message imprint mismatch") !== false
	 * 
	 * every other case (Certificate not found / invalid / openssl is not installed / ts command not known)
	 * are being handled the same way -> retcode 1 + any retarray NOT containing "message imprint mismatch"
	 */
	
	if ($retcode === 0 && strtolower(trim($retarray[0])) == "verification: ok")
	{
		if (self::getTimestampFromAnswer ($base64_response_string) != $response_time)
			throw new Exception("The responsetime of the request was changed");
		
		return true;
	}

	foreach ($retarray as $retline)
	{
		if (stripos($retline, "message imprint mismatch") !== false)
			return false;
	}

	throw new Exception("Systemcommand failed: ".implode(", ", $retarray));
}

Wohow, that’s a lot of code. But what’s happening isn’t very complicated. We do some verification of our parameters and then mess around with return-codes from the openssl ts-command to see if our hash is still valid when checking against our Timestamp Response. We also check, if the unix timestamp of signing is still the same. It’s important to mention that we need the certificate chain (usually pem-format) of the TSA. Our method expects the path to the certificate-file in the last parameter. The certificate chain for the DFN-Service can be found here.

Our Helper-Function to create a tempfile

public static function createTempFile ($str = "")
{
	$tempfilename = tempnam(sys_get_temp_dir(), rand());

	if (!file_exists($tempfilename))
		throw new Exception("Tempfile could not be created");
		
	if (!empty($str) && !file_put_contents($tempfilename, $str))
		throw new Exception("Could not write to tempfile");

	return $tempfilename;
}

Umm… nothing spectacular.

How do I use this class?

require_once "TrustedTimestamps.php";

$my_hash = sha1("Some Data for testing");

$requestfile_path = TrustedTimestamps::createRequestfile($my_hash);

$response = TrustedTimestamps::signRequestfile($requestfile_path, "http://zeitstempel.dfn.de");
print_r($response);
/*
Array
(
    [response_string] => Shitload of text (base64-encoded Timestamp-Response of the TSA)
    [response_time] => 1299098823
)
*/

echo TrustedTimestamps::getTimestampFromAnswer($response['response_string']); //1299098823

$tsa_cert_chain_file = "chain.txt"; //from https://pki.pca.dfn.de/global-services-ca/pub/cacert/chain.txt

$validate = TrustedTimestamps::validate($my_hash, $response['response_string'], $response['response_time'], $tsa_cert_chain_file); 
var_dump($validate); //bool(true)

//now with an incorrect hash. Same goes for a manipulated response string or response time
$validate = TrustedTimestamps::validate(sha1("im not the right hash"), $response['response_string'], $response['response_time'], $tsa_cert_chain_file);
var_dump($validate); //bool(false)

The code should speak for itself.

I don’t want to copy and paste all your little code samples!

Okay, okay… There you are, the complete class:

<?php
/**
 * TrustedTimestamps.php - Creates Timestamp Requestfiles, processes the request at a Timestamp Authority (TSA) after RFC 3161
 *
 * Released under the MIT license (opensource.org/licenses/MIT) Copyright (c) 2015 David Müller
 *
 * bases on OpenSSL and RFC 3161: http://www.ietf.org/rfc/rfc3161.txt
 *
 * WARNING: 
 * 	needs openssl ts, which is availible in OpenSSL versions >= 0.99
 * 	This is currently (2011-03-02) not the case in Debian
 * 	(see http://stackoverflow.com/questions/5043393/openssl-ts-command-not-working-trusted-timestamps)
 * 	-> Possibility: Debian Experimentals -> http://wiki.debian.org/DebianExperimental
 * 
 * For OpenSSL on Windows, see
 * 	http://www.slproweb.com/products/Win32OpenSSL.html
 * 	http://www.switch.ch/aai/support/howto/openssl-windows.html
 * 
 * @version 0.3
 * @author David Müller
 * @package trustedtimestamps
*/

class TrustedTimestamps
{
    /**
     * Creates a Timestamp Requestfile from a hash
     *
     * @param string $hash: The hashed data (sha1)
     * @return string: path of the created timestamp-requestfile
     */
	public static function createRequestfile ($hash)
	{
		if (strlen($hash) !== 40)
			throw new Exception("Invalid Hash.");
			
		$outfilepath = self::createTempFile();
		$cmd = "openssl ts -query -digest ".escapeshellarg($hash)." -cert -out ".escapeshellarg($outfilepath);

		$retarray = array();
		exec($cmd." 2>&1", $retarray, $retcode);
		
		if ($retcode !== 0)
			throw new Exception("OpenSSL does not seem to be installed: ".implode(", ", $retarray));
		
		if (stripos($retarray[0], "openssl:Error") !== false)
			throw new Exception("There was an error with OpenSSL. Is version >= 0.99 installed?: ".implode(", ", $retarray));

		return $outfilepath;
	}

    /**
     * Signs a timestamp requestfile at a TSA using CURL
     *
     * @param string $requestfile_path: The path to the Timestamp Requestfile as created by createRequestfile
     * @param string $tsa_url: URL of a TSA such as http://zeitstempel.dfn.de
     * @return array of response_string with the unix-timetamp of the timestamp response and the base64-encoded response_string
     */
	public static function signRequestfile ($requestfile_path, $tsa_url)
	{
		if (!file_exists($requestfile_path))
			throw new Exception("The Requestfile was not found");

		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $tsa_url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_TIMEOUT, 10);
		curl_setopt($ch, CURLOPT_POST, 1);
		curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
		curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents($requestfile_path));
		curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/timestamp-query'));
		curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)"); 
		$binary_response_string = curl_exec($ch);
		$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		curl_close($ch);
		
		if ($status != 200 || !strlen($binary_response_string))
			throw new Exception("The request failed");
		
		$base64_response_string = base64_encode($binary_response_string);
		
		$response_time = self::getTimestampFromAnswer ($base64_response_string);
		
		return array("response_string" => $base64_response_string,
					 "response_time" => $response_time);
	}

    /**
     * Extracts the unix timestamp from the base64-encoded response string as returned by signRequestfile
     *
     * @param string $base64_response_string: Response string as returned by signRequestfile
     * @return int: unix timestamp
     */
	public static function getTimestampFromAnswer ($base64_response_string)
	{
		$binary_response_string = base64_decode($base64_response_string);

		$responsefile = self::createTempFile($binary_response_string);

		$cmd = "openssl ts -reply -in ".escapeshellarg($responsefile)." -text";
		
		$retarray = array();
		exec($cmd." 2>&1", $retarray, $retcode);
		
		if ($retcode !== 0)
			throw new Exception("The reply failed: ".implode(", ", $retarray));
		
		$matches = array();
		$response_time = 0;

		/*
		 * Format of answer:
		 * 
		 * Foobar: some stuff
		 * Time stamp: 21.08.2010 blabla GMT
		 * Somestuff: Yayayayaya
		 */
		foreach ($retarray as $retline)
		{
			if (preg_match("~^Time\sstamp\:\s(.*)~", $retline, $matches))
			{
				$response_time = strtotime($matches[1]);
				break;		
			}
		}

		if (!$response_time)
			throw new Exception("The Timestamp was not found");	
			
		return $response_time;
	}

    /**
     *
     * @param string $hash: sha1 hash of the data which should be checked
     * @param string $base64_response_string: The response string as returned by signRequestfile
     * @param int $response_time: The response time, which should be checked
     * @param string $tsa_cert_file: The path to the TSAs certificate chain (e.g. https://pki.pca.dfn.de/global-services-ca/pub/cacert/chain.txt)
     * @return <type>
     */
	public static function validate ($hash, $base64_response_string, $response_time, $tsa_cert_file)
	{
		if (strlen($hash) !== 40)
			throw new Exception("Invalid Hash");
		
		$binary_response_string = base64_decode($base64_response_string);
		
		if (!strlen($binary_response_string))
			throw new Exception("There was no response-string");	
			
		if (!intval($response_time))
			throw new Exception("There is no valid response-time given");
		
		if (!file_exists($tsa_cert_file))
			throw new Exception("The TSA-Certificate could not be found");
		
		$responsefile = self::createTempFile($binary_response_string);

		$cmd = "openssl ts -verify -digest ".escapeshellarg($hash)." -in ".escapeshellarg($responsefile)." -CAfile ".escapeshellarg($tsa_cert_file);
		
		$retarray = array();
		exec($cmd." 2>&1", $retarray, $retcode);
		
		/*
		 * just 2 "normal" cases: 
		 * 	1) Everything okay -> retcode 0 + retarray[0] == "Verification: OK"
		 *  2) Hash is wrong -> retcode 1 + strpos(retarray[somewhere], "message imprint mismatch") !== false
		 * 
		 * every other case (Certificate not found / invalid / openssl is not installed / ts command not known)
		 * are being handled the same way -> retcode 1 + any retarray NOT containing "message imprint mismatch"
		 */
		
		if ($retcode === 0 && strtolower(trim($retarray[0])) == "verification: ok")
		{
			if (self::getTimestampFromAnswer ($base64_response_string) != $response_time)
				throw new Exception("The responsetime of the request was changed");
			
			return true;
		}

		foreach ($retarray as $retline)
		{
			if (stripos($retline, "message imprint mismatch") !== false)
				return false;
		}

		throw new Exception("Systemcommand failed: ".implode(", ", $retarray));
	}

    /**
     * Create a tempfile in the systems temp path
     *
     * @param string $str: Content which should be written to the newly created tempfile
     * @return string: filepath of the created tempfile
     */
	public static function createTempFile ($str = "")
	{
		$tempfilename = tempnam(sys_get_temp_dir(), rand());

		if (!file_exists($tempfilename))
			throw new Exception("Tempfile could not be created");
			
		if (!empty($str) && !file_put_contents($tempfilename, $str))
			throw new Exception("Could not write to tempfile");

		return $tempfilename;
	}
}

I hope you will find any use in this, don’t hesitate to ask if something is not clear to you.

License

This class is released under the MIT License.

Credits: Thanks to the DFN!

The guys at the DFN gave me the allowance to use their service as an example for my blogpost. The service is free. You’ll find further information to the DFN service at this point. Please also respect the DFN Charter (Google Translator is your friend).

Weitere Posts:

Dieser Beitrag wurde unter php, Security, webdev veröffentlicht. Setze ein Lesezeichen auf den Permalink.

11 Antworten auf Dealing with Trusted Timestamps in PHP (RFC 3161)

  1. Toller Artikel, wirklich interessantes Thema (kannte ich vorher noch nicht wirklich). Hinweis noch: Ganz oben fehlt ein ‚r‘ bei „Tusted…“ :)

    Liebe Grüße

  2. Basil sagt:

    Hallo, ich bin Musiker und Komponist und versuche zu verstehen, wie ich diese Methode dafür benutzen kann, um meine geschriebene Musik mit so einem Zeitstempel zu versehen, der auch vor Gericht als Urhebernachweis dienen kann. Ich würde mich über Hilfe sehr freuen! :)

    Liebe Grüße

  3. mik sagt:

    I’m testing this on a private service but I have a „The request failed“ message.
    How to use this class on a service with user and password assigned?
    Thanks

  4. Danibert sagt:

    Hi David,

    vielen Dank für diesen tollen Beitrag. Ich habe dazu eine Frage:
    Mein Webhoster stellt mir OpenSSL leider nur in der Version 0.9.80 zur Verfügung. Gibt es eine Chance, das Ganze auch damit zum Laufen zu bekommen? Was müsste ich dafür am Scriptbeispiel abändern?

    Danke Dir und mach weiter mit diesem tollen Blog

  5. Nick Borgers sagt:

    Thanks for writing and publishing this. I will refer to it while I build another web interface for monitoring our TSA servers.

  6. Miguel sagt:

    Thanks for the info!

    What should the mysql field size be if I need to store the response of the TSA for each of the mysql record that has to be time stamped?

    Can you point me to other books or urls where I can educate myself on this timestamping thing?

  7. Swen sagt:

    Hallo,

    Wie die Zeitstempel mit dieser Klasse erstellt werden später validiert. Du anderen vertrauenswürdigen Zeitstempeldienste wissen, dass ich eine gefunden bei tecxoft tsa.

    Vielen Dank für den nützlichen Beitrag

  8. MICHAIL CHOURDAKIS sagt:

    Hello.
    Thanks for the code, it works, but do you know how to extract the certificate from the time stamp?

    Thanks a lot.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.