-- Leo's gemini proxy

-- Connecting to gmi.noulin.net:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini

Using libsodium


Feed


date: 2023-01-25 11:55:07


categories: programming


firstPublishDate: 2022-09-30 23:01:38


Libsodium is a wrapper around the Nacl cryptography library which simplifies the handling of buffers.


libsodium


Nacl


I want to use libsodium because it is minimal, easy to use, easy to install and has a simple API.


I install libsodium in debian (stretch, buster, bullseye, bookworm) like this:


git clone https://github.com/jedisct1/libsodium
git checkout stable
./configure
make
make install

To use libsodium, I include `#include "sodium.h"` and link with `/usr/local/lib/libsodium.a`.


sodiumTest repository


The repository is available at:

sodiumTest


sodiumTest (gemini link)


This repository is a playground I use to learn about libsodium.


In `sodiumTest.c`, I implemented:


public key cryptograpy

public key signature, the message is signed the secret key and anyone can verify the signature with the public key

one shot encryption with secret key. Usage: encrypt a file on disk

derive key from password

password hashing for storing in a server database


`client.c` and `server.c` are client and server using public key cryptograpy to exchange messages. This is a simple prototype which uses slow public key algorithms.


`client2.c` and `server2.c` implement a request-response system which supports anonymous clients. The clients don't have to be known in advance by the server. The system uses public keys for key exchange and secret keys are derived for symetric key cryptograpy. After the key exchange, messages are send and received using symetric key cryptograpy. For each session, the server changes public key to avoid reusing the same key pair and prevent replay attacks. For more nonce randomness, the server provides the first nonce for the client in the key exchange when the session opens. The server identity is verified in the client, the trust is established on first use (TOFU). The client can have identity keys which are verified by the server when provided during the key exchange. The server should store the client public identity key and link it to a user.


I created `sel.c` to handle the various keys, there are functions that don't take keys as parameters, they use the keys stored in `sel.c` buffers.


To implement something like SNI (send hostname during key exchange) for virtual hosting (use a server identity key depending on the hostname), the encrypted connection is established and then the hostname and server identity key are transfered, the key exchange has to be changed:


client opens connection and sends public session key

server responds with public session key and nonce

client sends encrypted public identity key, hostname it is connecting to and signature (to prove its identity)

server responds with encrypted public identity key linked to requested hostname and signature (to prove the server identity)

client sends application request


The encryption of the hostname adds a round trip so it takes more time to established the encrypted connection for the application. I think it is still reasonable most of the time.


The proposed TLS ECH (ecrypted client hello) tries to remove the additional round trip in most cases.

https://blog.cloudflare.com/encrypted-client-hello/

https://blog.cloudflare.com/encrypted-client-hello/


https://www.ietf.org/archive/id/draft-ietf-tls-esni-08.html

https://www.ietf.org/archive/id/draft-ietf-tls-esni-08.html


the public key is distributed by DoH (DNS over HTTPS), the key can be cached

when the key is correct, there is no additional round trip, and when the key is incorrect, the client facing server provides the correct public key


To remove the additional round trip, I have been thinking about keeping the same server session keys for all sessions. If the client is allowed to reuse session keys then the server has to force the client to use the nonces only once. This is not feasible because it takes too much resources. So the server should prevent the client from reusing session keys. The server could keep track of the client public session keys in a bloomfilter, it is ok when there are not too many sessions (under 100 million sessions) but when there are many sessions over the lifetime of the server session key, the bit set in the bloom filter becomes too large. The server session key can be changed regurlarly after a certain amount of sessions to reset the bloom filter. For 45 million sessions and a 10 percent chance of false positive, the bit set in the bloom filter is 25MB.


`client3.c` and `server3.c` also implement a request-response system which supports anonymous clients. The server keeps the same public key and forces the client to change key to avoid reusing the same key pair and prevent replay attacks. `server3.c` uses a bloom filter to keep track of the client public keys (it is the most space efficient).


`client3.c` is like `client2.c` but it keeps the same client session key for all sessions. `server3.c` is like `server2.c` with a check to verify that client session keys are not reused, the server detects `client3.c` after the first session.


The key exchange is unnecessary when the server public key already known and the client can use its public key and send the first encrypted (with the symetric key algorithms) request directly.


when the client key is already in the filter, the server requests a new client session key (not implemented)

when the server can't decrypt the message, the server sends its public session key to the client (not implemented)


This solution is not scalable because the bloom filter has to be shared with all processes and machines.


If there are only trusted clients and servers, the server public session key can be stored in the client or the clients store their public keys in the server (like ssh), then there is no need to do the key exchange and the clients can send their public key and the first encrypted (with the symetric key algorithms) request directly.


`client4.c` generates keys and the public key is shared with `server4.c`. When `client4.c` connects the first time to `server4.c`, it requests the server public key. On the second session, `client4.c` and send its public key and the first encrypted message directly without requesting the server public key.


Replacing TLS with libsodium in gmni (gemini client) and gmnisrv (gemini server)


First clone and build gmni and gmnisrv as described in my other blog article:

building gmni and gmnisrv


I created prototypes gmni and gmnisrv in which I replaced TLS with the algorithms from `client2.c` and `server2.c`. The TLS code is still there but it is not used.


The communication is encrypted and if a flaw is discovered, the algorithms will have to be changed. If this system is used by many servers and clients, they will not all change to the new version simultaneously so both algorithms have to be supported for some period and we end up with a library similar to the TLS libraries with many crytographic algorithms and a more complex handskake.


In my change below:


gmnisrv supports one client at a time (it uses static buffers in `sel.c`).

there are no buffer length checks

the server response has a limited size

the server identity changes after each start

hostname is hard coded to `localhost` in gmnisrv

the packet length ints are not converted to network byte order with htonl()

I haven't tried to catch all possible errors


To run the prototypes:


build gmni and gmnisrv

apply the patches below

copy config.mk to .build/config.mk

build gmni and gmnisrv

start gmnisrv

with gmni, connect to the server


In `.build/config.mk` for gmni, I added libsodium and `sel.c`:


