commit - d40addaae71726e78912c56b7b2b14a4795fe7e4
commit + 97858f5c321756c6495a682f6913b59211b51a96
blob - fc2b64876b2b20537494f7fa72e0f2a19756ca54
blob + 4638c10ed322c7d77ce9a38b39a234f80a172047
--- gotd/gotd.conf.5
+++ gotd/gotd.conf.5
and
.Ic port
directives can be used to specify a different SMTP server address and port.
-.It Ic url Ar URL Oo Ic user Ar user Ic password Ar password Oo Ic insecure Oc Oc
+.It Ic url Ar URL Oo Ic user Ar user Ic password Ar password Oo Ic insecure Oc Oc Oo Ic hmac Ar secret Oc
Send notifications via HTTP.
This directive may be specified multiple times to build a list of
HTTP servers to send notifications to.
.Dq https://
URL to avoid leaking of authentication credentials.
.Pp
+If a
+.Ic hmac
+.Ar secret
+is provided, the request body will be signed using HMAC, allowing the
+receiver to verify the notification message's authenticity and integrity.
+The signature uses HMAC-SHA256 and will be sent in the HTTP header
+.Dq HTTP_X_GOTD_SIGNATURE_256 .
+Suitable secrets can be generated with
+.Xr openssl 1
+as follows:
+.Pp
+.Dl $ openssl rand -base64 32
+.Pp
The request body contains a JSON object with a
.Dq notifications
property containing an array of notification objects.
blob - 127286a48554f9687ba552171a77f0e02cb100f7
blob + 9f241a410e3a686fa14a79fae05f48c7bc955556
--- gotd/gotd.h
+++ gotd/gotd.h
char *path;
char *user;
char *password;
+ char *hmac_secret;
} http;
} conf;
};
blob - 66d39b63e3d3fd27e90584c6189567206564c049
blob + 81a563d1b9541e72594736ccf73c37af464ef9a7
--- gotd/libexec/got-notify-http/got-notify-http.c
+++ gotd/libexec/got-notify-http/got-notify-http.c
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
+#include <sys/queue.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <netdb.h>
#include <poll.h>
+#include <sha1.h>
+#include <sha2.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
+#include <openssl/evp.h>
+#include <openssl/hmac.h>
+
#include "got_opentemp.h"
#include "got_version.h"
+#include "got_object.h"
+#include "got_lib_hash.h"
+
#include "bufio.h"
#include "log.h"
#include "utf8d.h"
ret |= POLLOUT;
return ret;
}
+
+static unsigned char *
+compute_hmac_sha256(FILE *payload, off_t paylen, const char *hmac_secret,
+ size_t secret_len, unsigned char *hmac_sig_buf, unsigned int *hmac_siglen)
+{
+ HMAC_CTX *ctx;
+ char buf[4096];
+ off_t n;
+ ssize_t r;
+ *hmac_siglen = 0;
+
+ ctx = HMAC_CTX_new();
+ if (ctx == NULL) {
+ log_warn("HMAC_CTX_new");
+ return NULL;
+ }
+
+ if (!HMAC_Init_ex(ctx, hmac_secret, secret_len, EVP_sha256(), NULL)) {
+ log_warn("HMAC_Init_ex");
+ goto fail;
+ }
+
+ n = paylen;
+ while (n > 0) {
+ r = fread(buf, 1, n > sizeof(buf) ? sizeof(buf) : n, payload);
+ if (r == 0) {
+ if (feof(payload)) {
+ log_warnx("HMAC payload truncated");
+ goto fail;
+ }
+ log_warnx("reading HMAC payload: %s",
+ strerror(ferror(payload)));
+ goto fail;
+ }
+ if (!HMAC_Update(ctx, buf, r)) {
+ log_warn("HMAC_Update");
+ goto fail;
+ }
+ n -= r;
+ }
+
+ if (!HMAC_Final(ctx, hmac_sig_buf, hmac_siglen)) {
+ log_warn("HMAC_Final");
+ goto fail;
+ }
+
+ *hmac_siglen = HMAC_size(ctx);
+
+ HMAC_CTX_free(ctx);
+ return hmac_sig_buf;
+fail:
+ HMAC_CTX_free(ctx);
+ return NULL;
+}
+
int
main(int argc, char **argv)
{
const char *username;
const char *password;
const char *timeoutstr;
+ const char *hmac_secret;
const char *errstr;
const char *repo = NULL;
const char *host = NULL, *port = NULL, *path = NULL;
const char *gotd_auth_user = NULL;
char *auth, *line, *spc;
+ unsigned char *hmac_sig = NULL;
+ unsigned char hmac_sig_buf[EVP_MAX_MD_SIZE];
+ unsigned int hmac_siglen;
+ char hex[SHA256_DIGEST_STRING_LENGTH];
size_t len;
ssize_t r;
off_t paylen;
if (pledge("stdio rpath dns inet", NULL) == -1)
err(1, "pledge");
#endif
+ hmac_secret = getenv("GOT_NOTIFY_HTTP_HMAC_SECRET");
+ if (hmac_secret) {
+ hmac_sig = compute_hmac_sha256(tmpfp, paylen, hmac_secret,
+ strlen(hmac_secret), hmac_sig_buf, &hmac_siglen);
+ if (hmac_sig == NULL || hmac_siglen != SHA256_DIGEST_LENGTH)
+ fatalx("HMAC computation failed");
+ if (got_sha256_digest_to_str(hmac_sig, hex, sizeof(hex))
+ == NULL)
+ fatalx("HMAC conversion to hex string failed");
+ if (fseeko(tmpfp, 0, SEEK_SET) == -1)
+ fatal("fseeko");
+ }
+
memset(&pfd, 0, sizeof(pfd));
pfd.fd = dial(host, port);
"Content-Type: application/json\r\n"
"Content-Length: %lld\r\n"
"User-Agent: %s\r\n"
- "Connection: close\r\n",
+ "Connection: close\r\n"
+ "%s%s%s%s",
path, host,
nonstd ? ":" : "", nonstd ? port : "",
- (long long)paylen, USERAGENT);
+ (long long)paylen, USERAGENT,
+ hmac_sig ? "HTTP_X_GOTD_SIGNATURE_256: " : "",
+ hmac_sig ? "sha256=" : "",
+ hmac_sig ? hex : "",
+ hmac_sig ? "\r\n" : "");
if (ret == -1)
fatal("bufio_compose_fmt");
blob - e57194c1b439e147659de38acc77b1ddfab8c286
blob + b70d407163a937ee506422c8fdcdfefe7155a6ba
--- gotd/notify.c
+++ gotd/notify.c
static void
run_notification_helper(const char *prog, const char **argv, int fd,
- const char *user, const char *pass)
+ const char *user, const char *pass, const char *hmac_secret)
{
const struct got_error *err = NULL;
pid_t pid;
setenv("GOT_NOTIFY_HTTP_USER", user, 1);
setenv("GOT_NOTIFY_HTTP_PASS", pass, 1);
}
+
+ if (hmac_secret)
+ setenv("GOT_NOTIFY_HTTP_HMAC_SECRET", hmac_secret, 1);
+ else
+ unsetenv("GOT_NOTIFY_HTTP_HMAC_SECRET");
if (execv(prog, (char *const *)argv) == -1) {
fprintf(stderr, "%s: exec %s: %s\n", getprogname(),
argv[i] = NULL;
run_notification_helper(GOTD_PATH_PROG_NOTIFY_EMAIL, argv, fd,
- NULL, NULL);
+ NULL, NULL, NULL);
}
static void
argv[argc] = NULL;
run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd,
- target->conf.http.user, target->conf.http.password);
+ target->conf.http.user, target->conf.http.password,
+ target->conf.http.hmac_secret);
}
static const struct got_error *
blob - d8c3ed4f6c061359778be4ea784b59494d658587
blob + 22e5ac55f023330a537ca5055c6edba55f97b782
--- gotd/parse.y
+++ gotd/parse.y
static int conf_notify_email(struct gotd_repo *,
char *, char *, char *, char *, char *);
static int conf_notify_http(struct gotd_repo *,
- char *, char *, char *, int);
+ char *, char *, char *, int, char *);
static enum gotd_procid gotd_proc_id;
typedef struct {
%token PATH ERROR LISTEN ON USER REPOSITORY PERMIT DENY
%token RO RW CONNECTION LIMIT REQUEST TIMEOUT
%token PROTECT NAMESPACE BRANCH TAG REFERENCE RELAY PORT
-%token NOTIFY EMAIL FROM REPLY TO URL PASSWORD INSECURE
+%token NOTIFY EMAIL FROM REPLY TO URL PASSWORD INSECURE HMAC
%token <v.string> STRING
%token <v.number> NUMBER
gotd_proc_id == PROC_SESSION_WRITE ||
gotd_proc_id == PROC_NOTIFY) {
if (conf_notify_http(new_repo, $2, NULL,
- NULL, 0)) {
+ NULL, 0, NULL)) {
free($2);
YYERROR;
}
if (gotd_proc_id == PROC_GOTD ||
gotd_proc_id == PROC_SESSION_WRITE ||
gotd_proc_id == PROC_NOTIFY) {
- if (conf_notify_http(new_repo, $2, $4, $6, 0)) {
+ if (conf_notify_http(new_repo, $2, $4, $6, 0,
+ NULL)) {
free($2);
free($4);
free($6);
if (gotd_proc_id == PROC_GOTD ||
gotd_proc_id == PROC_SESSION_WRITE ||
gotd_proc_id == PROC_NOTIFY) {
- if (conf_notify_http(new_repo, $2, $4, $6, 1)) {
+ if (conf_notify_http(new_repo, $2, $4, $6, 1,
+ NULL)) {
+ free($2);
+ free($4);
+ free($6);
+ YYERROR;
+ }
+ }
+ free($2);
+ free($4);
+ free($6);
+ }
+ | URL STRING HMAC STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_http(new_repo, $2, NULL,
+ NULL, 0, $4)) {
+ free($2);
+ free($4);
+ YYERROR;
+ }
+ }
+ free($2);
+ free($4);
+ }
+ | URL STRING USER STRING PASSWORD STRING HMAC STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_http(new_repo, $2, $4, $6, 0,
+ $8)) {
+ free($2);
+ free($4);
+ free($6);
+ free($8);
+ YYERROR;
+ }
+ }
+ free($2);
+ free($4);
+ free($6);
+ free($8);
+ }
+ | URL STRING USER STRING PASSWORD STRING INSECURE HMAC STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_SESSION_WRITE ||
+ gotd_proc_id == PROC_NOTIFY) {
+ if (conf_notify_http(new_repo, $2, $4, $6, 1,
+ $9)) {
free($2);
free($4);
free($6);
+ free($9);
YYERROR;
}
}
free($2);
free($4);
free($6);
+ free($9);
}
;
{ "deny", DENY },
{ "email", EMAIL },
{ "from", FROM },
+ { "hmac", HMAC },
{ "insecure", INSECURE },
{ "limit", LIMIT },
{ "listen", LISTEN },
static int
conf_notify_http(struct gotd_repo *repo, char *url, char *user, char *password,
- int insecure)
+ int insecure, char *hmac_secret)
{
const struct got_error *error;
struct gotd_notification_target *target;
if (target->conf.http.password == NULL)
fatal("strdup");
}
+ if (hmac_secret) {
+ target->conf.http.hmac_secret = strdup(hmac_secret);
+ if (target->conf.http.hmac_secret == NULL)
+ fatal("strdup");
+ }
STAILQ_INSERT_TAIL(&repo->notification_targets, target, entry);
done:
blob - 4897b27e2bbd6fa672d2a449a5b25ad65e303850
blob + 184054f12c04e1cd6db0a1f82dff27b5bcbb2de3
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
test_repo_write test_repo_write_empty test_request_bad \
test_repo_write_protected test_repo_write_readonly \
test_email_notification test_http_notification \
- test_git_interop test_email_and_http_notification
+ test_git_interop test_email_and_http_notification \
+ test_http_notification_hmac
NOOBJ=Yes
CLEANFILES=gotd.conf
GOTD_TEST_REPO_URL=ssh://${GOTD_DEVUSER}@127.0.0.1/$(GOTD_TEST_REPO_NAME)
GOTD_TEST_SMTP_PORT=2525
GOTD_TEST_HTTP_PORT=8000
+GOTD_TEST_HMAC_SECRET=test1234
GOTD_TEST_USER?=${DOAS_USER}
.if empty(GOTD_TEST_USER)
GOTD_CONF=$(PWD)/gotd.conf \
GOTD_TEST_SMTP_PORT=$(GOTD_TEST_SMTP_PORT) \
GOTD_TEST_HTTP_PORT=$(GOTD_TEST_HTTP_PORT) \
+ GOTD_TEST_HMAC_SECRET=$(GOTD_TEST_HMAC_SECRET) \
HOME=$(GOTD_TEST_USER_HOME) \
PATH=$(GOTD_TEST_USER_HOME)/bin:$(PATH)
@$(GOTD_TRAP); $(GOTD_START_CMD)
@$(GOTD_TRAP); sleep .5
+start_gotd_http_notification_hmac: ensure_root
+ @echo 'listen on "$(GOTD_SOCK)"' > $(PWD)/gotd.conf
+ @echo "user $(GOTD_USER)" >> $(PWD)/gotd.conf
+ @echo 'repository "test-repo" {' >> $(PWD)/gotd.conf
+ @echo ' path "$(GOTD_TEST_REPO)"' >> $(PWD)/gotd.conf
+ @echo ' permit rw $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
+ @echo ' notify {' >> $(PWD)/gotd.conf
+ @echo ' url "http://localhost:${GOTD_TEST_HTTP_PORT}/" user flan password "password" insecure hmac "${GOTD_TEST_HMAC_SECRET}"' >> $(PWD)/gotd.conf
+ @echo " }" >> $(PWD)/gotd.conf
+ @echo "}" >> $(PWD)/gotd.conf
+ @$(GOTD_TRAP); $(GOTD_START_CMD)
+ @$(GOTD_TRAP); sleep .5
prepare_test_repo: ensure_root
@chown ${GOTD_USER} "${GOTD_TEST_REPO}"
@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./prepare_test_repo.sh'
'env $(GOTD_TEST_ENV) sh ./http_notification.sh'
@$(GOTD_STOP_CMD) 2>/dev/null
+test_http_notification_hmac: prepare_test_repo start_gotd_http_notification_hmac
+ @-$(GOTD_TRAP); su -m ${GOTD_TEST_USER} -c \
+ 'env $(GOTD_TEST_ENV) sh ./http_notification_hmac.sh'
+ @$(GOTD_STOP_CMD) 2>/dev/null
+
test_email_and_http_notification: prepare_test_repo start_gotd_email_and_http_notification
@-$(GOTD_TRAP); su -m ${GOTD_TEST_USER} -c \
'env $(GOTD_TEST_ENV) sh ./http_notification.sh test_file_changed'
blob - 4e5c767b884376a69d6df25e19f47a8c53829fba
blob + a2518bf858a18dbc66fcb6ad278ee0cfc54f9a8c
--- regress/gotd/README
+++ regress/gotd/README
The server test suite can now be run from the top-level directory:
- $ doas pkg_add git p5-http-daemon
+ $ doas pkg_add git p5-http-daemon p5-digest-hmac
$ doas make server-regress
The suite must be started as root in order to be able to start and stop gotd.
blob - a9297fb4c098dec8fe8f0e0da89a4516a4ddaecc
blob + 800393052afde0178d8c080fbbf7ab7dffd883e8
--- regress/gotd/http-server
+++ regress/gotd/http-server
use v5.36;
use IPC::Open2;
use Getopt::Long qw(:config bundling);
+use Digest;
+use Digest::HMAC;
my $auth;
my $port = 8000;
+my $hmac_secret;
+my $hmac_signature;
+my $hmac;
-GetOptions("a:s" => \$auth, "p:i" => \$port)
- or die("usage: $0 [-a auth] [-p port]\n");
+GetOptions("a:s" => \$auth, "p:i" => \$port, "s:s" => \$hmac_secret)
+ or die("usage: $0 [-a auth] [-p port] [-s hmac_secret]\n");
my $pid = open2(my $out, my $in, 'nc', '-l', 'localhost', $port);
if not defined($auth) or $auth ne $t;
next;
}
+
+ if (m/HTTP_X_GOTD_SIGNATURE_256/) {
+ die "bad hmac signature header"
+ unless m/HTTP_X_GOTD_SIGNATURE_256: sha256=(.*)$/;
+ $hmac_signature = $1;
+ next;
+ }
}
die "no Content-Length header" unless defined $clen;
+if (defined $hmac_signature) {
+ die "no HMAC secret provided" if not (defined $hmac_secret);
+ my $sha256 = Digest->new("SHA-256");
+ $hmac = Digest::HMAC->new($hmac_secret, $sha256);
+}
+
while ($clen != 0) {
my $len = $clen;
$len = 512 if $clen > 512;
my $r = read($out, my $buf, $len);
$clen -= $r;
+ if (defined $hmac) {
+ $hmac->add($buf);
+ }
+
print $buf;
}
say "";
+if (defined $hmac) {
+ my $digest = $hmac->hexdigest;
+ if ($digest ne $hmac_signature) {
+ die "bad hmac signature: expected: $hmac_signature, actual: $digest";
+ }
+}
+
print $in "HTTP/1.1 200 OK\r\n";
print $in "Content-Length: 0\r\n";
print $in "Connection: close\r\n";
blob - /dev/null
blob + 486096684215bcc49c73b7826b11ec392222db4f (mode 644)
--- /dev/null
+++ regress/gotd/http_notification_hmac.sh
+#!/bin/sh
+#
+# Copyright (c) 2024 Omar Polo <op@openbsd.org>
+# Copyright (c) 2024 Stefan Sperling <stsp@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ../cmdline/common.sh
+. ./common.sh
+
+# flan:password encoded in base64
+AUTH="ZmxhbjpwYXNzd29yZA=="
+
+test_file_changed() {
+ local testroot=`test_init file_changed 1`
+
+ got clone -a -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got checkout failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ echo "change alpha" > $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m 'make changes' > /dev/null)
+ local commit_id=`git_show_head $testroot/repo-clone`
+ local author_time=`git_show_author_time $testroot/repo-clone`
+
+ timeout 5 ./http-server -a $AUTH -p $GOTD_TEST_HTTP_PORT \
+ -s $GOTD_TEST_HMAC_SECRET > $testroot/stdout &
+
+ sleep 1 # server starts up
+
+ got send -b main -q -r $testroot/repo-clone
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got send failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ wait %1 # wait for the http "server"
+
+ touch "$testroot/stdout.expected"
+ ed -s "$testroot/stdout.expected" <<-EOF
+ a
+ {"notifications":[{
+ "type":"commit",
+ "short":false,
+ "repo":"test-repo",
+ "authenticated_user":"${GOTD_DEVUSER}",
+ "id":"$commit_id",
+ "author":{
+ "full":"$GOT_AUTHOR",
+ "name":"$GIT_AUTHOR_NAME",
+ "mail":"$GIT_AUTHOR_EMAIL",
+ "user":"$GOT_AUTHOR_11"
+ },
+ "committer":{
+ "full":"$GOT_AUTHOR",
+ "name":"$GIT_AUTHOR_NAME",
+ "mail":"$GIT_AUTHOR_EMAIL",
+ "user":"$GOT_AUTHOR_11"
+ },
+ "date":$author_time,
+ "short_message":"make changes",
+ "message":"make changes\n",
+ "diffstat":{
+ "files":[{
+ "action":"modified",
+ "file":"alpha",
+ "added":1,
+ "removed":1
+ }],
+ "total":{
+ "added":1,
+ "removed":1
+ }
+ }
+ }]}
+ .
+ ,j
+ w
+ EOF
+
+ cmp -s $testroot/stdout.expected $testroot/stdout
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ diff -u $testroot/stdout.expected $testroot/stdout
+ fi
+ test_done "$testroot" "$ret"
+}
+
+test_parseargs "$@"
+run_test test_file_changed