commit 0fbfa7970372d6314144d0da3641a34299e79981 from: Omar Polo via: Thomas Adam date: Tue Sep 10 14:10:40 2024 UTC initial gotd-secrets.conf implementation This moves the handling of the secrets outside gotd.conf. There will be a few changes to this, committing it as-is to continue hacking in tree. ok stsp@ commit - ce1788e8bd86b58d8138d8c7a5d520713e32a805 commit + 0fbfa7970372d6314144d0da3641a34299e79981 blob - 4745de0b61f8e8be97d67007beaa6d42af3c885d blob + 042c67513c22d551fad3940d43ff7577e7ea63f7 --- gitwrapper/gitwrapper.c +++ gitwrapper/gitwrapper.c @@ -130,7 +130,7 @@ main(int argc, char *argv[]) confpath = getenv("GOTD_CONF_PATH"); if (confpath == NULL) confpath = GOTD_CONF_PATH; - parse_config(confpath, PROC_GITWRAPPER, &gotd); + parse_config(confpath, PROC_GITWRAPPER, NULL, &gotd); error = apply_unveil(myserver); if (error) blob - 38567d159047ce5179b9982fcefdf2bff0d21e9b blob + 2eaa6f893d71aee3fc293fbaef9932969dbf0923 --- gotd/gotd.8 +++ gotd/gotd.8 @@ -23,6 +23,7 @@ .Nm .Op Fl dnv .Op Fl f Ar config-file +.Op Fl s Ar secrets .Sh DESCRIPTION .Nm is a Git repository server which listens on a @@ -63,6 +64,11 @@ will be used. .It Fl n Configtest mode. Only check the configuration file for validity. +.It Fl s Ar secrets +Set the path to the secrets file. +If not specified, the file +.Pa /etc/gotd-secrets.conf +will be used if it exists. .It Fl v Verbose mode. Verbosity increases if this option is used multiple times. @@ -109,6 +115,7 @@ The flan_hacker user can now populate the empty reposi .Xr gotsh 1 , .Xr git-repository 5 , .Xr gotd.conf 5 +.Xr gotd-secrets.conf 5 .Sh AUTHORS .An Stefan Sperling Aq Mt stsp@openbsd.org .Sh CAVEATS blob - /dev/null blob + 9c0a9efd1ed96a42a798c7f69a70c8645bdf0ead (mode 644) --- /dev/null +++ gotd/gotd-secrets.conf.5 @@ -0,0 +1,103 @@ +.\" +.\" Copyright (c) 2024 Omar Polo +.\" +.\" 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. +.\" +.Dd $Mdocdate$ +.Dt GOTD-SECRETS.CONF 5 +.Os +.Sh NAME +.Nm gotd-secrets.conf +.Nd gotd secrets file +.Sh DESCRIPTION +.Nm +holds the authentication data and HMAC secrets for +.Xr gotd 8 +notifications. +.Pp +The file format is line-based, with one entry per line. +Comments can be put at the start of the line using a hash mark +.Pq Sq # , +and extend to the end of it. +Blank lines are also ignored. +.Pp +The entries have the following syntax: +.Pp +.Dl type key value +.Pp +with spaces or tabs to separate the fields. +No quoting is supported, so a space or a tab can't appear as part of +any field. +.Pp +The type is one of: +.Bl -tag -width Ds +.It Ic auth +The entry is for HTTP Basic Authentication. +.Ar key +is the username and +.ar value +the password. +The username is also used to identify this secret. +.It Ic hmac +The entry is for signing the notification HTTP payload with HMAC. +The +.Ar key +is a label to identify this secret and +.Ar value +is the HMAC secret. +.Pp +Suitable secrets can be generated with +.Xr openssl 1 +as follows: +.Pp +.Dl $ openssl rand -base64 32 +.El +.Pp +The key must be unique between entries with the same type. +.Sh FILES +.Bl -tag -width Ds -compact +.It Pa /etc/gotd-secrets.conf +Location of the +.Nm +configuration file. +.El +.Sh EXAMPLES +This example configuration defines two secrets, the first for +HTTP authentication and the second for HMAC signign. +.Bd -literal -offset indent +# /etc/gotd-secrets.conf +auth flan super-strong-password! +hmac hacker q0tcl8QhjYs7U75MW/2rwB30CpdbAhONkfLGxFHm/+8= +.Ed +.Pp +These values can be referenced in +.Xr gotd.conf 5 +as: +.Bd -literal -offset indent +# /etc/gotd.conf +repository "openbsd/ports" { + path "/var/git/ports.git" + permit rw :porters + permit ro anonymous + + notify { + url https://flan.com/notify/ auth flan + url https://hacker.com/notify/ hmac hacker + } +} +.El +.Sh SEE ALSO +.Xr got 1 , +.Xr gotsh 1 , +.Xr gotd.conf 5 , +.Xr gotd 8 blob - 173ebbce76e5fac0f4eaaa18e47157e34995a1b4 blob + 516efa50f6d737f7eedb15849968300d76e9a0f8 --- gotd/gotd.c +++ gotd/gotd.c @@ -65,6 +65,7 @@ #include "repo_read.h" #include "repo_write.h" #include "notify.h" +#include "secrets.h" #ifndef nitems #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) @@ -126,7 +127,8 @@ static void drop_privs(struct passwd *); __dead static void usage(void) { - fprintf(stderr, "usage: %s [-dnv] [-f config-file]\n", getprogname()); + fprintf(stderr, "usage: %s [-dnv] [-f config-file] [-s secrets]\n", + getprogname()); exit(1); } @@ -2053,8 +2055,10 @@ int main(int argc, char **argv) { const struct got_error *error = NULL; + struct gotd_secrets *secrets = NULL; int ch, fd = -1, daemonize = 1, verbosity = 0, noaction = 0; const char *confpath = GOTD_CONF_PATH; + const char *secretspath = NULL; char *argv0 = argv[0]; char title[2048]; struct passwd *pw = NULL; @@ -2065,7 +2069,8 @@ main(int argc, char **argv) int *pack_fds = NULL, *temp_fds = NULL; struct gotd_repo *repo = NULL; char *default_sender = NULL; - char hostname[_POSIX_HOST_NAME_MAX + 1]; + char hostname[HOST_NAME_MAX + 1]; + FILE *fp; FILE *diff_f1 = NULL, *diff_f2 = NULL; int diff_fd1 = -1, diff_fd2 = -1; const char *errstr; @@ -2074,7 +2079,7 @@ main(int argc, char **argv) log_init(1, LOG_DAEMON); /* Log to stderr until daemonized. */ - while ((ch = getopt(argc, argv, "df:nP:T:v")) != -1) { + while ((ch = getopt(argc, argv, "df:nP:s:T:v")) != -1) { switch (ch) { case 'd': daemonize = 0; @@ -2090,6 +2095,9 @@ main(int argc, char **argv) if (repo_path == NULL) fatal("realpath '%s'", optarg); break; + case 's': + secretspath = optarg; + break; case 'T': switch (*optarg) { case 'A': @@ -2135,7 +2143,23 @@ main(int argc, char **argv) if (geteuid() && (proc_id == PROC_GOTD || proc_id == PROC_LISTEN)) fatalx("need root privileges"); - if (parse_config(confpath, proc_id, &gotd) != 0) + if (proc_id == PROC_GOTD) { + const char *p = secretspath ? secretspath : GOTD_SECRETS_PATH; + + fp = fopen(p, "r"); + if (fp == NULL && (secretspath != NULL || errno != ENOENT)) + fatal("can't open secret file %s", p); + + if (fp != NULL) { + error = gotd_secrets_parse(p, fp, &secrets); + fclose(fp); + if (error) + fatalx("failed to parse secrets file %s: %s", + p, error->msg); + } + } + + if (parse_config(confpath, proc_id, secrets, &gotd) != 0) return 1; pw = getpwnam(gotd.user_name); @@ -2430,9 +2454,57 @@ main(int argc, char **argv) signal_add(&evsigchld, NULL); gotd_imsg_event_add(&gotd.listen_proc->iev); - if (gotd.notify_proc) + if (gotd.notify_proc) { + struct imsgbuf *imsgbuf = &gotd.notify_proc->iev.ibuf; + struct gotd_secret *s; + size_t i, n = 0; + gotd_imsg_event_add(&gotd.notify_proc->iev); + + if (gotd.secrets) + n = gotd.secrets->len; + + if (imsg_compose(imsgbuf, GOTD_IMSG_SECRETS, 0, 0, -1, + &n, sizeof(n)) == -1) + fatal("imsg_compose GOTD_IMSG_SECRETS"); + if (imsg_flush(imsgbuf)) + fatal("imsg_flush"); + for (i = 0; i < n; ++i) { + struct iovec iov[5]; + int keylen, vallen; + + s = &gotd.secrets->secrets[i]; + + keylen = strlen(s->key) + 1; + vallen = strlen(s->val) + 1; + + iov[0].iov_base = &s->type; + iov[0].iov_len = sizeof(s->type); + + iov[1].iov_base = &keylen; + iov[1].iov_len = sizeof(keylen); + + iov[2].iov_base = &vallen; + iov[2].iov_len = sizeof(vallen); + + iov[3].iov_base = s->key; + iov[3].iov_len = keylen; + + iov[4].iov_base = s->val; + iov[4].iov_len = vallen; + + if (imsg_composev(imsgbuf, GOTD_IMSG_SECRET, + 0, 0, -1, iov, 5) == -1) + fatal("imsg_composev GOTD_IMSG_SECRET"); + if (imsg_flush(imsgbuf)) + fatal("imsg_flush"); + } + + gotd_secrets_free(gotd.secrets); + gotd.secrets = NULL; + } + event_dispatch(); free(repo_path); blob - 966c6008573b577e99cc85b6d7eada163453396c blob + 54878c9402ad6203ea5e8dd27dea510086c07f1e --- gotd/gotd.conf.5 +++ gotd/gotd.conf.5 @@ -333,7 +333,7 @@ The 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 Oo Ic hmac Ar secret Oc +.It Ic url Ar URL Oo Ic auth Ar auth Oo Ic insecure Oc Oc Oo Ic hmac Ar label Oc Send notifications via HTTP. This directive may be specified multiple times to build a list of HTTP servers to send notifications to. @@ -348,18 +348,8 @@ If HTTPS is used, sending of notifications will only s no TLS errors occur. .Pp The optional -.Ic user -and -.Ic password -directives enable HTTP Basic authentication. -If used, both a -.Ar user -and a -.Ar password -must be specified. -The -.Ar password -must not be an empty string. +.Ic auth +directive enables HTTP Basic authentication. Unless the .Ic insecure option is specified the notification target @@ -370,16 +360,20 @@ URL to avoid leaking of authentication credentials. .Pp If a .Ic hmac -.Ar secret +.Ar label 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 X-Gotd-Signature . -Suitable secrets can be generated with -.Xr openssl 1 -as follows: .Pp -.Dl $ openssl rand -base64 32 +If provided, +the authentication data +.Ar auth +and the HMAC secret +.Ar label +are resolved using the +.Xr gotd-secrets.conf 5 +file. .Pp The request body contains a JSON object with a .Dq notifications @@ -563,4 +557,5 @@ connection { .Sh SEE ALSO .Xr got 1 , .Xr gotsh 1 , +.Xr gotd-secrets.conf 5 , .Xr gotd 8 blob - 9f241a410e3a686fa14a79fae05f48c7bc955556 blob + 5d5572b92f4df628e76278a4cb7aa64b56f631c7 --- gotd/gotd.h +++ gotd/gotd.h @@ -20,7 +20,7 @@ #define GOTD_UNIX_SOCKET_BACKLOG 10 #define GOTD_USER "_gotd" #define GOTD_CONF_PATH "/etc/gotd.conf" -#ifndef GOTD_EMPTY_PATH +#define GOTD_SECRETS_PATH "/etc/gotd-secrets.conf" #define GOTD_EMPTY_PATH "/var/empty" #endif @@ -113,9 +113,8 @@ struct gotd_notification_target { char *hostname; char *port; char *path; - char *user; - char *password; - char *hmac_secret; + char *auth; + char *hmac; } http; } conf; }; @@ -156,6 +155,7 @@ struct gotd_uid_connection_limit { struct gotd_child_proc; +struct gotd_secrets; struct gotd { pid_t pid; char unix_socket_path[PATH_MAX]; @@ -169,6 +169,7 @@ struct gotd { struct timeval auth_timeout; struct gotd_uid_connection_limit *connection_limits; size_t nconnection_limits; + struct gotd_secrets *secrets; char *argv0; const char *confpath; @@ -243,7 +244,11 @@ enum gotd_imsg_type { GOTD_IMSG_CONNECT_NOTIFIER, GOTD_IMSG_CONNECT_SESSION, GOTD_IMSG_NOTIFY, - GOTD_IMSG_NOTIFICATION_SENT + GOTD_IMSG_NOTIFICATION_SENT, + + /* Secrets. */ + GOTD_IMSG_SECRETS, /* number of secrets */ + GOTD_IMSG_SECRET, }; /* Structure for GOTD_IMSG_ERROR. */ @@ -485,8 +490,8 @@ struct gotd_imsg_notify { /* Followed by username_len data bytes. */ }; -int enter_chroot(const char *); -int parse_config(const char *, enum gotd_procid, struct gotd *); +int parse_config(const char *, enum gotd_procid, struct gotd_secrets *, + struct gotd *); struct gotd_repo *gotd_find_repo_by_name(const char *, struct gotd_repolist *); struct gotd_repo *gotd_find_repo_by_path(const char *, struct gotd *); struct gotd_uid_connection_limit *gotd_find_uid_connection_limit( blob - 70e3428d2a91ac2b436fbecabf85eb0149a17487 blob + 6c3bc476afd311f7dd08f2597ac7faee7cf44a83 --- gotd/notify.c +++ gotd/notify.c @@ -38,10 +38,13 @@ #include "gotd.h" #include "log.h" #include "notify.h" +#include "secrets.h" #ifndef nitems #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) #endif + +static struct gotd_secrets secrets; static struct gotd_notify { pid_t pid; @@ -271,6 +274,7 @@ static void notify_http(struct gotd_notification_target *target, const char *repo, const char *username, int fd) { + const char *http_user = NULL, *http_pass = NULL, *hmac = NULL; const char *argv[12]; int argc = 0; @@ -290,10 +294,19 @@ notify_http(struct gotd_notification_target *target, c argv[argc++] = target->conf.http.path; argv[argc] = NULL; + + if (target->conf.http.auth) { + http_user = target->conf.http.auth; + http_pass = gotd_secrets_get(&secrets, GOTD_SECRET_AUTH, + http_user); + } + if (target->conf.http.hmac) { + hmac = gotd_secrets_get(&secrets, GOTD_SECRET_HMAC, + target->conf.http.hmac); + } run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd, - target->conf.http.user, target->conf.http.password, - target->conf.http.hmac_secret); + http_user, http_pass, hmac); } static const struct got_error * @@ -460,6 +473,10 @@ notify_dispatch(int fd, short event, void *arg) ssize_t n; int shut = 0; struct imsg imsg; + struct ibuf ibuf; + struct gotd_secret *s; + int keylen, vallen; + char *key, *val; if (event & EV_READ) { if ((n = imsg_read(imsgbuf)) == -1 && errno != EAGAIN) @@ -493,7 +510,40 @@ notify_dispatch(int fd, short event, void *arg) switch (imsg.hdr.type) { case GOTD_IMSG_CONNECT_SESSION: err = recv_session(&imsg); + break; + case GOTD_IMSG_SECRETS: + if (secrets.cap != 0) + fatal("unexpected GOTD_IMSG_SECRETS"); + if (imsg_get_data(&imsg, &secrets.cap, + sizeof(secrets.cap)) == -1) + fatalx("corrupted GOTD_IMSG_SECRETS"); + if (secrets.cap == 0) + break; + secrets.secrets = calloc(secrets.cap, + sizeof(*secrets.secrets)); + if (secrets.secrets == NULL) + fatal("calloc"); break; + case GOTD_IMSG_SECRET: + if (secrets.len == secrets.cap) + fatalx("unexpected GOTD_SECRET_AUTH"); + s = &secrets.secrets[secrets.len++]; + if (imsg_get_ibuf(&imsg, &ibuf) == -1) + fatal("imsg_get_ibuf"); + if (ibuf_get(&ibuf, &s->type, sizeof(s->type)) == -1 || + ibuf_get(&ibuf, &keylen, sizeof(keylen)) == -1 || + ibuf_get(&ibuf, &vallen, sizeof(vallen)) == -1 || + keylen <= 0 || vallen <= 0 || + ibuf_size(&ibuf) != (keylen + vallen) || + (key = ibuf_data(&ibuf)) == NULL || + (val = ibuf_seek(&ibuf, keylen, vallen)) == NULL || + key[keylen - 1] != '\0' || val[vallen - 1] != '\0') + fatalx("corrupted GOTD_IMSG_SECRET"); + s->key = strdup(key); + s->val = strdup(val); + if (s->key == NULL || s->val == NULL) + fatal("strdup"); + break; default: log_debug("unexpected imsg %d", imsg.hdr.type); break; blob - dbf35299573c9394146b8d24de88b083bc2a2718 blob + 300a079f3b809f423bfd3e3749907b3c957fc10a --- gotd/parse.y +++ gotd/parse.y @@ -52,6 +52,7 @@ #include "gotd.h" #include "auth.h" #include "listen.h" +#include "secrets.h" TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files); static struct file { @@ -111,7 +112,7 @@ static int conf_notify_ref_namespace(struct gotd_re 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 *, char *, int); static enum gotd_procid gotd_proc_id; typedef struct { @@ -128,7 +129,7 @@ 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 HMAC +%token NOTIFY EMAIL FROM REPLY TO URL INSECURE HMAC AUTH %token STRING %token NUMBER @@ -611,51 +612,47 @@ notifyflags : BRANCH STRING { gotd_proc_id == PROC_SESSION_WRITE || gotd_proc_id == PROC_NOTIFY) { if (conf_notify_http(new_repo, $2, NULL, - NULL, 0, NULL)) { + NULL, 0)) { free($2); YYERROR; } } free($2); } - | URL STRING USER STRING PASSWORD STRING { + | URL STRING AUTH 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, - NULL)) { + if (conf_notify_http(new_repo, $2, $4, NULL, + 0)) { free($2); free($4); - free($6); YYERROR; } } free($2); free($4); - free($6); } - | URL STRING USER STRING PASSWORD STRING INSECURE { + | URL STRING AUTH STRING INSECURE { 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, - NULL)) { + if (conf_notify_http(new_repo, $2, $4, NULL, + 1)) { 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)) { + if (conf_notify_http(new_repo, $2, NULL, $4, + 0)) { free($2); free($4); YYERROR; @@ -664,41 +661,37 @@ notifyflags : BRANCH STRING { free($2); free($4); } - | URL STRING USER STRING PASSWORD STRING HMAC STRING { + | URL STRING AUTH 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)) { + if (conf_notify_http(new_repo, $2, $4, $6, + 0)) { 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 { + | URL STRING AUTH 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)) { + if (conf_notify_http(new_repo, $2, $4, $7, + 1)) { free($2); free($4); - free($6); - free($9); + free($7); YYERROR; } } free($2); free($4); - free($6); - free($9); + free($7); } ; @@ -839,6 +832,7 @@ lookup(char *s) { /* This has to be sorted always. */ static const struct keywords keywords[] = { + { "auth", AUTH }, { "branch", BRANCH }, { "connection", CONNECTION }, { "deny", DENY }, @@ -851,7 +845,6 @@ lookup(char *s) { "namespace", NAMESPACE }, { "notify", NOTIFY }, { "on", ON }, - { "password", PASSWORD }, { "path", PATH }, { "permit", PERMIT }, { "port", PORT }, @@ -1192,7 +1185,7 @@ closefile(struct file *xfile) int parse_config(const char *filename, enum gotd_procid proc_id, - struct gotd *env) + struct gotd_secrets *secrets, struct gotd *env) { struct sym *sym, *next; struct gotd_repo *repo; @@ -1202,6 +1195,7 @@ parse_config(const char *filename, enum gotd_procid pr gotd = env; gotd_proc_id = proc_id; + gotd->secrets = secrets; TAILQ_INIT(&gotd->repos); /* Apply default values. */ @@ -1614,8 +1608,8 @@ conf_notify_email(struct gotd_repo *repo, char *sender } static int -conf_notify_http(struct gotd_repo *repo, char *url, char *user, char *password, - int insecure, char *hmac_secret) +conf_notify_http(struct gotd_repo *repo, char *url, char *auth, char *hmac, + int insecure) { const struct got_error *error; struct gotd_notification_target *target; @@ -1650,15 +1644,23 @@ conf_notify_http(struct gotd_repo *repo, char *url, ch } } - if ((user != NULL && password == NULL) || - (user == NULL && password != NULL)) { - yyerror("missing username or password"); + if (auth != NULL && gotd_proc_id == PROC_GOTD && + (gotd->secrets == NULL || gotd_secrets_get(gotd->secrets, + GOTD_SECRET_AUTH, auth) == NULL)) { + yyerror("no auth secret `%s' defined", auth); ret = -1; goto done; } - if (!insecure && strcmp(proto, "http") == 0 && - (user != NULL || password != NULL)) { + if (hmac != NULL && gotd_proc_id == PROC_GOTD && + (gotd->secrets == NULL && gotd_secrets_get(gotd->secrets, + GOTD_SECRET_HMAC, hmac) == NULL)) { + yyerror("no hmac secret `%s' defined", hmac); + ret = -1; + goto done; + } + + if (!insecure && strcmp(proto, "http") == 0 && auth) { yyerror("%s: HTTP notifications with basic authentication " "over plaintext HTTP will leak credentials; add the " "'insecure' config keyword if this is intentional", url); @@ -1690,17 +1692,14 @@ conf_notify_http(struct gotd_repo *repo, char *url, ch target->conf.http.path = path; hostname = port = path = NULL; - if (user) { - target->conf.http.user = strdup(user); - if (target->conf.http.user == NULL) - fatal("strdup"); - target->conf.http.password = strdup(password); - if (target->conf.http.password == NULL) + if (auth) { + target->conf.http.auth = strdup(auth); + if (target->conf.http.auth == NULL) fatal("strdup"); } - if (hmac_secret) { - target->conf.http.hmac_secret = strdup(hmac_secret); - if (target->conf.http.hmac_secret == NULL) + if (hmac) { + target->conf.http.hmac = strdup(hmac); + if (target->conf.http.hmac == NULL) fatal("strdup"); } blob - /dev/null blob + ed01d9084a794a49fa7bb2649a4a635bba6c744f (mode 644) --- /dev/null +++ gotd/secrets.c @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2024 Omar Polo + * + * 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. + */ + +#include +#include +#include +#include + +#include "got_error.h" + +#include "log.h" +#include "secrets.h" + +static const struct got_error * +push(struct gotd_secrets *s, const char *path, int lineno, + const char *type, const char *key, const char *val) +{ + size_t newcap, i; + void *t; + + if (s->len == s->cap) { + newcap = s->cap + 16; + t = reallocarray(s->secrets, newcap, sizeof(*s->secrets)); + if (t == NULL) + return got_error_from_errno("reallocarray"); + s->secrets = t; + s->cap = newcap; + } + + i = s->len; + if (!strcmp(type, "auth")) + s->secrets[i].type = GOTD_SECRET_AUTH; + else if (!strcmp(type, "hmac")) + s->secrets[i].type = GOTD_SECRET_HMAC; + else { + log_warnx("%s:%d invalid type %s", path, lineno, type); + return got_error(GOT_ERR_PARSE_CONFIG); + } + + if (gotd_secrets_get(s, s->secrets[i].type, key) != NULL) { + log_warnx("%s:%d duplicate %s entry %s", path, lineno, + type, key); + return got_error(GOT_ERR_PARSE_CONFIG); + } + + s->secrets[i].key = strdup(key); + if (s->secrets[i].key == NULL) + return got_error_from_errno("strdup"); + s->secrets[i].val = strdup(val); + if (s->secrets[i].val == NULL) + return got_error_from_errno("strdup"); + + s->len++; + return NULL; +} + +const struct got_error * +gotd_secrets_parse(const char *path, FILE *fp, struct gotd_secrets **s) +{ + const struct got_error *err = NULL; + int lineno = 0; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + char *type, *key, *val, *t; + struct gotd_secrets *secrets; + + *s = NULL; + + secrets = calloc(1, sizeof(*secrets)); + if (secrets == NULL) + return got_error_from_errno("calloc"); + + while ((linelen = getline(&line, &linesize, fp)) != -1) { + lineno++; + if (line[linelen - 1] == '\n') + line[--linelen] = '\0'; + + if (*line == '\0' || *line == '#') + continue; + + type = line; + + key = type + strcspn(type, " \t"); + *key++ = '\0'; + key += strspn(key, " \t"); + + val = key + strcspn(key, " \t"); + *val++ = '\0'; + val += strspn(val, " \t"); + + t = val + strcspn(val, " \t"); + if (*t != '\0') { + log_warnx("%s:%d malformed entry\n", path, lineno); + err = got_error(GOT_ERR_PARSE_CONFIG); + break; + } + + err = push(secrets, path, lineno, type, key, val); + if (err) + break; + } + free(line); + if (ferror(fp) && err == NULL) + err = got_error_from_errno("getline"); + + if (err) { + gotd_secrets_free(secrets); + secrets = NULL; + } + + *s = secrets; + return err; +} + +const char * +gotd_secrets_get(struct gotd_secrets *s, enum gotd_secret_type type, + const char *key) +{ + size_t i; + + for (i = 0; i < s->len; ++i) { + if (s->secrets[i].type != type) + continue; + if (strcmp(s->secrets[i].key, key) != 0) + continue; + return s->secrets[i].val; + } + + return NULL; +} + +void +gotd_secrets_free(struct gotd_secrets *s) +{ + size_t i; + + for (i = 0; i < s->len; ++i) { + free(s->secrets[i].key); + free(s->secrets[i].val); + } + + free(s); +} blob - /dev/null blob + 5fd139e32a18178b1d1232c954b80219a532d5ce (mode 644) --- /dev/null +++ gotd/secrets.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Omar Polo + * + * 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. + */ + +enum gotd_secret_type { + GOTD_SECRET_AUTH, + GOTD_SECRET_HMAC, +}; + +struct gotd_secret { + enum gotd_secret_type type; + char *key; /* label or username */ + char *val; /* hmac secret or password */ +}; + +struct gotd_secrets { + struct gotd_secret *secrets; + size_t len; + size_t cap; +}; + +const struct got_error *gotd_secrets_parse(const char *, FILE *, + struct gotd_secrets **); +const char *gotd_secrets_get(struct gotd_secrets *, enum gotd_secret_type, + const char *); +void gotd_secrets_free(struct gotd_secrets *); blob - a460654e422802961de72d85be843313f25dc003 blob + 1efc9a36789275292bd8b1a0612e4d1844ceec7c --- regress/gotd/.gitignore +++ regress/gotd/.gitignore @@ -1 +1,2 @@ gotd.conf +gotd-secrets.conf blob - 5bcb3d151e4aa620cbbc366ecb2783c7b851d6fa blob + d2f5c834b32eb56747464e08d55c728712cec941 --- regress/gotd/Makefile +++ regress/gotd/Makefile @@ -187,44 +187,48 @@ start_gotd_email_notification: ensure_root @$(GOTD_TRAP); sleep .5 start_gotd_http_notification: ensure_root + @echo 'auth flan password' > $(PWD)/gotd-secrets.conf @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' >> $(PWD)/gotd.conf + @echo ' url "http://localhost:${GOTD_TEST_HTTP_PORT}/" auth flan insecure' >> $(PWD)/gotd.conf @echo " }" >> $(PWD)/gotd.conf @echo "}" >> $(PWD)/gotd.conf - @$(GOTD_TRAP); $(GOTD_START_CMD) + @$(GOTD_TRAP); $(GOTD_START_CMD) -s $(PWD)/gotd-secrets.conf @$(GOTD_TRAP); sleep .5 start_gotd_email_and_http_notification: ensure_root + @echo 'auth flan password' > $(PWD)/gotd-secrets.conf @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' >> $(PWD)/gotd.conf + @echo ' url "http://localhost:${GOTD_TEST_HTTP_PORT}/" auth flan insecure' >> $(PWD)/gotd.conf @echo -n ' email to ${GOTD_DEVUSER}' >> $(PWD)/gotd.conf @echo ' relay 127.0.0.1 port ${GOTD_TEST_SMTP_PORT}' >> $(PWD)/gotd.conf @echo " }" >> $(PWD)/gotd.conf @echo "}" >> $(PWD)/gotd.conf - @$(GOTD_TRAP); $(GOTD_START_CMD) + @$(GOTD_TRAP); $(GOTD_START_CMD) -s $(PWD)/gotd-secrets.conf @$(GOTD_TRAP); sleep .5 start_gotd_http_notification_hmac: ensure_root + @echo 'auth flan password' > $(PWD)/gotd-secrets.conf + @echo 'hmac flan ${GOTD_TEST_HMAC_SECRET}' >> $(PWD)/gotd-secrets.conf @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 ' url "http://localhost:${GOTD_TEST_HTTP_PORT}/" auth flan insecure hmac flan' >> $(PWD)/gotd.conf @echo " }" >> $(PWD)/gotd.conf @echo "}" >> $(PWD)/gotd.conf - @$(GOTD_TRAP); $(GOTD_START_CMD) + @$(GOTD_TRAP); $(GOTD_START_CMD) -s $(PWD)/gotd-secrets.conf @$(GOTD_TRAP); sleep .5 prepare_test_repo: ensure_root