LIBS= /usr/local/lib/libsodium.a -lbearssl
...
src/sel.o: src/sel.c
src/tofu.o: src/tofu.c
src/url.o: src/url.c
src/util.o: src/util.c
gmni_objects=\
        src/certs.o \
        src/client.o \
        src/escape.o \
        src/gmni.o \
        src/sel.o \
        src/tofu.o \
        src/url.o \
        src/util.o
# End generated rules for gmni
# Begin generated rules for gmnlm
src/certs.o: src/certs.c
src/client.o: src/client.c
src/escape.o: src/escape.c
src/gmnlm.o: src/gmnlm.c
src/parser.o: src/parser.c
src/sel.o: src/sel.c
src/tofu.o: src/tofu.c
src/url.o: src/url.c
src/util.o: src/util.c
gmnlm_objects=\
        src/certs.o \
        src/client.o \
        src/escape.o \
        src/gmnlm.o \
        src/parser.o \
        src/sel.o \
        src/tofu.o \
        src/url.o \
        src/util.o
# End generated rules for gmnlm
# Begin generated rules for libgmni.a
src/certs.o: src/certs.c
src/client.o: src/client.c
src/escape.o: src/escape.c
src/tofu.o: src/tofu.c
src/sel.o: src/sel.c
src/url.o: src/url.c
src/util.o: src/util.c
src/parser.o: src/parser.c
libgmni.a_objects=\
        src/certs.o \
        src/client.o \
        src/escape.o \
        src/sel.o \

In `.build/config.mk` for gmnisvr, I also added libsodium and `sel.c`:


LIBS= /usr/local/lib/libsodium.a -L/usr/local/lib -lssl -L/usr/local/lib -lcrypto
...
src/sel.o: src/sel.c
src/serve.o: src/serve.c
src/server.o: src/server.c
src/tls.o: src/tls.c
src/url.o: src/url.c
src/util.o: src/util.c
gmnisrv_objects=\
        src/config.o \
        src/escape.o \
        src/ini.o \
        src/log.o \
        src/main.o \
        src/mime.o \
        src/regexp.o \
        src/sel.o \

Here is the gmni patch to apply on commit `e4d3984`:


From b285d0625fe7b37706a38a249ce41ceb65e8368e Mon Sep 17 00:00:00 2001
From: Remy Noulin <loader2x@gmail.com>
Date: Mon, 26 Sep 2022 12:04:05 +0200
Subject: [PATCH] replace tls with libsodium

config.mk          |  84 ++++++++++++++++++++++++++++
include/gmni/sel.h |  57 +++++++++++++++++++
src/client.c       |  88 +++++++++++++++++++++++++++--
src/gmnlm.c        |  14 ++++-
src/sel.c          | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 394 insertions(+), 7 deletions(-)
---
 config.mk          |  84 ++++++++++++++++++++++++
 include/gmni/sel.h |  57 ++++++++++++++++
 src/client.c       |  88 +++++++++++++++++++++++--
 src/gmnlm.c        |  14 +++-
 src/sel.c          | 158 +++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 394 insertions(+), 7 deletions(-)
 create mode 100644 config.mk
 create mode 100644 include/gmni/sel.h
 create mode 100644 src/sel.c

diff --git a/config.mk b/config.mk
new file mode 100644
index 0000000..4b57e8d
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,84 @@
+CC=cc
+SCDOC=scdoc
+LIBS= /usr/local/lib/libsodium.a -lbearssl
+PREFIX=/usr/local
+OUTDIR=.build
+_INSTDIR=$(DESTDIR)$(PREFIX)
+BINDIR?=$(_INSTDIR)/bin
+LIBDIR?=$(_INSTDIR)/lib
+INCLUDEDIR?=$(_INSTDIR)/include
+MANDIR?=$(_INSTDIR)/share/man
+CACHE=$(OUTDIR)/cache
+CFLAGS= \
+	-g \
+	-std=c11 \
+	-D_XOPEN_SOURCE=700 \
+	-Wall \
+	-Wextra \
+	-Werror \
+	-pedantic
+CFLAGS+=-Iinclude -I$(OUTDIR)
+CFLAGS+=-DPREFIX='"$(PREFIX)"'
+CFLAGS+=-DLIBDIR='"$(LIBDIR)"'
+
+all: gmni gmnlm libgmni.a libgmni.pc
+install_docs:
+# Begin generated rules for gmni
+src/certs.o: src/certs.c
+src/client.o: src/client.c
+src/escape.o: src/escape.c
+src/gmni.o: src/gmni.c
+src/sel.o: src/sel.c
+src/tofu.o: src/tofu.c
+src/url.o: src/url.c
+src/util.o: src/util.c
+gmni_objects=\
+	src/certs.o \
+	src/client.o \
+	src/escape.o \
+	src/gmni.o \
+	src/sel.o \
+	src/tofu.o \
+	src/url.o \
+	src/util.o
+# End generated rules for gmni
+# Begin generated rules for gmnlm
+src/certs.o: src/certs.c
+src/client.o: src/client.c
+src/escape.o: src/escape.c
+src/gmnlm.o: src/gmnlm.c
+src/parser.o: src/parser.c
+src/sel.o: src/sel.c
+src/tofu.o: src/tofu.c
+src/url.o: src/url.c
+src/util.o: src/util.c
+gmnlm_objects=\
+	src/certs.o \
+	src/client.o \
+	src/escape.o \
+	src/gmnlm.o \
+	src/parser.o \
+	src/sel.o \
+	src/tofu.o \
+	src/url.o \
+	src/util.o
+# End generated rules for gmnlm
+# Begin generated rules for libgmni.a
+src/certs.o: src/certs.c
+src/client.o: src/client.c
+src/escape.o: src/escape.c
+src/tofu.o: src/tofu.c
+src/sel.o: src/sel.c
+src/url.o: src/url.c
+src/util.o: src/util.c
+src/parser.o: src/parser.c
+libgmni.a_objects=\
+	src/certs.o \
+	src/client.o \
+	src/escape.o \
+	src/sel.o \
+	src/tofu.o \
+	src/url.o \
+	src/util.o \
+	src/parser.o
+# End generated rules for libgmni.a
diff --git a/include/gmni/sel.h b/include/gmni/sel.h
new file mode 100644
index 0000000..4ebf3b1
--- /dev/null
+++ b/include/gmni/sel.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "sodium.h"
+
+#ifndef u8
+#define u8  uint8_t
+#endif
+
+typedef struct {
+	u8 publicKey[crypto_box_PUBLICKEYBYTES];
+	u8 secretKey[crypto_box_SECRETKEYBYTES];
+	u8 remotePublicKey[crypto_box_PUBLICKEYBYTES];
+	u8 nonce[crypto_box_NONCEBYTES];
+} keyst;
+
+typedef struct {
+	u8 rx[crypto_kx_SESSIONKEYBYTES];
+	u8 tx[crypto_kx_SESSIONKEYBYTES];
+	u8 nonce[crypto_box_NONCEBYTES];
+} sessionKeyst;
+
+#define CLIENT_SESSION_KEYS 0
+#define SERVER_SESSION_KEYS 1
+
+typedef struct {
+	u8 publicKey[crypto_sign_PUBLICKEYBYTES];
+	u8 secretKey[crypto_sign_SECRETKEYBYTES];
+} signKeyst;
+
+extern signKeyst identityKeys;
+extern u8 remoteId[crypto_sign_PUBLICKEYBYTES];
+extern sessionKeyst sessionKeys;
+extern keyst keys;
+
+extern u8 selBufNet[1024*1024];
+extern u8 selBuf[1024*1024];
+
+extern char *bodyStart;
+
+ /*
+These functions return 0 when they fail.
+*/
+
+int selInit(void);
+void newKeys(void);
+void newKeysBuf(keyst *keys);
+void newSignKeys(void);
+void newSignKeysBuf(signKeyst *keys);
+int selPublicEncrypt(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen, keyst *keys);
+int selPublicDecrypt(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen, keyst *keys);
+int computeSharedKeys(int clientOrServer);
+int computeSharedKeysBuf(int clientOrServer, sessionKeyst *sessionKeys, keyst *clientKeys);
+// secret/symetric key encryption
+int selEncrypt(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen);
+int selEncryptBuf(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen, const u8 *nonce, const u8 *k);
+int selDecrypt(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen);
+int selDecryptBuf(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen, const u8 *nonce, const u8 *k);
diff --git a/src/client.c b/src/client.c
index 0044122..862c749 100644
--- a/src/client.c
+++ b/src/client.c
@@ -13,8 +13,11 @@
 #include <gmni/tofu.h>
 #include <gmni/url.h>

