Verify secret key from Github webooks | hmac_hash

A Github Webhook will ping your server when your repository gets modified. Every time your server is pinged, you might want to do a git pull or something. You could get away with simply doing a git pull every time your url is pinged, but then your site could be abused in various ways. Plus, you might actually be interested in the body that Github is sending you!

TLDR; The Tricky Bits

Skip to step 4 for the full PHP script, if you already figured out the secret key aspect.

This is the part I really had a hard time figuring out - receiving the secret key & verifying it against my private key file.

$sshDir = `~/private-keys/`;    
//get the body as it was received by the server (which was all put into $_POST as an array)    
$body = trim(file_get_contents("php://input"));     
$headers = getallheaders();    
    
//if you want to know what repo was updated    
$repo = $_POST['repository']['full_name'];    
$keyFileName = 'github_private_key'; //or whatever you named the file on your server    
    
//This is the secret key, hashed with the body of the request    
$gitKeySHA1Hashed = $headers['X-Hub-Signature'];    
//this converts the private key to a public key and removes whitespace    
$localKeyRaw = trim(shell_exec('ssh-keygen -y -f '.$sshDir.'/'.$keyFileName));    
//github prefixes the key with `sha1=`, so we do too.     
// and we hash the key using the raw body sent to our server    
$localKeySHA1Hashed = 'sha1='.hash_hmac('sha1',$body,$localKeyRaw);    
    
// verify if the received hash is equal to the hash we generated.    
// apparently using === opens up a security issue regarding "timing attacks". I don't know what that means.    
$success = hash_equals($localKeySHA1Hashed,$gitKeySHA1Hashed);    
    
if ($success){    
    //do the stuff you want    
}    

Step 1: Make a secret key

  1. On your local machine, open a terimanl window & navigate to a temporary directory
  2. Execute ssh-keygen -t rsa -b 4096 -C "".
    • Name the file whatever you like. Don't use a password to protect it. (Unless you figure out how to make that work)
  3. Setup the webhook on github.
  4. After the PHP step, delete the ssh key files from your local machine.

Step 2: Setup the Webhook on Github

  1. Go to your repo online
  2. Click Settings -> Webhooks -> Add Webhook
  3. Set the url to something like https://YOUR-DOMAIN/webhook/update-repos/
  4. Open the .pub file from step 1 & Copy everything except trailing white-space
  5. Paste the .pub file content (without trailing whitespace!) into the Secret field.
    • Make sure content type is application/x-www-form-encoded
    • The other settings are probably fine as they are. Enable SSL Verification, Just the push event, Active
  6. Click Add Webhook
  7. Click on the webhook you just made & scroll down.
  8. (click to) Expand the ping that was sent. It doesn't matter if succeeded or failed. We'll use the Redeliver button later once we have the remote script setup.

Step 3: Upload the private key to your server

  1. Log in to your server
    • ssh user@yourdomain.com
  2. Put the private key somewhere on your server, preferably NOT where deliverable files are stored. (Just in case apache ever breaks & your files are exposed)
    1. cd ~/; mkdir private-keys; chmod ug+rx private-keys; chmod o-rwx private-keys; cd private-keys;
    2. Open the SSH key file from step 1, but the one with NO extension
    3. Copy the entire file contents (whitespace is fine)
    4. Back to ssh terminal: nano github_private_key
    5. ctrl+shift+v to paste the file contents
    6. ctrl+x -> y -> enter to save the file.
  3. Fix permissions on the new file (still in ssh terminal, in the private-keys directory)
    1. chmod o-rwx github_private_key; chmod ug-wx; chmod g-r; - remove all permissions from the file, except user read
    2. You MIGHT need to do chmod ug+r, if the group needs read access (depends on apache) or if the user didn't already have read acccess (which would be weird)
    3. ls -la - the file should have user read permissions (and maybe group) & nothing else

Step 4: PHP to verify the webhook you receive

References: Securing Your Webhook & push Event

Now you need to make a php file on your server that responds to the request to https://YOUR-DOMAIN/webhook/update-repos/ (or whatever url you used). In that file, put the following:

<?php    
$exitOnCompletion = false; //set true to exit at the end of this script    
    
$payload = $_POST['payload'];     
$json = json_decode($payload,true);    
    
$_POST = $json;    
$repoName = $_POST['repository']["name"];    
$wikiDir = dirname(dirname(dirname(__DIR__))).'/6-Wiki/';    
    
$sshDir = '~/private-keys/';    
$keyFileName = 'github_taeluf_webhook';    
    
