From: "Omar Polo" Subject: Re: gotwebd ssh host key fingerprints To: Stefan Sperling Cc: gameoftrees@openbsd.org Date: Fri, 06 Feb 2026 18:28:22 +0100 Stefan Sperling wrote: > 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? okay op@ > 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 }} >
Clone URL:
>
{{ t->repo_dir->url }}
> + {{ if t->repo_dir->sshfp[GOTWEBD_SSHFP_ECDSA] }} > +
ECDSA:
> +
{{ t->repo_dir->sshfp[GOTWEBD_SSHFP_ECDSA] }}
> {{ end }} > + {{ if t->repo_dir->sshfp[GOTWEBD_SSHFP_ED25519] }} > +
ED25519:
> +
{{ t->repo_dir->sshfp[GOTWEBD_SSHFP_ED25519] }}
> + {{ end }} > + {{ if t->repo_dir->sshfp[GOTWEBD_SSHFP_RSA] }} > +
RSA:
> +
{{ t->repo_dir->sshfp[GOTWEBD_SSHFP_RSA] }}
> + {{ end }} > + {{ end }} > >
> {{ 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 STRING > %token 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 },