Commit Diff


commit - 4d982e99055cf01430da8b639924c8b13726090e
commit + 6e1b28b643862111cb5b8b916fef8249b1b36c97
blob - f64b1024e378845bb310d66748b3ffb736d8022a
blob + 211ce127da138b7f0ae6451335215737fabc1c1a
--- gitwrapper/gitwrapper.c
+++ gitwrapper/gitwrapper.c
@@ -55,9 +55,9 @@
 #endif
 
 /* only needed to satisfy the linker */
-const char *
+struct gotd_secret *
 gotd_secrets_get(struct gotd_secrets *secrets, enum gotd_secret_type t,
-    const char *key)
+    const char *label)
 {
 	return NULL;
 }
blob - 6b87a0e60a950f2a87d40202550a866c319f4d9d
blob + 78e45385e723721f6e996d5f99378d14263aee4f
--- gotd/gotd-secrets.conf.5
+++ gotd/gotd-secrets.conf.5
@@ -31,41 +31,31 @@ 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.
+Empty lines are also ignored.
 .Pp
-The entries have the following syntax:
+Each entry is made by blanks-separated words.
+Arguments containing whitespaces should be surrounded by single or double
+quotes.
 .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:
+The supported entries are:
 .Bl -tag -width Ds
-.It Ic auth
+.It Ic auth Ar label Ic user Ar user Ic password Ar password
 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
+.It Ic hmac Ar label Ar secret
 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
+A suitable
+.Ar secret
+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.
+The
+.Ar label
+must be unique between entries with the same type
+.Pq i.e. Ic auth No or Ic hmac .
 .Sh FILES
 .Bl -tag -width Ds -compact
 .It Pa /etc/gotd-secrets.conf
@@ -78,7 +68,7 @@ This example configuration defines two secrets, the fi
 HTTP authentication and the second for HMAC signing.
 .Bd -literal -offset indent
 # /etc/gotd-secrets.conf
-auth flan super-strong-password!
+auth mochi user "flan" password "super-strong-password!"
 hmac hacker q0tcl8QhjYs7U75MW/2rwB30CpdbAhONkfLGxFHm/+8=
 .Ed
 .Pp
@@ -87,14 +77,12 @@ These values can be referenced in
 as:
 .Bd -literal -offset indent
 # /etc/gotd.conf