echo "Start Verification\n";    
echo "Working on repo '{$repoName}'";    
    
    
function verifyGithubWebhook($sshDir, $keyFileName,$writeLog=false,$printLog=false){    
    $body = trim(file_get_contents("php://input"));    
    $headers = getallheaders();    
        
    $log = $sshDir.'/log';    
    $repo = $_POST['repository']['full_name'];    
    $keyFileName = 'github_taeluf_webhook';    
    
    
    $gitKeySHA1Hashed = $headers['X-Hub-Signature'];    
    $localKeyRaw = trim(shell_exec('ssh-keygen -y -f '.$sshDir.'/'.$keyFileName));    
    $localKeySHA1Hashed = 'sha1='.hash_hmac('sha1',$body,$localKeyRaw);    
    
    $success = hash_equals($localKeySHA1Hashed,$gitKeySHA1Hashed);    
    
    if ($writeLog||$printLog){    
        $logData = "Repo '{$repo}'\n"."Received:\n{$gitKeySHA1Hashed}\n\nLocallyHashed:\n{$localKeySHA1Hashed}\n\n";    
        $logData .= $success ? 'SUCCESS' : 'FAILURE';    
        $logData .= "\n\n----------------------------------\n\n";    
    }    
    if ($writeLog)file_put_contents($log,$logData,FILE_APPEND|LOCK_EX);    
    if ($printLog)echo $logData;    
    
    return $success;        
}    
    
if (verifyGithubWebhook($sshDir,$keyFileName)){    
    echo "Github webhook verified successfully.\n";    
    //do some stuff if you wanna    
} else {    
    echo "The github webhook failed to verify.";    
}    
    
    
if ($exitOnCompletion)exit;    

Step 5: Check your work

Now you just need to go back to that github webhook page.

  1. Click Redeliver OR, you can do a git push to your repository & refresh that webhook page.
  2. Go to the Response tab for the redelivered-hook & it SHOULD say that your webhook was verified successfully. Hope so, anyway, lol

Bonus Step: Setup a git pull

For bonus points, you can set up a git pull from PHP once you've verified the webhook. Change the above script with this code:

    
$gitProjectDir = //a path you feel is safe. Don't put it somewhere that PHP files might be executed by Apache or your CMS, unless that's your intent.    
$githubUrl = "https://github.com/Taeluf/"; //change this!!! Unless you're hosting my repos for some weird reason...    
    
function isValidGitRepo($repoName){    
    
    $validRepos = [    
        //these are some of my repo names, at the time of writing    
        //change them! I have them hard-coded so that we ONLY pull repos that I very explicitly have allowed    
        'PHP-Documentor',    
        'Liaison',    
        'Better-Regex',    
        'Wikitten-Liaison',    
    ];    
    
    if (in_array($repoName,$validRepos))return true;    
    
    return false;    
}    
    
function updateGitRepo($dir,$repoName,$pullUrl){    
    $proj = $repoName;    
    if (!isValidGitRepo($repoName)){    
        echo "Repo name '{$repoName}' is invalid.";    
        return;    
    }    
    if (!is_dir($dir)){    
        echo "project root dir '{$dir}' does not exist. Cannot update git repo.";    
        return;    
    }    
    //you'll need to change this url, of course.    
    $url = $pullUrl.$proj.'.git';    
    $projectDir = $dir.$proj.'/';    
    $dirCheck = $projectDir.'.git';    
    if (is_dir($dirCheck)){    
        $command = "cd {$projectDir};\ngit pull;";    
        $output = shell_exec($command);    
        echo "Did git pull on '{$proj}'\n<br>\n";    
        return;    
    }     
    if (is_dir($projectDir)&&count(scandir($projectDir))>2){    
        echo "We can't git clone or git pull '{$proj}' because the directory exists, has content, but does NOT have a .git directory.\n<br>\n";    
        return;    
    }    
    $command = "cd {$dir};\ngit clone {$pullUrl}{$proj}.git";    
    $output = shell_exec($command);    
    echo "Did git clone on '{$proj}'\n<br>\n";    
    
}    
    
    
if (verifyGithubWebhook($sshDir,$keyFileName)){    
    echo "Github webhook verified successfully.\n";    
    updateGitRepo($gitProjectsDir, $repoName, $githubUrl); //make sure you change this url    
    if ($repoName==''){    
        print_r($_POST);    
    }    
} else {    
    echo "The github webhook failed to verify.";    
}