+#include <gmni/sel.h>
+
+
 static enum gemini_result
-gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options,
+gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options,
    struct gemini_response *resp, struct addrinfo **addr)
 {
    int port = 1965;
@@ -199,8 +202,85 @@ gemini_request(const char *url, struct gemini_options *options,
    r = snprintf(req, sizeof(req), "%s\r\n", url);
    assert(r > 0);

-	br_sslio_write_all(&resp->body, req, r);
-	br_sslio_flush(&resp->body);
+	// session public keys
+	newKeys();
+	unsigned char exchange[crypto_sign_BYTES + crypto_sign_PUBLICKEYBYTES + sizeof(keys.publicKey)] = {0};
+	memcpy(exchange+crypto_sign_BYTES+crypto_sign_PUBLICKEYBYTES, &keys.publicKey, sizeof(keys.publicKey));
+	sock_write(&resp->fd, exchange, sizeof(exchange));
+
+	unsigned char serverInfo[crypto_sign_BYTES + crypto_sign_PUBLICKEYBYTES + sizeof(keys.remotePublicKey) + crypto_box_NONCEBYTES] = {0};
+	sock_read(&resp->fd, serverInfo, sizeof(serverInfo));
+
+	// check remote server
+	unsigned char unsigned_message[crypto_sign_PUBLICKEYBYTES + sizeof(keys.remotePublicKey) + crypto_box_NONCEBYTES] = {0};
+	unsigned long long unsigned_message_len;
+	const unsigned char *idPublicKey = serverInfo + crypto_sign_BYTES;
+
+	if (crypto_sign_open(unsigned_message, &unsigned_message_len, serverInfo, sizeof(serverInfo), idPublicKey) != 0) {
+	  fprintf(stderr, "Incorrect signature!");
+	}
+
+	memcpy(keys.remotePublicKey, unsigned_message + crypto_sign_PUBLICKEYBYTES, sizeof(keys.remotePublicKey));
+	memcpy(sessionKeys.nonce, unsigned_message + crypto_sign_PUBLICKEYBYTES + sizeof(keys.remotePublicKey), crypto_box_NONCEBYTES);
+
+	// key exchange
+	if (!computeSharedKeys(CLIENT_SESSION_KEYS)) {
+	  fprintf(stderr, "Invalid server key");
+	}
+
+	// send encrypted message
+	// *nonce is incremented by after sending or receiving a message
+	// *nonce is allowed to wrap from the max value
+	uint64_t *nonce = (uint64_t*)sessionKeys.nonce;
+	unsigned char sb[16384] = {0};
+	int len = selEncrypt(sb, sizeof(sb), (const uint8_t *)req, r);
+	++*nonce;
+
+	sock_write(&resp->fd, (const unsigned char *)&len, sizeof(len));
+	sock_write(&resp->fd, sb, len);
+
+	r = sock_read(&resp->fd, (unsigned char *)&len, sizeof(len));
+	r = sock_read(&resp->fd, selBufNet, len);
+
+	r = selDecrypt(selBuf, sizeof(selBuf), selBufNet, len);
+	++*nonce;
+
+	if (!r) {
+	  fprintf(stderr, "failed to decrypt");
+	}
+
+	selBuf[r] = 0;
+
+	char *endptr;
+	resp->status = (enum gemini_status)strtol((const char *)selBuf, &endptr, 10);
+	if (*endptr != ' ' || resp->status < 10 || (int)resp->status >= 70) {
+		fprintf(stderr, "invalid status\n");
+		res = GEMINI_ERR_PROTOCOL;
+		goto cleanup;
+	}
+	//char *end = strstr((const char *)selBuf+3, "\r\n");
+	char *end = (char *)selBuf+r-2;
+	*end = 0;
+	resp->meta = strdup((const char *)selBuf+3);
+	*end = '\r';
+
+	bodyStart = end+2;
+
+	r = sock_read(&resp->fd, (unsigned char *)&len, sizeof(len));
+	r = sock_read(&resp->fd, selBufNet, len);
+
+	r = selDecrypt((uint8_t *)bodyStart, sizeof(selBuf)-1024, selBufNet, len);
+	++*nonce;
+
+	if (!r) {
+	  fprintf(stderr, "failed to decrypt");
+	}
+
+	selBuf[r] = 0;
+	goto cleanup;
+
+	/* br_sslio_write_all(&resp->body, req, r); */
+	/* br_sslio_flush(&resp->body); */

    // The SSL engine maintains an internal buffer, so this shouldn't be as
    // inefficient as it looks. It's necessary to do this one byte at a time
@@ -230,7 +310,7 @@ gemini_request(const char *url, struct gemini_options *options,
    	goto cleanup;
    }

-	char *endptr;
+	//char *endptr;
    resp->status = (enum gemini_status)strtol(buf, &endptr, 10);
    if (*endptr != ' ' || resp->status < 10 || (int)resp->status >= 70) {
    	fprintf(stderr, "invalid status\n");
diff --git a/src/gmnlm.c b/src/gmnlm.c
index b773b37..5ed9b10 100644
--- a/src/gmnlm.c
+++ b/src/gmnlm.c
@@ -23,6 +23,9 @@
 #include <unistd.h>
 #include "util.h"

+
+#include <gmni/sel.h>
+
 struct link {
    char *url;
    struct link *next;
@@ -920,6 +923,11 @@ resp_read(void *state, void *buf, size_t nbyte)
 {
    struct gemini_response *resp = state;
    if (resp->sc) {
+		if (!bodyStart) return 0;
+		int len = strlen(bodyStart);
+		strncpy(buf, bodyStart, nbyte);
+		bodyStart = NULL;
+		return len;
    	return br_sslio_read(&resp->body, buf, nbyte);
    } else {
    	return read(resp->fd, buf, nbyte);
@@ -1314,17 +1322,17 @@ main(int argc, char *argv[])
    	open_bookmarks(&browser);
    }

+	if (!selInit()) return 1;
    gemini_tofu_init(&browser.tofu, &tofu_callback, &browser);

+
    struct gemini_response resp;
    browser.running = true;
    while (browser.running) {
    	static char prompt[4096];
    	bool skip_prompt = do_requests(&browser, &resp) == GEMINI_OK
    		&& display_response(&browser, &resp);
-		if (browser.meta) {
-			free(browser.meta);
-		}
+		free(browser.meta);
    	browser.meta = resp.status == GEMINI_STATUS_SUCCESS
    		? strdup(resp.meta) : NULL;
    	gemini_response_finish(&resp);
diff --git a/src/sel.c b/src/sel.c
new file mode 100644
index 0000000..1c276ac
--- /dev/null
+++ b/src/sel.c
@@ -0,0 +1,158 @@
+#include <gmni/sel.h>
+
+// detect entropy quality
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <linux/random.h>
+
+#include <iso646.h> /* and or not defines */
+
+signKeyst identityKeys                  = {0};
+u8 remoteId[crypto_sign_PUBLICKEYBYTES] = {0};
+sessionKeyst sessionKeys                = {0};
+keyst keys                              = {0};
+
+u8 selBufNet[1024*1024];
+u8 selBuf[1024*1024];
+
+char *bodyStart;
+
+int selInit(void) {
+  // detect entropy quality
+  int urandomfd;
+  if ((urandomfd = open("/dev/urandom", O_RDONLY)) != -1) {
+    int c;
+    if (ioctl(urandomfd, RNDGETENTCNT, &c) == 0 && c < 160) {
+      /* logN("This system doesn't provide enough entropy to quickly generate high-quality random numbers.\n" */
+      /*     "Installing the rng-utils/rng-tools, jitterentropy or haveged packages may help.\n" */
+      /*     "On virtualized Linux environments, also consider using virtio-rng.\n" */
+      /*     "The service will not start until enough entropy has been collected.\n", stderr); */
+      close(urandomfd);
+      return 0;
+    }
+  }
+  close(urandomfd);
+  if (sodium_init() == -1) {
+    /* logC("Panic! libsodium couldn't be initialized; it is not safe to use"); */
+    return 0;
+  }
+  return 1;
+}
+
+void newKeys(void) {
+  crypto_box_keypair(keys.publicKey, keys.secretKey);
+}
+
+void newKeysBuf(keyst *keys) {
+  crypto_box_keypair(keys->publicKey, keys->secretKey);
+  /* logD("Public key"); */
+  /* loghex(keys->publicKey, sizeof(keys->publicKey)); */
+  /* put; */
+  /* logD("Secret key"); */
+  /* loghex(keys->secretKey, sizeof(keys->secretKey)); */
+  /* put; */
+}
+
+void newSignKeys(void) {
+	crypto_sign_keypair(identityKeys.publicKey, identityKeys.secretKey);
+}
+
+void newSignKeysBuf(signKeyst *keys) {
+	crypto_sign_keypair(keys->publicKey, keys->secretKey);
+}
+
+// return ciphertext (encrypted message) length
+int selPublicEncrypt(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen, keyst *keys) {
+  // csize is ciphertext buffer size
+  // check is there is enough space in ciphertext
+  if (csize < mlen + crypto_box_MACBYTES) return 0;
+  if (crypto_box_easy(ciphertext, msg, mlen, keys->nonce, keys->remotePublicKey, keys->secretKey) != 0) return 0;
+  return mlen + crypto_box_MACBYTES;
+}
+
+// return message length
+int selPublicDecrypt(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen, keyst *keys) {
+  // msize is message buffer size
+  // check ciphertext has minimal length, the message has to be at least one byte
+  // check is there is enough space in message buffer
+  if (clen <= crypto_box_MACBYTES or msize < clen - crypto_box_MACBYTES) return 0;
+  if (crypto_box_open_easy(msg, ciphertext, clen, keys->nonce, keys->remotePublicKey, keys->secretKey) != 0) return 0;
+  return clen - crypto_box_MACBYTES;
+}
+
+int computeSharedKeys(int clientOrServer) {
+  switch (clientOrServer) {
+    case CLIENT_SESSION_KEYS:
+      if (crypto_kx_client_session_keys(sessionKeys.rx, sessionKeys.tx, keys.publicKey, keys.secretKey, keys.remotePublicKey) != 0) {
+        // Suspicious server public key, bail out
+        return 0;
+      }
+      break;
+    case SERVER_SESSION_KEYS:
+      if (crypto_kx_server_session_keys(sessionKeys.rx, sessionKeys.tx, keys.publicKey, keys.secretKey, keys.remotePublicKey) != 0) {
+        // Suspicious server public key, bail out
+        return 0;
+      }
+      break;
+    default:
+      return 0;
+  }
+  return 1;
+}
+
+int computeSharedKeysBuf(int clientOrServer, sessionKeyst *sessionKeys, keyst *keys) {
+  switch (clientOrServer) {
+    case CLIENT_SESSION_KEYS:
+      if (crypto_kx_client_session_keys(sessionKeys->rx, sessionKeys->tx, keys->publicKey, keys->secretKey, keys->remotePublicKey) != 0) {
+        // Suspicious server public key, bail out
+        return 0;
+      }
+      break;
+    case SERVER_SESSION_KEYS:
+      if (crypto_kx_server_session_keys(sessionKeys->rx, sessionKeys->tx, keys->publicKey, keys->secretKey, keys->remotePublicKey) != 0) {
+        // Suspicious server public key, bail out
+        return 0;
+      }
+      break;
+    default:
+      return 0;
+  }
+  return 1;
+}
+
+int selEncrypt(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen) {
+  // csize is ciphertext buffer size
+  // check is there is enough space in ciphertext
+  if (csize < mlen + crypto_secretbox_MACBYTES) return 0;
+  if (crypto_secretbox_easy(ciphertext, msg, mlen, sessionKeys.nonce, sessionKeys.tx) != 0) return 0;
+  return mlen + crypto_secretbox_MACBYTES;
+}
+
+int selEncryptBuf(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen, const u8 *nonce, const u8 *k) {
+  // csize is ciphertext buffer size
+  // check is there is enough space in ciphertext
+  if (csize < mlen + crypto_secretbox_MACBYTES) return 0;
+  if (crypto_secretbox_easy(ciphertext, msg, mlen, nonce, k) != 0) return 0;
+  return mlen + crypto_secretbox_MACBYTES;
+}
+
+int selDecrypt(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen) {
+  // msize is message buffer size
+  // check ciphertext has minimal length, the message has to be at least one byte
+  // check is there is enough space in message buffer
+  if (clen <= crypto_secretbox_MACBYTES or msize < clen - crypto_secretbox_MACBYTES) return 0;
+	if (crypto_secretbox_open_easy(msg, ciphertext, clen, sessionKeys.nonce, sessionKeys.rx) != 0) return 0;
+  return clen - crypto_secretbox_MACBYTES;
+}
+
+int selDecryptBuf(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen, const u8 *nonce, const u8 *k) {
+  // msize is message buffer size
+  // check ciphertext has minimal length, the message has to be at least one byte
+  // check is there is enough space in message buffer
+  if (clen <= crypto_secretbox_MACBYTES or msize < clen - crypto_secretbox_MACBYTES) return 0;
+	if (crypto_secretbox_open_easy(msg, ciphertext, clen, nonce, k) != 0) return 0;
+  return clen - crypto_secretbox_MACBYTES;
+}
+
+// vim: set expandtab ts=2 sw=2:
--
2.35.1

gmnisrv patch to apply on commit `132f2ec`:


From 257b12b13ab0f047bfbcb7cf98b4f85f6dd4c71d Mon Sep 17 00:00:00 2001
From: Remy Noulin <loader2x@gmail.com>
Date: Mon, 26 Sep 2022 12:10:36 +0200
Subject: [PATCH] replace tls with libsodium

---
 config.mk    |  59 ++++++++++++++++++++
 src/main.c   |   5 ++
 src/sel.c    | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/sel.h    |  51 +++++++++++++++++
 src/server.c | 123 +++++++++++++++++++++++++++++++++++++++--
 5 files changed, 386 insertions(+), 5 deletions(-)
 create mode 100644 config.mk
 create mode 100644 src/sel.c
 create mode 100644 src/sel.h

diff --git a/config.mk b/config.mk
new file mode 100644
index 0000000..b6ca0c3
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,59 @@
+CC=cc
+SCDOC=scdoc
+LIBS= /usr/local/lib/libsodium.a -L/usr/local/lib -lssl -L/usr/local/lib -lcrypto
+PREFIX=/usr/local
+OUTDIR=.build
+SRCDIR=.
+BINDIR?=$(PREFIX)/bin
+SHAREDIR?=$(PREFIX)/share
+SYSCONFDIR?=$(PREFIX)/etc
+LIBDIR?=$(PREFIX)/lib
+MANDIR?=$(PREFIX)/share/man
+VARLIBDIR?=$(PREFIX)/var/lib
+MIMEDB?=/etc/mime.types
+CACHE=$(OUTDIR)/cache
+CFLAGS= \
+	-g \
+	-std=c11 \
+	-D_XOPEN_SOURCE=700 \
+	-Wall \
+	-Wextra \
+	-Werror \
+	-pedantic -I/usr/local/include -I/usr/local/include
+CFLAGS+=-Iinclude -I$(OUTDIR)
+CFLAGS+=-DPREFIX='"$(PREFIX)"'
+CFLAGS+=-DLIBDIR='"$(LIBDIR)"'
+CFLAGS+=-DVARLIBDIR='"$(VARLIBDIR)"'
+CFLAGS+=-DSYSCONFDIR='"$(SYSCONFDIR)"'
+CFLAGS+=-DMIMEDB='"$(MIMEDB)"'
+
+all: gmnisrv
+# Begin generated rules for gmnisrv
+src/config.o: src/config.c
+src/escape.o: src/escape.c
+src/ini.o: src/ini.c
+src/log.o: src/log.c
+src/main.o: src/main.c
+src/mime.o: src/mime.c
+src/regexp.o: src/regexp.c
+src/sel.o: src/sel.c
+src/serve.o: src/serve.c
+src/server.o: src/server.c
+src/tls.o: src/tls.c
+src/url.o: src/url.c
+src/util.o: src/util.c
+gmnisrv_objects=\
+	src/config.o \
+	src/escape.o \
+	src/ini.o \
+	src/log.o \
+	src/main.o \
+	src/mime.o \
+	src/regexp.o \
+	src/sel.o \
+	src/serve.o \
+	src/server.o \
+	src/tls.o \
+	src/url.o \
+	src/util.o
+# End generated rules for gmnisrv
diff --git a/src/main.c b/src/main.c
index abc80ff..c985f8e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -6,6 +6,8 @@
 #include "server.h"
 #include "tls.h"

+#include "sel.h"
+
 static void
 usage(const char *argv_0)
 {
@@ -46,6 +48,9 @@ main(int argc, char **argv)
    	goto exit;
    }

+	if (!selInit()) return 1;
+	// generate id keys
+	newSignKeys();
    r = tls_init(&conf);
    if (r != 0) {
    	server_error("TLS initialization failed");
diff --git a/src/sel.c b/src/sel.c
new file mode 100644
index 0000000..c51defc
--- /dev/null
+++ b/src/sel.c
@@ -0,0 +1,153 @@
+#include "sel.h"
+
+// detect entropy quality
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <linux/random.h>
+
+#include <iso646.h> /* and or not defines */
+
+signKeyst identityKeys                  = {0};
+u8 remoteId[crypto_sign_PUBLICKEYBYTES] = {0};
+sessionKeyst sessionKeys                = {0};
+keyst keys                              = {0};
+
+int selInit(void) {
+  // detect entropy quality
+  int urandomfd;
+  if ((urandomfd = open("/dev/urandom", O_RDONLY)) != -1) {
+    int c;
+    if (ioctl(urandomfd, RNDGETENTCNT, &c) == 0 && c < 160) {
+      /* logN("This system doesn't provide enough entropy to quickly generate high-quality random numbers.\n" */
+      /*     "Installing the rng-utils/rng-tools, jitterentropy or haveged packages may help.\n" */
+      /*     "On virtualized Linux environments, also consider using virtio-rng.\n" */
+      /*     "The service will not start until enough entropy has been collected.\n", stderr); */
+      close(urandomfd);
+      return 0;
+    }
+  }
+  close(urandomfd);
+  if (sodium_init() == -1) {
+    /* logC("Panic! libsodium couldn't be initialized; it is not safe to use"); */
+    return 0;
+  }
+  return 1;
+}
+
+void newKeys(void) {
+  crypto_box_keypair(keys.publicKey, keys.secretKey);
+}
+
+void newKeysBuf(keyst *keys) {
+  crypto_box_keypair(keys->publicKey, keys->secretKey);
+  /* logD("Public key"); */
+  /* loghex(keys->publicKey, sizeof(keys->publicKey)); */
+  /* put; */
+  /* logD("Secret key"); */
+  /* loghex(keys->secretKey, sizeof(keys->secretKey)); */
+  /* put; */
+}
+
+void newSignKeys(void) {
+	crypto_sign_keypair(identityKeys.publicKey, identityKeys.secretKey);
+}
+
+void newSignKeysBuf(signKeyst *keys) {
+	crypto_sign_keypair(keys->publicKey, keys->secretKey);
+}
+
+// return ciphertext (encrypted message) length
+int selPublicEncrypt(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen, keyst *keys) {
+  // csize is ciphertext buffer size
+  // check is there is enough space in ciphertext
+  if (csize < mlen + crypto_box_MACBYTES) return 0;
+  if (crypto_box_easy(ciphertext, msg, mlen, keys->nonce, keys->remotePublicKey, keys->secretKey) != 0) return 0;
+  return mlen + crypto_box_MACBYTES;
+}
+
+// return message length
+int selPublicDecrypt(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen, keyst *keys) {
+  // msize is message buffer size
+  // check ciphertext has minimal length, the message has to be at least one byte
+  // check is there is enough space in message buffer
+  if (clen <= crypto_box_MACBYTES or msize < clen - crypto_box_MACBYTES) return 0;
+  if (crypto_box_open_easy(msg, ciphertext, clen, keys->nonce, keys->remotePublicKey, keys->secretKey) != 0) return 0;
+  return clen - crypto_box_MACBYTES;
+}
+
+int computeSharedKeys(int clientOrServer) {
+  switch (clientOrServer) {
+    case CLIENT_SESSION_KEYS:
+      if (crypto_kx_client_session_keys(sessionKeys.rx, sessionKeys.tx, keys.publicKey, keys.secretKey, keys.remotePublicKey) != 0) {
+        // Suspicious server public key, bail out
+        return 0;
+      }
+      break;
+    case SERVER_SESSION_KEYS:
+      if (crypto_kx_server_session_keys(sessionKeys.rx, sessionKeys.tx, keys.publicKey, keys.secretKey, keys.remotePublicKey) != 0) {
+        // Suspicious server public key, bail out
+        return 0;
+      }
+      break;
+    default:
+      return 0;
+  }
+  return 1;
+}
+
+int computeSharedKeysBuf(int clientOrServer, sessionKeyst *sessionKeys, keyst *keys) {
+  switch (clientOrServer) {
+    case CLIENT_SESSION_KEYS:
+      if (crypto_kx_client_session_keys(sessionKeys->rx, sessionKeys->tx, keys->publicKey, keys->secretKey, keys->remotePublicKey) != 0) {
+        // Suspicious server public key, bail out
+        return 0;
+      }
+      break;
+    case SERVER_SESSION_KEYS:
+      if (crypto_kx_server_session_keys(sessionKeys->rx, sessionKeys->tx, keys->publicKey, keys->secretKey, keys->remotePublicKey) != 0) {
+        // Suspicious server public key, bail out
+        return 0;
+      }
+      break;
+    default:
+      return 0;
+  }
+  return 1;
+}
+
+int selEncrypt(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen) {
+  // csize is ciphertext buffer size
+  // check is there is enough space in ciphertext
+  if (csize < mlen + crypto_secretbox_MACBYTES) return 0;
+  if (crypto_secretbox_easy(ciphertext, msg, mlen, sessionKeys.nonce, sessionKeys.tx) != 0) return 0;
+  return mlen + crypto_secretbox_MACBYTES;
+}
+
+int selEncryptBuf(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen, const u8 *nonce, const u8 *k) {
+  // csize is ciphertext buffer size
+  // check is there is enough space in ciphertext
+  if (csize < mlen + crypto_secretbox_MACBYTES) return 0;
+  if (crypto_secretbox_easy(ciphertext, msg, mlen, nonce, k) != 0) return 0;
+  return mlen + crypto_secretbox_MACBYTES;
+}
+
+int selDecrypt(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen) {
+  // msize is message buffer size
+  // check ciphertext has minimal length, the message has to be at least one byte
+  // check is there is enough space in message buffer
+  if (clen <= crypto_secretbox_MACBYTES or msize < clen - crypto_secretbox_MACBYTES) return 0;
+	if (crypto_secretbox_open_easy(msg, ciphertext, clen, sessionKeys.nonce, sessionKeys.rx) != 0) return 0;
+  return clen - crypto_secretbox_MACBYTES;
+}
+
+int selDecryptBuf(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen, const u8 *nonce, const u8 *k) {
+  // msize is message buffer size
+  // check ciphertext has minimal length, the message has to be at least one byte
+  // check is there is enough space in message buffer
+  if (clen <= crypto_secretbox_MACBYTES or msize < clen - crypto_secretbox_MACBYTES) return 0;
+	if (crypto_secretbox_open_easy(msg, ciphertext, clen, nonce, k) != 0) return 0;
+  return clen - crypto_secretbox_MACBYTES;
+}
+
+// vim: set expandtab ts=2 sw=2:
diff --git a/src/sel.h b/src/sel.h
new file mode 100644
index 0000000..7b830fe
--- /dev/null
+++ b/src/sel.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "sodium.h"
+
+#ifndef u8
+#define u8  uint8_t
+#endif
+
+typedef struct {
+	u8 publicKey[crypto_box_PUBLICKEYBYTES];
+	u8 secretKey[crypto_box_SECRETKEYBYTES];
+	u8 remotePublicKey[crypto_box_PUBLICKEYBYTES];
+	u8 nonce[crypto_box_NONCEBYTES];
+} keyst;
+
+typedef struct {
+	u8 rx[crypto_kx_SESSIONKEYBYTES];
+	u8 tx[crypto_kx_SESSIONKEYBYTES];
+	u8 nonce[crypto_box_NONCEBYTES];
+} sessionKeyst;
+
+#define CLIENT_SESSION_KEYS 0
+#define SERVER_SESSION_KEYS 1
+
+typedef struct {
+	u8 publicKey[crypto_sign_PUBLICKEYBYTES];
+	u8 secretKey[crypto_sign_SECRETKEYBYTES];
+} signKeyst;
+
+extern signKeyst identityKeys;
+extern u8 remoteId[crypto_sign_PUBLICKEYBYTES];
+extern sessionKeyst sessionKeys;
+extern keyst keys;
+/*
+These functions return 0 when they fail.
+*/
+
+int selInit(void);
+void newKeys(void);
+void newKeysBuf(keyst *keys);
+void newSignKeys(void);
+void newSignKeysBuf(signKeyst *keys);
+int selPublicEncrypt(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen, keyst *keys);
+int selPublicDecrypt(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen, keyst *keys);
+int computeSharedKeys(int clientOrServer);
+int computeSharedKeysBuf(int clientOrServer, sessionKeyst *sessionKeys, keyst *clientKeys);
+// secret/symetric key encryption
+int selEncrypt(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen);
+int selEncryptBuf(u8 *ciphertext/*result*/, size_t csize, const u8 *msg, size_t mlen, const u8 *nonce, const u8 *k);
+int selDecrypt(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen);
+int selDecryptBuf(u8 *msg/*result*/, size_t msize, const u8 *ciphertext, size_t clen, const u8 *nonce, const u8 *k);
diff --git a/src/server.c b/src/server.c
index 56e11c9..7d13ef0 100644
--- a/src/server.c
+++ b/src/server.c
@@ -17,6 +17,8 @@
 #include "server.h"
 #include "tls.h"

+#include "sel.h"
+
 int
 server_init(struct gmnisrv_server *server, struct gmnisrv_config *conf)
 {
@@ -231,6 +233,10 @@ client_init_ssl(struct gmnisrv_server *server, struct gmnisrv_client *client)

    SSL_set_accept_state(client->ssl);
    SSL_set_bio(client->ssl, client->rbio, client->wbio);
+
+	// public key for session
+	newKeys();
+	randombytes_buf(sessionKeys.nonce, sizeof(sessionKeys.nonce));
    return 0;
 }

@@ -242,17 +248,65 @@ enum connection_state {
 static enum connection_state
 client_readable(struct gmnisrv_server *server, struct gmnisrv_client *client)
 {
-	if (!client->ssl && client_init_ssl(server, client) != 0) {
-		return DISCONNECTED;
+	if (!client->ssl) {
+		if (client_init_ssl(server, client) != 0) {
+			return DISCONNECTED;
+		}
+		unsigned char clientInfo[crypto_sign_BYTES + crypto_sign_PUBLICKEYBYTES + sizeof(keys.remotePublicKey)] = {0};
+		ssize_t n = recv(client->sockfd, clientInfo, sizeof(clientInfo), MSG_WAITALL);
+		if (n <= 0) {
+			disconnect_client(server, client);
+			return DISCONNECTED;
+		}
+		memcpy(keys.remotePublicKey, clientInfo + crypto_sign_BYTES + crypto_sign_PUBLICKEYBYTES, sizeof(keys.remotePublicKey));
+		client->state = CLIENT_STATE_SSL;
+		client->next = CLIENT_STATE_REQUEST;
+		struct gmnisrv_host *host = gmnisrv_config_get_host(
+			server->conf, "localhost");
+		if (!host) {
+			disconnect_client(server, client);
+			return DISCONNECTED;
+		}
+		client->host = host;
+		client->pollfd->events = POLLOUT;
+		return CONNECTED;
    }

    char buf[BUFSIZ];
-	ssize_t n = read(client->sockfd, buf, sizeof(buf));
+	int len;
+	ssize_t n = read(client->sockfd, &len, sizeof(len));
+	n = read(client->sockfd, buf, len);
    if (n <= 0) {
    	disconnect_client(server, client);
    	return DISCONNECTED;
    }

+	uint64_t *nonce = (uint64_t*)sessionKeys.nonce;
+	n = selDecrypt((uint8_t*)client->buf, sizeof(client->buf), (const uint8_t*)buf, len);
+	++*nonce;
+	if (n == 0) {
+		disconnect_client(server, client);
+		return DISCONNECTED;
+	}
+	client->bufln = n;
+	client->buf[client->bufln] = '\0';
+	char *newline = strstr(client->buf, "\r\n");
+	if (!newline) {
+		const char *error = "Protocol error: malformed request";
+		client_submit_response(client,
+				GEMINI_STATUS_BAD_REQUEST, error, NULL);
+		return CONNECTED;
+	}
+	*newline = 0;
+
+	if (!request_validate(client, &client->path)) {
+		return CONNECTED;
+	}
+
+	serve_request(client);
+	return CONNECTED;
+
+
    size_t w = 0;
    while (w < (size_t)n) {
    	int r = BIO_write(client->rbio, &buf[w], n - w);
@@ -339,7 +393,7 @@ client_readable(struct gmnisrv_server *server, struct gmnisrv_client *client)

    client->buf[client->bufln] = '\0';

-	char *newline = strstr(client->buf, "\r\n");
+	/* char *newline = strstr(client->buf, "\r\n"); */
    if (!newline) {
    	const char *error = "Protocol error: malformed request";
    	switch (e) {
@@ -399,7 +453,34 @@ client_writable(struct gmnisrv_server *server, struct gmnisrv_client *client)
    switch (client->state) {
    case CLIENT_STATE_REQUEST:
    	assert(0); // Invariant
-	case CLIENT_STATE_SSL:
+	case CLIENT_STATE_SSL:;
+		// send public key
+		unsigned char exchange[crypto_sign_PUBLICKEYBYTES + sizeof(keys.publicKey) + crypto_box_NONCEBYTES] = {0};
+		unsigned char signed_message[crypto_sign_BYTES + sizeof(exchange)] = {0};
+		unsigned long long signed_message_len = 0;
+		memcpy(exchange, identityKeys.publicKey, crypto_sign_PUBLICKEYBYTES);
+		memcpy(exchange + crypto_sign_PUBLICKEYBYTES, keys.publicKey, sizeof(keys.publicKey));
+		memcpy(exchange + crypto_sign_PUBLICKEYBYTES + sizeof(keys.publicKey), sessionKeys.nonce, sizeof(sessionKeys.nonce));
+		crypto_sign(signed_message, &signed_message_len, exchange, sizeof(exchange), identityKeys.secretKey);
+		n = write(client->sockfd, signed_message, sizeof(signed_message));
+		if (n <= 0) {
+			client_log(&client->addr, "write error: %s",
+					strerror(errno));
+			disconnect_client(server, client);
+			return DISCONNECTED;
+		}
+		// key exchange
+		if (!computeSharedKeys(SERVER_SESSION_KEYS)) {
+			client_log(&client->addr, "write error: %s",
+					strerror(errno));
+			disconnect_client(server, client);
+			return DISCONNECTED;
+		}
+		client->state = client->next;
+		if (client->state == CLIENT_STATE_REQUEST) {
+			client->pollfd->events = POLLIN;
+		}
+		return CONNECTED;
    	assert(client->bufln > 0);
    	n = write(client->sockfd, client->buf, client->bufln);
    	if (n <= 0) {
@@ -449,6 +530,38 @@ client_writable(struct gmnisrv_server *server, struct gmnisrv_client *client)
    	break;
    }

+	r = selEncrypt((uint8_t *)buf, sizeof(buf), (const uint8_t *)client->buf, client->bufln);
+	uint64_t *nonce = (uint64_t*)sessionKeys.nonce;
+	++*nonce;
+	write(client->sockfd, &r, sizeof(r));
+	r = write(client->sockfd, buf, r);
+	if (r < 0) {
+		client_error(&client->addr,
+			"client write: %s",
+			strerror(errno));
+		disconnect_client(server, client);
+		return DISCONNECTED;
+	}
+
+	switch (client->state) {
+	case CLIENT_STATE_REQUEST:
+	case CLIENT_STATE_SSL:
+		assert(0); // Invariant
+	case CLIENT_STATE_HEADER:
+		if (!client->body) {
+			disconnect_client(server, client);
+			return DISCONNECTED;
+		} else {
+			client->state = CLIENT_STATE_BODY;
+			client->bufix = client->bufln = 0;
+			return CONNECTED;
+		}
+		break;
+	case CLIENT_STATE_BODY:
+		break;
+	}
+
+	return CONNECTED;
    r = SSL_write(client->ssl, &client->buf[client->bufix],
    		client->bufln - client->bufix);
    if (r <= 0) {
--
2.35.1

Tags: #cryptography #libsodium #nacl #tls


Feed

-- Response ended

-- Page fetched on Tue May 21 11:30:46 2024