"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
gotwebd ssh host key fingerprints
To:
gameoftrees@openbsd.org
Date:
Fri, 6 Feb 2026 11:02:48 +0100

Download raw body.

Thread
Add options to gotwebd.conf which set SSH host key fingeprints to
be displayed beneath the clone URL.

In gotwebd.conf the clone URL is arbitrary, which means the admin needs to
be able to specify key fingerprints per clone-URL, rather than per server.
So, for now, ssh fingerprints must be set per repository. If this causes
too much copy-pasting we can introduce equivalent server-scope options later.

gotsysd will be able to make assumptions about how the server is set
up and generate appropriate gotwebd.conf stanzas automatically.
A patch for gotsysd will follow.

ok?

M  gotwebd/gotweb.c        |  21+  0-
M  gotwebd/gotwebd.conf.5  |  12+  0-
M  gotwebd/gotwebd.h       |   8+  0-
M  gotwebd/pages.tmpl      |  12+  0-
M  gotwebd/parse.y         |  70+  0-

5 files changed, 123 insertions(+), 0 deletions(-)

commit - 8e075c1ec8542ad827b525234bea2eba76a011ad
commit + 896f2e7025bdfa07698b31229ccfdd7de5984ba4
blob - f4dc9ee62c67b484e6013abf9d57672569a9f3b9
blob + 2dcdf486c002a5a2a948db88611b6912fc1de984
--- gotwebd/gotweb.c
+++ gotwebd/gotweb.c
@@ -1124,11 +1124,15 @@ gotweb_free_repo_commit(struct repo_commit *rc)
 static void
 gotweb_free_repo_dir(struct repo_dir *repo_dir)
 {
+	size_t i;
+
 	if (repo_dir != NULL) {
 		free(repo_dir->name);
 		free(repo_dir->owner);
 		free(repo_dir->description);
 		free(repo_dir->url);
+		for (i = 0; i < nitems(repo_dir->sshfp); i++)
+			free(repo_dir->sshfp[i]);
 		free(repo_dir->path);
 	}
 	free(repo_dir);
@@ -1708,6 +1712,23 @@ gotweb_load_got_path(struct repo_dir **rp, const char 
 		error = gotweb_get_clone_url(&repo_dir->url, srv,
 		    repo_dir->path, dirfd(dt));
 	}
+
+	if (srv->show_repo_cloneurl && repo) {
+		size_t i;
+
+		for (i = 0; i < nitems(repo->clone_url_hostkey); i++) {
+			if (repo->clone_url_hostkey[i][0] == '\0')
+				continue;
+
+			repo_dir->sshfp[i] = strdup(repo->clone_url_hostkey[i]);
+			if (repo_dir->sshfp[i] == NULL) {
+				error = got_error_from_errno("strdup");
+				goto err;
+			}
+		}
+
+	}
+
 err:
 	free(dir_test);
 	if (dt != NULL && closedir(dt) == EOF && error == NULL)
blob - dd2a181f36cae86843ad2bd1da24552cfd704f34
blob + 213c5b4be700f2eab0b7e9c32341ac7d848a4123
--- gotwebd/gotwebd.conf.5
+++ gotwebd/gotwebd.conf.5
@@ -497,6 +497,18 @@ then URLs stored in the repository's
 .Pa cloneurl
 file will be shown instead.
 This file may contain multiple URLs, one per line.
+.It Ic ssh_hostkey_ecdsa Ar string
+Set the server's SSH ECDSA host key fingerprint to be displayed beneath
+the clone URL.
+Should be set when the clone URL uses the SSH protocol.
+.It Ic ssh_hostkey_ed25519 Ar string
+Set the server's SSH ED25519 host key fingerprint to be displayed beneath
+the clone URL.
+Should be set when the clone URL uses the SSH protocol.
+.It Ic ssh_hostkey_rsa Ar string
+Set the server's SSH RSA host key fingerprint to be displayed beneath
+the clone URL.
+Should be set when the clone URL uses the SSH protocol.
 .It Ic description Ar string
 Sets the repository description shown on the repository listing page.
 The
blob - af216fb97464e9ca56a9e377fdd5fc49502ebd0f
blob + 69c2896f72735357062dc4f09af598567a4aa339
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -54,7 +54,13 @@
 #define GOTWEBD_MAXPORT		 6
 #define GOTWEBD_NUMPROC		 3
 #define GOTWEBD_SOCK_FILENO	 3
+#define GOTWEBD_MAX_SSHFP	 64
 
+#define GOTWEBD_SSHFP_ECDSA	0
+#define GOTWEBD_SSHFP_ED25519	1
+#define GOTWEBD_SSHFP_RSA	2
+#define GOTWEBD_NUM_SSHFP	3
+
 #define PROC_MAX_INSTANCES	 32
 
 /* GOTWEB DEFAULTS */
@@ -206,6 +212,7 @@ struct repo_dir {
 	char			*owner;
 	char			*description;
 	char			*url;
+	char			*sshfp[GOTWEBD_NUM_SSHFP];
 	time_t			 age;
 	char			*path;
 };
@@ -398,6 +405,7 @@ struct gotwebd_repo {
 	char name[NAME_MAX];
 	char description[GOTWEBD_MAXDESCRSZ];
 	char clone_url[GOTWEBD_MAXCLONEURLSZ];
+	char clone_url_hostkey[GOTWEBD_NUM_SSHFP][GOTWEBD_MAX_SSHFP];
 
 	enum gotwebd_auth_config	auth_config;
 	struct gotwebd_access_rule_list access_rules;
blob - cebcb17ddb34bf214026aa7e215c0e0f358e0824
blob + e146964ba1ffd06404dc161bd891095107b8ecc8
--- gotwebd/pages.tmpl
+++ gotwebd/pages.tmpl
@@ -1133,7 +1133,19 @@ nextsep(char *s, char **t)
   {{ if srv->show_repo_cloneurl }}
     <dt>Clone URL:</dt>
     <dd><pre class="clone-url">{{ t->repo_dir->url }}</pre></dd>
+  {{ if t->repo_dir->sshfp[GOTWEBD_SSHFP_ECDSA] }}
+    <dt>ECDSA:</dt>
+    <dd><pre class="clone-url">{{ t->repo_dir->sshfp[GOTWEBD_SSHFP_ECDSA] }}</pre></dd>
   {{ end }}
+  {{ if t->repo_dir->sshfp[GOTWEBD_SSHFP_ED25519] }}
+    <dt>ED25519:</dt>
+    <dd><pre class="clone-url">{{ t->repo_dir->sshfp[GOTWEBD_SSHFP_ED25519] }}</pre></dd>
+  {{ end }}
+  {{ if t->repo_dir->sshfp[GOTWEBD_SSHFP_RSA] }}
+    <dt>RSA:</dt>
+    <dd><pre class="clone-url">{{ t->repo_dir->sshfp[GOTWEBD_SSHFP_RSA] }}</pre></dd>
+  {{ end }}
+  {{ end }}
 </dl>
 <div class="summary-briefs">
   {{ render gotweb_render_briefs(tp) }}
blob - 03821d0d02b8707a48377d243fee6f91c949727b
blob + 06d0fa8ac5221e0906bb1adb03a35e49c67c6f35
--- gotwebd/parse.y
+++ gotwebd/parse.y
@@ -157,6 +157,7 @@ mediatype_ok(const char *s)
 %token	ENABLE DISABLE INSECURE REPOSITORY REPOSITORIES PERMIT DENY HIDE
 %token	WEBSITE PATH BRANCH REPOS_URL_PATH DESCRIPTION
 %token	TYPES INCLUDE GOTWEBD_CONTROL
+%token	SSH_HOSTKEY_ECDSA SSH_HOSTKEY_ED25519 SSH_HOSTKEY_RSA
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
@@ -978,6 +979,72 @@ repoopts1	: DISABLE AUTHENTICATION {
 				YYERROR;
 			}
 		}
+		| SSH_HOSTKEY_ECDSA STRING {
+			int i = GOTWEBD_SSHFP_ECDSA;
+
+			if (*$2 == '\0') {
+				yyerror("ssh host key fingerprint cannot be "
+				    "an empty string");
+				free($2);
+				YYERROR;
+			}
+
+			if (strlcpy(new_repo->clone_url_hostkey[i], $2,
+			    sizeof(new_repo->clone_url_hostkey[i])) >=
+			    sizeof(new_repo->clone_url_hostkey[i])) {
+				yyerror("ssh host key fingerprint too long, "
+				    "exceeds " "%zd bytes: %s",
+				    sizeof(new_repo->clone_url_hostkey[i]), $2);
+				free($2);
+				YYERROR;
+			}
+
+			free($2);
+		}
+		| SSH_HOSTKEY_ED25519 STRING {
+			int i = GOTWEBD_SSHFP_ED25519;
+
+			if (*$2 == '\0') {
+				yyerror("ssh host key fingerprint cannot be "
+				    "an empty string");
+				free($2);
+				YYERROR;
+			}
+
+			if (strlcpy(new_repo->clone_url_hostkey[i], $2,
+			    sizeof(new_repo->clone_url_hostkey[i])) >=
+			    sizeof(new_repo->clone_url_hostkey[i])) {
+				yyerror("ssh host key fingerprint too long, "
+				    "exceeds " "%zd bytes: %s",
+				    sizeof(new_repo->clone_url_hostkey[i]), $2);
+				free($2);
+				YYERROR;
+			}
+
+			free($2);
+		}
+		| SSH_HOSTKEY_RSA STRING {
+			int i = GOTWEBD_SSHFP_RSA;
+
+			if (*$2 == '\0') {
+				yyerror("ssh host key fingerprint cannot be "
+				    "an empty string");
+				free($2);
+				YYERROR;
+			}
+
+			if (strlcpy(new_repo->clone_url_hostkey[i], $2,
+			    sizeof(new_repo->clone_url_hostkey[i])) >=
+			    sizeof(new_repo->clone_url_hostkey[i])) {
+				yyerror("ssh host key fingerprint too long, "
+				    "exceeds " "%zd bytes: %s",
+				    sizeof(new_repo->clone_url_hostkey[i]), $2);
+				free($2);
+				YYERROR;
+			}
+
+			free($2);
+		}
 		;
 
 types		: TYPES	'{' optnl mediaopts_l '}'
@@ -1129,6 +1196,9 @@ lookup(char *s)
 		{ "site_name",			SITE_NAME },
 		{ "site_owner",			SITE_OWNER },
 		{ "socket",			SOCKET },
+		{ "ssh_hostkey_ecdsa",		SSH_HOSTKEY_ECDSA},
+		{ "ssh_hostkey_ed25519",	SSH_HOSTKEY_ED25519},
+		{ "ssh_hostkey_rsa",		SSH_HOSTKEY_RSA},
 		{ "summary_commits_display",	SUMMARY_COMMITS_DISPLAY },
 		{ "summary_tags_display",	SUMMARY_TAGS_DISPLAY },
 		{ "types",			TYPES },