From: Stefan Sperling Subject: gotwebd ssh host key fingerprints To: gameoftrees@openbsd.org Date: Fri, 6 Feb 2026 11:02:48 +0100 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 }}
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 },