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
- On your local machine, open a terimanl window & navigate to a temporary directory
- 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)
- Setup the webhook on github.
- After the PHP step, delete the ssh key files from your local machine.
Step 2: Setup the Webhook on Github
- Go to your repo online
- Click
Settings
->Webhooks
->Add Webhook
- Set the url to something like
https://YOUR-DOMAIN/webhook/update-repos/
- Open the
.pub
file from step 1 & Copy everything except trailing white-space - Paste the
.pub
file content (without trailing whitespace!) into theSecret
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
- Click
Add Webhook
- Click on the webhook you just made & scroll down.
- (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
- Log in to your server
-
ssh user@yourdomain.com
-
- 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)
-
cd ~/; mkdir private-keys; chmod ug+rx private-keys; chmod o-rwx private-keys; cd private-keys;
- Open the SSH key file from step 1, but the one with NO extension
- Copy the entire file contents (whitespace is fine)
- Back to ssh terminal:
nano github_private_key
-
ctrl+shift+v
to paste the file contents -
ctrl+x
->y
->enter
to save the file.
-
- Fix permissions on the new file (still in ssh terminal, in the
private-keys
directory)-
chmod o-rwx github_private_key; chmod ug-wx; chmod g-r;
- remove all permissions from the file, exceptuser read
- 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) -
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.
- Click
Redeliver
OR, you can do agit push
to your repository & refresh that webhook page. - 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.";
}