summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGab Virebent <gab@virebent.art>2026-06-26 19:20:00 +0200
committerGab Virebent <gab@virebent.art>2026-06-26 19:20:00 +0200
commitfb48d1308d2f63f8e9b23c5d1d921783fa0dacbe (patch)
treeceecce2f057cd4ea1e6724305fcd52915939c419
parent41c4bfecc770ea7c914e845e4ac3792cb4a473d3 (diff)
downloadgmnisrv-fb48d1308d2f63f8e9b23c5d1d921783fa0dacbe.tar.gz
gmnisrv-fb48d1308d2f63f8e9b23c5d1d921783fa0dacbe.tar.xz
gmnisrv-fb48d1308d2f63f8e9b23c5d1d921783fa0dacbe.zip
Serve CA full chain (Let's Encrypt), TLS 1.3, fix handshake buffer overflowHEADmain
- tls.c/config.h: read full PEM chain into STACK_OF(X509), send via SSL_set1_chain - tls.c: minimum protocol TLS 1.2 -> TLS 1.3 - server.c: drain handshake output via local buffer loop instead of staging into the fixed 4 KB client buffer (a full CA chain overflowed it -> assert) - FORK.md: describe the fork
-rw-r--r--FORK.md40
-rw-r--r--include/config.h1
-rw-r--r--src/server.c34
-rw-r--r--src/tls.c23
4 files changed, 81 insertions, 17 deletions
diff --git a/FORK.md b/FORK.md
new file mode 100644
index 0000000..fc3f889
--- /dev/null
+++ b/FORK.md
@@ -0,0 +1,40 @@
+# Virebent fork of gmnisrv
+
+This is a fork of [gmnisrv](https://git.sr.ht/~sircmpwn/gmnisrv), a small
+Gemini server, with three patches that let it serve **CA-signed certificates
+(e.g. Let's Encrypt)** instead of being limited to self-signed TOFU certs.
+
+## Why
+
+Upstream gmnisrv reads only the leaf certificate from the cert file
+(`PEM_read_X509`) and never sends the intermediate chain, so clients cannot
+build a path to a trusted root — CA validation always fails and you are stuck
+with TOFU. agate has the same limitation by design. This fork makes a Gemini
+capsule verifiable with a normal Let's Encrypt certificate.
+
+## Patches
+
+1. **Full certificate chain** — `tls_host_init` now reads every certificate in
+ the PEM (leaf + intermediates) into a `STACK_OF(X509)`, and `tls_set_host`
+ sends it with `SSL_set1_chain`. (`src/tls.c`, `include/config.h`)
+2. **TLS 1.3 only** — minimum protocol bumped from TLS 1.2 to TLS 1.3.
+ (`src/tls.c`)
+3. **Handshake buffer overflow fix** — the handshake flush staged the whole
+ TLS output into a fixed 4 KB buffer. A full CA chain (~5 KB) overflowed it
+ and aborted the process on an `assert` — a crash loop on any public port hit
+ by scanners. The flush now drains the write BIO through a local buffer in a
+ loop, like the response path already does. (`src/server.c`)
+
+## Build
+
+```
+./configure
+# OpenSSL 3.x emits deprecation warnings for the EC self-signed generator;
+# allow them through:
+echo 'CFLAGS += -Wno-error=deprecated-declarations' >> .build/config.mk
+make gmnisrv
+```
+
+Put the Let's Encrypt `fullchain.pem` as `<store>/<hostname>.crt` and
+`privkey.pem` as `<store>/<hostname>.key`, and refresh them from a certbot
+deploy hook on renewal.
diff --git a/include/config.h b/include/config.h
index f893b20..071c253 100644
--- a/include/config.h
+++ b/include/config.h
@@ -38,6 +38,7 @@ struct gmnisrv_host {
char *hostname;
X509 *x509;
EVP_PKEY *pkey;
+ STACK_OF(X509) *chain;
struct gmnisrv_route *routes;
diff --git a/src/server.c b/src/server.c
index 359300a..022aa3d 100644
--- a/src/server.c
+++ b/src/server.c
@@ -376,28 +376,34 @@ client_readable(struct gmnisrv_server *server, struct gmnisrv_client *client)
return CONNECTED;
queue_ssl_write:
- client->bufln = 0;
- client->state = CLIENT_STATE_SSL;
- client->next = CLIENT_STATE_REQUEST;
+ // Flush all pending TLS output (e.g. the ServerHello/Certificate
+ // handshake flight) directly to the socket, draining the write BIO via
+ // a local buffer in a loop. A full CA certificate chain can exceed
+ // sizeof(client->buf), so it must not be staged into the fixed-size
+ // client buffer (that overflowed and aborted on assert).
do {
- assert(client->bufln < sizeof(client->buf));
- r = BIO_read(client->wbio,
- &client->buf[client->bufln],
- sizeof(client->buf) - client->bufln);
- if (r <= 0) {
- if (BIO_should_retry(client->wbio)) {
- continue;
- }
+ r = BIO_read(client->wbio, buf, sizeof(buf));
+ if (r < 0 && !BIO_should_retry(client->wbio)) {
client_error(&client->addr,
"BIO read error: %s",
ERR_error_string(r, NULL));
disconnect_client(server, client);
return DISCONNECTED;
- } else {
- client->bufln += r;
- client->pollfd->events = POLLOUT;
+ }
+ for (int ww = 0; ww < r; ) {
+ int q = write(client->sockfd, &buf[ww], r - ww);
+ if (q < 0) {
+ client_error(&client->addr,
+ "client write: %s",
+ strerror(errno));
+ disconnect_client(server, client);
+ return DISCONNECTED;
+ }
+ ww += q;
}
} while (r > 0);
+ client->state = CLIENT_STATE_REQUEST;
+ client->pollfd->events = POLLIN;
return CONNECTED;
}
diff --git a/src/tls.c b/src/tls.c
index 02d7ab7..8f9bb81 100644
--- a/src/tls.c
+++ b/src/tls.c
@@ -140,13 +140,24 @@ tls_host_init(struct gmnisrv_tls *tlsconf, struct gmnisrv_host *host)
}
X509 *x509 = PEM_read_X509(xf, NULL, NULL, NULL);
- fclose(xf);
if (!x509) {
+ fclose(xf);
server_error("error loading certificate from %s", crtpath);
fclose(kf);
return 1;
}
+ // Read any remaining certificates in the PEM file as the intermediate
+ // chain so CA-signed certs (e.g. Let's Encrypt fullchain.pem) are sent
+ // to the client for proper validation.
+ STACK_OF(X509) *chain = sk_X509_new_null();
+ X509 *ca;
+ while ((ca = PEM_read_X509(xf, NULL, NULL, NULL)) != NULL) {
+ sk_X509_push(chain, ca);
+ }
+ ERR_clear_error();
+ fclose(xf);
+
EVP_PKEY *pkey = PEM_read_PrivateKey(kf, NULL, NULL, NULL);
fclose(kf);
if (!pkey) {
@@ -160,12 +171,15 @@ tls_host_init(struct gmnisrv_tls *tlsconf, struct gmnisrv_host *host)
assert(r == 1);
if (day < 0 || sec < 0) {
server_log("%s certificate is expired", host->hostname);
+ sk_X509_pop_free(chain, X509_free);
goto generate;
}
host->x509 = x509;
host->pkey = pkey;
- server_log("loaded certificate for %s", host->hostname);
+ host->chain = chain;
+ server_log("loaded certificate for %s (%d chain cert(s))",
+ host->hostname, sk_X509_num(chain));
return 0;
generate:
@@ -181,7 +195,7 @@ tls_init(struct gmnisrv_config *conf)
conf->tls.ssl_ctx = SSL_CTX_new(TLS_server_method());
assert(conf->tls.ssl_ctx);
- int r = SSL_CTX_set_min_proto_version(conf->tls.ssl_ctx, TLS1_2_VERSION);
+ int r = SSL_CTX_set_min_proto_version(conf->tls.ssl_ctx, TLS1_3_VERSION);
assert(r == 1);
r = SSL_CTX_set_cipher_list(conf->tls.ssl_ctx,
@@ -238,4 +252,7 @@ tls_set_host(SSL *ssl, struct gmnisrv_host *host)
{
SSL_use_certificate(ssl, host->x509);
SSL_use_PrivateKey(ssl, host->pkey);
+ if (host->chain && sk_X509_num(host->chain) > 0) {
+ SSL_set1_chain(ssl, host->chain);
+ }
}