HMAC verification with PHP
Learn how to secure your Docusign Connect webhook calls with HMAC verification
To secure your Connect webhook notification messages, Docusign recommends using the HMAC feature. With HMAC, your message is cryptographically hashed using a private key known only to you and Docusign. It’s set at the account level. The resulting hash key is included in the header of the notification message. When the message is received, your application repeats the hash operation and compares the result to the HMAC signature included in the header.
HMAC guarantees that the message is from Docusign and has not been altered on the way by a third party.
The problem: Testing your HMAC verification code
Regardless of your listener implementation, whether Platform-as-a-Service (PaaS) as suggested in Building best practices webhook listeners, part 4 or a custom listener, you face the issue of constantly resending envelopes while you test your application.
The PHP sample code below verifies the HMAC signature sent in the header of the notification message**.** To test the code on your localhost development machine, use the Postman replaying webhook messages technique.
Note that the HMAC hash should be verified before the request body is parsed. The request body is untrusted until the HMAC hash is verified.
Example: Checking the HMAC signature using vanilla PHP
<?php // Copyright (c) 2020 Docusign Inc. MIT License https://opensource.org/licenses/MIT
// save this file as index.php in your web server root folder
// enable only in development
error_reporting(E_ALL);
ini_set('display_errors', 1);
$secret = "your secret key";// your secret key
$payload = "";// request body
$headers = "";// request message headers array
$signature = "";// the HMAC hash key in the HTTP header x-docusign-signature-1
$result = false;// verification result
if (isset($_POST)) {
try {
$payload = file_get_contents('php://input');
$headers = get_ds_headers();
if (array_key_exists("XDocusignSignature1", $headers)) {
$signature = $headers["XDocusignSignature1"];
$result = hash_is_valid($secret, $payload, $signature);
log_result($signature, $payload, $result);
}
} catch (Exception $e) {
logger("\nException: " . $e->getMessage() . "\n");
}
header("HTTP/1.1 200 OK");
}
function get_ds_headers()
{
$headers = array();
foreach ($_SERVER as $key => $value) {
if (strpos($key, 'HTTP_') === 0) {
$headers[str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower(substr($key, 5)))))] = $value;
}
}
return $headers;
}
function compute_hash($secret, $payload)
{
$hexHash = hash_hmac('sha256', $payload, utf8_encode($secret));
$base64Hash = base64_encode(hex2bin($hexHash));
return $base64Hash;
}
function hash_is_valid($secret, $payload, $verify)
{
$computed_hash = compute_hash($secret, $payload);
eturn hash_equals($verify,$computed_hash);
}
function log_result($signature, $payload, $result)
{
$result_to_log = $result == 1 ? "pass" : "fail";
if ($result) {
try {
$xml = simplexml_load_string($payload);
$envelopeId = $xml->EnvelopeStatus->EnvelopeID;
$status = $xml->EnvelopeStatus->Status;
$created = $xml->EnvelopeStatus->Created;
} catch (Exception $e) {
logger("\nException: " . $e->getMessage() . "\n");
}
logger("\n");
logger("EnvelopeID: " . $envelopeId);
logger("Signature: " . $signature);
logger("HMAC check status: " . $result_to_log);
logger("Envelope Status: " . $status);
logger("Generated at: " . $created);
} else {
logger("\n");
logger("HMAC check status: " . $result_to_log);
logger("Generated at: " . date('Y-m-d H:i:s'));
}
}
function logger($txt)
{
$log_file = "log.txt";
$myfile = fopen($log_file, "a") or die("Unable to open file!");
fwrite($myfile, "\n" . $txt);
fclose($myfile);
}
?>
Summary
By using the Postman replay technique, you can test your HMAC verification implementation in your local environment.