-repository "openbsd/ports" {
-	path "/var/git/ports.git"
-	permit rw :porters
-	permit ro anonymous
+repository "openbsd/src" {
+	path "/var/git/src.git"
+	permit rw :hackers
 
 	notify {
-		url https://flan.com/notify/ auth flan
-		url https://hacker.com/notify/ hmac hacker
+		url https://flan.com/ci/ auth mochi hmac hacker
 	}
 }
 .El
blob - f5488c6250b396af6322ec60a1446b0adc75e0a0
blob + dae76c4625082615a1525efca313529fb5221c27
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -2411,27 +2411,23 @@ main(int argc, char **argv)
 
 		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[1].iov_base = s->label;
+			iov[1].iov_len = strlen(s->label) + 1;
 
-			iov[2].iov_base = &vallen;
-			iov[2].iov_len = sizeof(vallen);
+			iov[2].iov_base = s->user;
+			iov[2].iov_len = s->user ? strlen(s->user) + 1 : 0 ;
 
-			iov[3].iov_base = s->key;
-			iov[3].iov_len = keylen;
+			iov[3].iov_base = s->pass;
+			iov[3].iov_len = s->pass ? strlen(s->pass) + 1 : 0 ;
 
-			iov[4].iov_base = s->val;
-			iov[4].iov_len = vallen;
+			iov[4].iov_base = s->hmac;
+			iov[4].iov_len = s->hmac ? strlen(s->hmac) + 1 : 0 ;
 
 			if (imsg_composev(imsgbuf, GOTD_IMSG_SECRET,
 			    0, 0, -1, iov, 5) == -1)
blob - 08fee725f18dbcca82f68aba86a3132f5368fda6
blob + 6c4f25f2f3c6e6277708d4483155e5783e9a8b49
--- gotd/notify.c
+++ gotd/notify.c
@@ -276,6 +276,7 @@ static void
 notify_http(struct gotd_notification_target *target, const char *repo,
     const char *username, int fd)
 {
+	struct gotd_secret *secret;
 	const char *http_user = NULL, *http_pass = NULL, *hmac = NULL;
 	const char *argv[12];
 	int argc = 0;
@@ -298,13 +299,15 @@ notify_http(struct gotd_notification_target *target, c
 	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);
+		secret = gotd_secrets_get(&secrets, GOTD_SECRET_AUTH,
+		    target->conf.http.auth);
+		http_user = secret->user;
+		http_pass = secret->pass;
 	}
 	if (target->conf.http.hmac) {
-		hmac = gotd_secrets_get(&secrets, GOTD_SECRET_HMAC,
+		secret = gotd_secrets_get(&secrets, GOTD_SECRET_HMAC,
 		    target->conf.http.hmac);
+		hmac = secret->hmac;
 	}
 
 	run_notification_helper(GOTD_PATH_PROG_NOTIFY_HTTP, argv, fd,
@@ -466,7 +469,34 @@ recv_session(struct imsg *imsg)
 
 	return NULL;
 }
+
+static const struct got_error *
+notify_ibuf_get_str(char **ret, struct ibuf *ibuf)
+{
+	const char	*str, *end;
+	size_t		 len;
+
+	*ret = NULL;
 
+	str = ibuf_data(ibuf);
+	len = ibuf_size(ibuf);
+
+	end = memchr(str, '\0', len);
+	if (end == NULL)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	*ret = strdup(str);
+	if (*ret == NULL)
+		return got_error_from_errno("strdup");
+
+	if (ibuf_skip(ibuf, end - str + 1) == -1) {
+		free(*ret);
+		*ret = NULL;
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	}
+
+	return NULL;
+}
+
 static void
 notify_dispatch(int fd, short event, void *arg)
 {
@@ -477,8 +507,6 @@ notify_dispatch(int fd, short event, void *arg)
 	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)
@@ -532,19 +560,26 @@ notify_dispatch(int fd, short event, void *arg)
 			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')
+			if (ibuf_get(&ibuf, &s->type, sizeof(s->type)) == -1)
 				fatalx("corrupted GOTD_IMSG_SECRET");
-			s->key = strdup(key);
-			s->val = strdup(val);
-			if (s->key == NULL || s->val == NULL)
-				fatal("strdup");
+			err = notify_ibuf_get_str(&s->label, &ibuf);
+			if (err)
+				break;
+			if (s->type == GOTD_SECRET_AUTH) {
+				err = notify_ibuf_get_str(&s->user, &ibuf);
+				if (err)
+					break;
+				err = notify_ibuf_get_str(&s->pass, &ibuf);
+				if (err)
+					break;
+			} else {
+				err = notify_ibuf_get_str(&s->hmac, &ibuf);
+				if (err)
+					break;
+			}
+			if (ibuf_size(&ibuf) != 0)
+				fatalx("unexpected extra data in "
+				    "GOTD_IMSG_SECRET");
 			break;
 		default:
 			log_debug("unexpected imsg %d", imsg.hdr.type);
blob - 3645645467b19a36a03d4f527df5743da2de2d17
blob + 3407009d5a8a939edea8ac031e24af4b599e567c
--- gotd/secrets.c
+++ gotd/secrets.c
@@ -25,8 +25,8 @@
 #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)
+push(struct gotd_secrets *s, enum gotd_secret_type type, const char *label,
+    const char *user, const char *pass, const char *hmac)
 {
 	size_t			 newcap, i;
 	void			*t;
@@ -41,30 +41,175 @@ push(struct gotd_secrets *s, const char *path, int lin
 	}
 
 	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;
+	memset(&s->secrets[i], 0, sizeof(s->secrets[i]));
+	s->secrets[i].type = type;
+	s->secrets[i].label = strdup(label);
+	if (s->secrets[i].label == NULL)
+		return got_error_from_errno("strdup");
+
+	if (type == GOTD_SECRET_AUTH) {
+		s->secrets[i].user = strdup(user);
+		if (s->secrets[i].user == NULL)
+			return got_error_from_errno("strdup");
+		s->secrets[i].pass = strdup(pass);
+		if (s->secrets[i].pass == NULL)
+			return got_error_from_errno("strdup");
+	} else {
+		s->secrets[i].hmac = strdup(hmac);
+		if (s->secrets[i].hmac == NULL)
+			return got_error_from_errno("strdup");
+	}
+
+	s->len++;
+	return NULL;
+}
+
+static char *
+read_word(char **word, const char *path, int lineno, char *s)
+{
+	char			*p, quote = 0;
+	int			 escape = 0;
+
+	s += strspn(s, " \t");
+	if (*s == '\0') {
+		log_warnx("%s:%d syntax error", path, lineno);
+		return NULL;
+	}
+	*word = s;
+
+	p = s;
+	while (*s) {
+		if (escape) {
+			escape = 0;
+			*p++ = *s++;
+			continue;
+		}
+
+		if (*s == '\\') {
+			escape = 1;
+			s++;
+			continue;
+		}
+
+		if (*s == quote) {
+			quote = 0;
+			s++;
+			continue;
+		}
+
+		if (*s == '\'' || *s == '\"') {
+			quote = *s;
+			s++;
+			continue;
+		}
+
+		if (!quote && (*s == ' ' || *s == '\t')) {
+			*p = '\0';
+			return s + 1;
+		}
+
+		*p++ = *s++;
+	}
+
+	if (quote) {
+		log_warnx("%s:%d no closing quote", path, lineno);
+		return NULL;
+	}
+
+	if (escape) {
+		log_warnx("%s:%d unterminated escape at end of line",
+		    path, lineno);
+		return NULL;
+	}
+
+	*p = '\0';
+	return s;
+}
+
+static char *
+read_keyword(char **kw, const char *path, int lineno, char *s)
+{
+	s += strspn(s, " \t");
+	if (*s == '\0') {
+		log_warnx("%s:%d syntax error", path, lineno);
+		return NULL;
+	}
+	*kw = s;
+
+	s += strcspn(s, " \t");
+	if (*s != '\0')
+		*s++ = '\0';
+	return s;
+}
+
+static const struct got_error *
+parse_line(struct gotd_secrets *secrets, const char *path, int lineno,
+    char *line)
+{
+	char			*kw, *label;
+	char			*user = NULL, *pass = NULL, *hmac = NULL;
+	enum gotd_secret_type	 type;
+
+	line = read_keyword(&kw, path, lineno, line);
+	if (line == NULL)
+		return got_error(GOT_ERR_PARSE_CONFIG);
+
+	if (!strcmp(kw, "auth"))
+		type = GOTD_SECRET_AUTH;
+	else if (!strcmp(kw, "hmac"))
+		type = GOTD_SECRET_HMAC;
 	else {
-		log_warnx("%s:%d invalid type %s", path, lineno, type);
+		log_warnx("%s:%d syntax error", path, lineno);
 		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);
+	line = read_word(&label, path, lineno, line);
+	if (line == NULL)
 		return got_error(GOT_ERR_PARSE_CONFIG);
+
+	if (type == GOTD_SECRET_AUTH) {
+		line = read_keyword(&kw, path, lineno, line);
+		if (line == NULL)
+			return got_error(GOT_ERR_PARSE_CONFIG);
+		if (strcmp(kw, "user") != 0) {
+			log_warnx("%s:%d syntax error", path, lineno);
+			return got_error(GOT_ERR_PARSE_CONFIG);
+		}
+
+		line = read_word(&user, path, lineno, line);
+		if (line == NULL)
+			return got_error(GOT_ERR_PARSE_CONFIG);
+
+		line = read_keyword(&kw, path, lineno, line);
+		if (line == NULL)
+			return got_error(GOT_ERR_PARSE_CONFIG);
+		if (strcmp(kw, "password") != 0) {
+			log_warnx("%s:%d syntax error", path, lineno);
+			return got_error(GOT_ERR_PARSE_CONFIG);
+		}
+
+		line = read_word(&pass, path, lineno, line);
+		if (line == NULL)
+			return got_error(GOT_ERR_PARSE_CONFIG);
+	} else {
+		line = read_word(&hmac, path, lineno, line);
+		if (line == NULL)
+			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");
+	line += strspn(line, " \t");
+	if (*line != '\0') {
+		log_warnx("%s:%d syntax error", path, lineno);
+		return got_error(GOT_ERR_PARSE_CONFIG);
+	}
 
-	s->len++;
-	return NULL;
+	if (gotd_secrets_get(secrets, type, label) != NULL) {
+		log_warnx("%s:%d duplicate %s entry %s", path, lineno,
+		    type == GOTD_SECRET_AUTH ? "auth" : "hmac", label);
+		return got_error(GOT_ERR_PARSE_CONFIG);
+	}
+
+	return push(secrets, type, label, user, pass, hmac);
 }
 
 const struct got_error *
@@ -75,7 +220,7 @@ gotd_secrets_parse(const char *path, FILE *fp, struct 
 	char			*line = NULL;
 	size_t			 linesize = 0;
 	ssize_t			 linelen;
-	char			*type, *key, *val, *t;
+	char			*t;
 	struct gotd_secrets	*secrets;
 
 	*s = NULL;
@@ -89,27 +234,13 @@ gotd_secrets_parse(const char *path, FILE *fp, struct 
 		if (line[linelen - 1] == '\n')
 			line[--linelen] = '\0';
 
-		if (*line == '\0' || *line == '#')
+		for (t = line; *t == ' ' || *t == '\t'; ++t)
+			/* nop */ ;
+
+		if (*t == '\0' || *t == '#')
 			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);
+		err = parse_line(secrets, path, lineno, t);
 		if (err)
 			break;
 	}
@@ -126,18 +257,18 @@ gotd_secrets_parse(const char *path, FILE *fp, struct 
 	return err;
 }
 
-const char *
+struct gotd_secret *
 gotd_secrets_get(struct gotd_secrets *s, enum gotd_secret_type type,
-    const char *key)
+    const char *label)
 {
 	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)
+		if (strcmp(s->secrets[i].label, label) != 0)
 			continue;
-		return s->secrets[i].val;
+		return &s->secrets[i];
 	}
 
 	return NULL;
@@ -152,8 +283,10 @@ gotd_secrets_free(struct gotd_secrets *s)
 		return;
 
 	for (i = 0; i < s->len; ++i) {
-		free(s->secrets[i].key);
-		free(s->secrets[i].val);
+		free(s->secrets[i].label);
+		free(s->secrets[i].user);
+		free(s->secrets[i].pass);
+		free(s->secrets[i].hmac);
 	}
 
 	free(s);
blob - 5fd139e32a18178b1d1232c954b80219a532d5ce
blob + c7aae88890d730e3497720296c513e7c2e639c1f
--- gotd/secrets.h
+++ gotd/secrets.h
@@ -21,8 +21,10 @@ enum gotd_secret_type {
 
 struct gotd_secret {
 	enum gotd_secret_type	 type;
-	char			*key;	/* label or username		*/
-	char			*val;	/* hmac secret or password	*/
+	char			*label;
+	char			*user;
+	char			*pass;
+	char			*hmac;
 };
 
 struct gotd_secrets {
@@ -33,6 +35,6 @@ struct gotd_secrets {
 
 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 *);
+struct gotd_secret *gotd_secrets_get(struct gotd_secrets *,
+    enum gotd_secret_type, const char *);
 void gotd_secrets_free(struct gotd_secrets *);
blob - c3b16b4b439df9cf2fee1a80ae0caffffa7ab0de
blob + 667368ea6d53f44cbd7bd376394d98cf5d3c8ad0
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
@@ -75,7 +75,7 @@ ensure_root:
 	fi
 
 ensure_secrets:
-	@echo 'auth flan password' > $(PWD)/gotd-secrets.conf
+	@echo 'auth flan user flan password password' > $(PWD)/gotd-secrets.conf
 	@echo 'hmac flan ${GOTD_TEST_HMAC_SECRET}' >> $(PWD)/gotd-secrets.conf
 	@chown root:0 $(PWD)/gotd-secrets.conf
 	@chmod 600 $(PWD)/gotd-secrets.conf
blob - /dev/null
blob + fa38aa565b73c665735383b9d5e89ab892024296 (mode 644)
--- /dev/null
+++ regress/secrets/01.conf
@@ -0,0 +1,7 @@
+# comment
+  # another comment
+
+hmac mochi MWZJblv6BNcmq4maua3XxV0VhsiVzNPXy4NdOdliD/o=
+
+auth mochi user "flan" password s3cr37!
+
blob - /dev/null
blob + e2bb78be82355cb00eafe6ba3b0e84c160ae71a5 (mode 644)
--- /dev/null
+++ regress/secrets/01.exp
@@ -0,0 +1,2 @@
+hmac mochi MWZJblv6BNcmq4maua3XxV0VhsiVzNPXy4NdOdliD/o=
+auth mochi user flan password s3cr37!
blob - /dev/null
blob + 022028b8dd8473cef44adda54fb260d5f9fc9636 (mode 644)
--- /dev/null
+++ regress/secrets/02.conf
@@ -0,0 +1,2 @@
+auth "mochi" user fl"a"'n' password "hello world"' !'
+auth mochi2 user flan password hello\ world\ \"!
blob - /dev/null
blob + 1afaa89044652a0e501b2126a22ade9a12df920e (mode 644)
--- /dev/null
+++ regress/secrets/02.exp
@@ -0,0 +1,2 @@
+auth mochi user flan password hello world !
+auth mochi2 user flan password hello world "!
blob - /dev/null
blob + 6d6bea38ab10b382b0a4f3edb21447810face661 (mode 644)
--- /dev/null
+++ regress/secrets/03.conf
@@ -0,0 +1 @@
+auth mochi user flan password "foo
blob - /dev/null
blob + 4283ca13d5e67b815966c80be3bef7c230110e8e (mode 644)
--- /dev/null
+++ regress/secrets/04.conf
@@ -0,0 +1 @@
+hmac mochi foo\
blob - /dev/null
blob + 4283ca13d5e67b815966c80be3bef7c230110e8e (mode 644)
--- /dev/null
+++ regress/secrets/05.conf
@@ -0,0 +1 @@
+hmac mochi foo\
blob - /dev/null
blob + a7171f20953ee46932809c2cdb7deac376f5d7ee (mode 644)
--- /dev/null
+++ regress/secrets/Makefile
@@ -0,0 +1,33 @@
+.PATH:${.CURDIR}/../../gotd/
+.PATH:${.CURDIR}/../../lib/
+
+PROG =		secrets
+SRCS =		secrets-parser.c error.c hash.c log.c secrets.c 
+
+CPPFLAGS +=	-I${.CURDIR}/../../include -I${.CURDIR}/../../lib
+CPPFLAGS +=	-I${.CURDIR}/../../gotd
+
+REGRESS_TARGETS =	empty comments quotes unclosed invalid-escape syntax
+
+empty:
+	./secrets /dev/null | diff -u /dev/null -
+
+comments:
+	./secrets ${.CURDIR}/01.conf | diff -u ${.CURDIR}/01.exp -
+
+quotes:
+	./secrets ${.CURDIR}/02.conf | diff -u ${.CURDIR}/02.exp -
+
+unclosed:
+	! ./secrets ${.CURDIR}/03.conf
+	@echo "expected failure; it's OK"
+
+invalid-escape:
+	! ./secrets ${.CURDIR}/04.conf
+	@echo "expected failure; it's OK"
+
+syntax:
+	! ./secrets ${.CURDIR}/05.conf
+	@echo "expected failure; it's OK"
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + 94fc117c61f36b5b7a021f2bf1c537b13e3b2555 (mode 644)
--- /dev/null
+++ regress/secrets/secrets-parser.c
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2024 Omar Polo <op@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.
+ */
+
+#include <err.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "got_error.h"
+
+#include "log.h"
+#include "secrets.h"
+
+static void __dead
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-v] file\n", getprogname());
+	exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+	const struct got_error	*error;
+	struct gotd_secrets	*secrets;
+	struct gotd_secret	*secret;
+	FILE			*fp;
+	size_t			 i;
+	int			 ch, verbose = 0;
+
+	if (pledge("stdio rpath", NULL) == -1)
+		err(1, "pledge");
+
+	while ((ch = getopt(argc, argv, "v")) != -1) {
+		switch (ch) {
+		case 'v':
+			verbose = 1;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (argc != 1)
+		usage();
+
+	log_init(1, LOG_USER);
+	log_procinit("secrets");
+	log_setverbose(verbose);
+
+	if ((fp = fopen(argv[0], "r")) == NULL)
+		err(1, "can't open %s", argv[0]);
+
+	if ((error = gotd_secrets_parse(argv[0], fp, &secrets)) != NULL)
+		errx(1, "failed to parse %s: %s", argv[0], error->msg);
+
+	for (i = 0; i < secrets->len; ++i) {
+		secret = &secrets->secrets[i];
+
+		if (secret->type == GOTD_SECRET_AUTH) {
+			printf("auth %s user %s password %s\n",
+			    secret->label, secret->user, secret->pass);
+		} else {
+			printf("hmac %s %s\n", secret->label, secret->hmac);
+		}
+	}
+
+	gotd_secrets_free(secrets);
+	return 0;
+}