Update endpoints.php to use whck for secret validation

This commit is contained in:
southerntofu 2022-02-20 17:38:42 +01:00
parent 20c7059fd8
commit 4cffbb64b3
4 changed files with 113 additions and 92 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "spec"]
path = spec
url = https://tildegit.org/forge/endpoints
[submodule "whck"]
path = whck
url = https://tildegit.org/forge/whck

199
index.php
View File

@ -54,70 +54,102 @@ function extract_header($header) {
return $value;
}
function verify_signature($payload, $secret, $claimed_signature) {
$payload_signature = hash_hmac('sha256', $payload, $secret, false);
// check payload signature against header signature
if ($claimed_signature != $payload_signature) {
error('FAILED - payload signature mismatch', 403);
function verify_claim(string $payload, string $claim, string $id, string $validator) {
$whck = find_whck();
switch ($validator) {
case "hmac-sha256":
$kind = "hmac-sha256";
break;
case "token":
$kind = "token";
break;
default:
error("Programming error", 500);
break;
}
}
function verify_token($secret, $claimed_secret) {
if ($secret !== $claimed_secret) {
error('FAILED - secret token mismatch', 403);
$descriptors = [ array("pipe", "r"), array("pipe", "w"), array("pipe", "w") ];
//$cmd = $whck." " . $kind . " ".escapeshellarg($id) . " " . escapeshellarg($claim);
$cmd = [ $whck, $kind, $id, $claim ];
var_dump($cmd);
//echo "$cmd";
$proc = proc_open($cmd, $descriptors, $pipes);
if (is_resource($proc)) {
// Write payload to STDIN of whck
fwrite($pipes[0], $payload);
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
} else {
error("FAILED - could not start whck", 500);
}
$status = proc_close($proc);
if ($status != 0) {
error("Process failed due to error. STDOUT:\n$stdout\n\nSTDERR:\n$stderr", 500);
}
}
// find_secret($repo_url)
// Find the secret corresponding to the repo_url, if any. Returns empty string otherwise
function find_secret($repo_url) {
$forgehook = isset($_ENV['FORGEHOOK']) ? $_ENV['FORGEHOOK'] // from $ENV
: (file_exists('forgehook') ? './forgehook' // from current directory
: 'forgehook'); // from $PATH
$repo = escapeshellarg($repo_url);
$lines = [];
$status = NULL;
$secret = exec($forgehook." secret ".$repo, $lines, $status);
if (($secret == NULL) or ($status != 0)) {
error("Secret not found for \"".$repo."\"");
}
if (empty($secret)) {
error("Secret empty for ".$repo);
}
return $secret;
// Decide which whck executable to use
function find_whck() {
// BUG: https://bugs.php.net/bug.php?id=77782
// $_ENV['whck'] and getenv("whck") are empty, but getenv()["whck"] works!
return isset(getenv()['WHCK']) ? getenv()['WHCK'] // from $ENV
: (file_exists('whck') ? './whck' // from current directory
: 'whck');
}
class GiteaWebhook {
function __construct($payload) {
$this->data = json_to_array($payload);
//enum Validator {
// case HMAC_SHA256;
// case Token;
//}
class JSONWebhook {
function __construct(string $validator) {
$this->validator = $validator;
$this->plaintext = extract_payload();
$this->data = json_to_array($this->plaintext);
}
function repo_url() {
function findId() {
return base64_encode($this->repo);
}
function verify() {
verify_claim($this->plaintext, $this->claim, $this->id, $this->validator);
}
}
class GiteaWebhook extends JSONWebhook {
function __construct() {
parent::__construct("hmac-sha256");
$this->repo = $this->findRepo();
$this->id = $this->findId();
$this->claim = extract_header("HTTP_X_GITEA_SIGNATURE");
}
function findRepo() {
$repo_url = isset($this->data["repository"]["html_url"]) ?
$this->data["repository"]["html_url"] : "";
if (empty($repo_url)) {
error('Could not find Gitea repository URL');
}
return $repo_url;
}
function secret() {
return extract_header("HTTP_X_GITEA_SIGNATURE");
return $repo_url;
}
}
class GitlabWebhook {
function __construct($payload) {
$this->data = json_to_array($payload);
class GitlabWebhook extends JSONWebhook {
function __construct() {
parent::__construct("token");
$this->repo = $this->findRepo();
$this->id = $this->findId();
$this->claim = extract_header("HTTP_X_GITLAB_TOKEN");
}
function repo_url() {
function findRepo() {
$repo_url = isset($this->data["project"]["git_http_url"]) ?
$this->data["project"]["git_http_url"] : "";
if (empty($repo_url)) {
@ -125,18 +157,18 @@ class GitlabWebhook {
}
return $repo_url;
}
function secret() {
return extract_header("HTTP_X_GITLAB_TOKEN");
}
}
class GithubWebhook {
function __construct($payload) {
$this->data = json_to_array($payload);
class GithubWebhook extends JSONWebhook {
function __construct() {
parent::__construct("hmac-sha256");
$this->repo = $this->findRepo();
$this->id = $this->findId();
$this->claim = extract_header("HTTP_X_HUB_SIGNATURE");
}
function repo_url() {
function findRepo() {
$repo_url = isset($this->data["repository"]["html_url"]) ?
$this->data["repository"]["html_url"] : "";
if (empty($repo_url)) {
@ -144,25 +176,6 @@ class GithubWebhook {
}
return $repo_url;
}
function secret() {
return extract_header("HTTP_X_HUB_SIGNATURE");
}
}
function notify($repo) {
//$notify = getenv('FORGEHOOKNOTIFY') ? : 'forgehook-notify';
$notify = isset($_ENV['FORGEHOOKNOTIFY']) ? $_ENV['FORGEHOOKNOTIFY'] // from $ENV
: (file_exists('forgehook-notify') ? './forgehook-notify' // from current directory
: 'forgehook-notify'); // from $PATH
$lines = [];
$status = NULL;
exec($notify." ".$repo, $lines, $status);
if ($status != 0) {
error("Notify failed (".$notify.") with:\n".print_r($lines));
}
}
function action() {
@ -170,32 +183,18 @@ function action() {
error("You need to specify an action (gitea, gitlab) like this: ?action=gitea", 404);
}
$payload = extract_payload();
switch($_GET['action']) {
case 'github':
$webhook = new GithubWebhook($payload);
$claimed_secret = $webhook->secret();
$repo_url = $webhook->repo_url();
$secret = find_secret($repo_url);
verify_signature($payload, $secret, $claimed_secret);
notify($repo_url);
$webhook = new GithubWebhook();
$webhook->verify();
break;
case 'gitea':
$webhook = new GiteaWebhook($payload);
$claimed_secret = $webhook->secret();
$repo_url = $webhook->repo_url();
$secret = find_secret($repo_url);
verify_signature($payload, $secret, $claimed_secret);
notify($repo_url);
$webhook = new GiteaWebhook();
$webhook->verify();
break;
case 'gitlab':
$webhook = new GitlabWebhook($payload);
$claimed_secret = $webhook->secret();
$repo_url = $webhook->repo_url();
$secret = find_secret($repo_url);
verify_token($secret, $claimed_secret);
notify($repo_url);
$webhook = new GitlabWebhook();
$webhook->verify();
break;
default:
error("Unrecognized action: ".$_GET['action'], 400);
@ -205,4 +204,22 @@ function action() {
action();
echo("OK");
// verify_secret($repo_url, $secret)
// Verify a secret for the given URL. Useful when we don't have permission to read the secret.
function verify_secret($repo_url, $secret) {
$whck = find_whck();
$repo = escapeshellarg($repo_url);
$secret = escapeshellarg($secret);
$lines = [];
$status = NULL;
$secret = exec($forgehook." verify ".$repo." ".$secret, $lines, $status);
if (($secret == NULL) or ($status != 0)) {
echo "$secret";
error("Incorrect secret for \"".$repo."\": \"".$secret."\"", 403);
}
}
?>

2
spec

@ -1 +1 @@
Subproject commit a72ceda21cfde2ed9e6e19f65e007f2bc8778660
Subproject commit 71804c4b3a5f19bb53fdab0bbcecb39ed0919aca

1
whck Submodule

@ -0,0 +1 @@
Subproject commit 510a0b13b99f85379fe10dabadce408aa95a795d