aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2020-06-21 22:32:27 +0200
committerTom Smeding <tom.smeding@gmail.com>2020-06-25 17:22:19 +0200
commitbbb8bc475593b9ff481ec214c4391fe6aff854f4 (patch)
tree4452579755867361fc5e6b7b395787f57e9fa2c7
parent46feba9a7b1e4ed023dcdc3c505b67f3eb8348f0 (diff)
WIP secure transport using libsodium secretstreamlibsodium-transport
-rw-r--r--.gitignore2
-rw-r--r--protocol.md9
-rw-r--r--protocol_transport.md128
-rw-r--r--transport.c99
-rw-r--r--transport.h38
5 files changed, 273 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore
index db08d13..af53483 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@ db.db
*.so
firebaseServiceAccountKey.json
compile_commands.json
+protocol.html
+protocol_transport.html
diff --git a/protocol.md b/protocol.md
index 4094b79..a5a2df4 100644
--- a/protocol.md
+++ b/protocol.md
@@ -1,8 +1,11 @@
# tomsg protocol
-The underlying transport of the protocol is a plain TCP socket. The individual
-messages are all line-based; this means that a single message, both
-client->server and server->client, always ends with a newline (ASCII 10)
+The underlying transport of the protocol is an encrypted transport using
+libsodium, based on a plain TCP socket. This encrypted transport is defined in
+`protocol_transport.md`.
+
+The individual messages are all line-based; this means that a single message,
+both client->server and server->client, always ends with a newline (ASCII 10)
character.
Preliminary definitions:
diff --git a/protocol_transport.md b/protocol_transport.md
new file mode 100644
index 0000000..7915e29
--- /dev/null
+++ b/protocol_transport.md
@@ -0,0 +1,128 @@
+# tomsg transport protocol
+
+This is the specification for the lower-level transport protocol that underlies
+the application-level protocol described in `protocol.md`. This transport is
+used for communication between a client and the server.
+
+If at any point one of the parties breaks the protocol, the socket and transport
+should be closed.
+
+## Size assumptions
+
+The protocol makes a few assumptions about lengths of certain values in the
+libsodium API. An implementation of the protocol should verify that these
+assumptions still hold when the application is finally run.
+
+- `crypto_secretstream_xchacha20poly1305_KEYBYTES` = 32
+- `crypto_secretstream_xchacha20poly1305_HEADERBYTES` = 24
+- `crypto_kx_SESSIONKEYBYTES` >= 32
+- `crypto_kx_PUBLICKEYBYTES` = 32
+- `crypto_kx_SECRETKEYBYTES` = 32
+
+## Message types
+
+- ClientHello:
+ - 16 bytes magic: `tomsg v01 client`
+ - 32 bytes client public key
+- ServerHello:
+ - 16 bytes magic: `tomsg v01 server`
+ - 32 bytes server public key
+ - 24 bytes server->client libsodium secretstream header
+- ClientHello2:
+ - 8 bytes magic: `cheader0`
+ - 24 bytes client->server libsodium secretstream header
+- DataMessage:
+ - 8 bytes magic: `datamsg0`
+ - 8 bytes (unsigned little-endian) message length = N
+ - N bytes data
+
+## Initialisation sequence
+
+### Client initialisation sequence
+
+The following handshake is performed in order to set up two libsodium
+secretstreams, one for client->server communication (a push stream) and one for
+server->client communication (a pull stream).
+
+1. Generate a keypair using `crypto_kx_keypair`. These keys are referred to as
+ the client public and secret keys.
+2. Send a ClientHello message.
+3. Receive a ServerHello message.
+ - Check that the magic is correct for this version of the transport protocol.
+ - Use `crypto_kx_client_session_keys` to compute the client->server and
+ server->client encryption keys.
+ - Use `crypto_secretstream_xchacha20poly1305_init_push` to create the
+ client->server libsodium push secretstream using the client->server
+ encryption key.
+ - Use `crypto_secretstream_xchacha20poly1305_init_pull` to create the
+ server->client libsodium pull secretstream using the server->client
+ encryption key and header.
+4. Send a ClientHello2 message.
+
+### Server initialisation sequence
+
+The following handshake is performed in order to set up two libsodium
+secretstreams, one for server->client communication (a push stream) and one for
+client->server communication (a pull stream).
+
+1. Generate a keypair using `crypto_kx_keypair`. These keys are referred to as
+ the server public and secret keys.
+2. Receive a ClientHello message.
+ - Check that the magic is correct for this version of the transport protocol.
+ - Use `crypto_kx_server_session_keys` to compute the client->server and
+ server->client symmetric keys.
+ - Use `crypto_secretstream_xchacha20poly1305_init_push` to create the
+ server->client libsodium push secretstream using the server->client
+ encryption key.
+3. Send a ServerHello message.
+4. Receive a ClientHello2 message.
+ - Check that the magic is correct for this version of the transport protocol.
+ - Use `crypto_secretstream_xchacha20poly1305_init_pull` to create the
+ client->server libsodium pull secretstream using the client->server
+ encryption key and header.
+
+## Data exchange
+
+At this point, we can reconcile the two halves of the protocol: both parties now
+have a push secretstream for transmission to the other party, and a pull
+secretstream for reception from the other party.
+
+### Receiving a message
+
+At all times, the current party can receive a DataMessage message. To handle the
+message, use `crypto_secretstream_xchacha20poly1305_pull` to decrypt the
+encrypted data in the message, and observe the tag.
+- If the tag is 0:
+ - Return the decrypted data to the application.
+- If the tag is `crypto_secretstream_xchacha20poly1305_TAG_FINAL`:
+ - Close the socket and consider the transport closed. Report this to the
+ application.
+
+### Sending a message
+
+When the current party wants to send a message, use
+`crypto_secretstream_xchacha20poly1305_push`, with no additional data and
+`tag` = 0, to encrypt the message in the push secretstream. Then send the
+encrypted result in a DataMessage message to the other party.
+
+### Ending the transport
+
+When the current party wants to end the transport and close the socket, one has
+a choice between a graceful shutdown and an improper shutdown (just closing the
+socket and forgetting about the connection). Sometimes an improper shutdown
+cannot be avoided, e.g. if the application is killed by the operating system,
+but a graceful shutdown is always preferred.
+
+To perform a graceful shutdown, use `crypto_secretstream_xchacha20poly1305_push`
+on the push secretstream to encrypt the empty string with no additional data and
+`tag` = `crypto_secretstream_xchacha20poly1305_TAG_FINAL`. Then send the
+encrypted result in a DataMessage message to the other party.
+
+After the DataMessage message is successfully sent, the socket can be closed and
+the transport considered ended.
+
+
+# SSH notes
+
+- Use libssh, not libssh2
+- `ssh_message.type` has type `enum ssh_requests_e`, not `int`
diff --git a/transport.c b/transport.c
new file mode 100644
index 0000000..fea3e7e
--- /dev/null
+++ b/transport.c
@@ -0,0 +1,99 @@
+#include <stdio.h>
+#include <string.h>
+#include <sodium.h>
+#include "transport.h"
+#include "net.h"
+
+#define STREAM_KEY_SIZE 32
+#define STREAM_HEADER_SIZE 24
+#define PARTY_PUBKEY_SIZE 32
+#define PARTY_SECKEY_SIZE 32
+
+
+__attribute__((warn_unused_result))
+static bool check_libsodium_size_assumptions(void) {
+ return
+ crypto_secretstream_xchacha20poly1305_KEYBYTES == STREAM_KEY_SIZE &&
+ crypto_secretstream_xchacha20poly1305_HEADERBYTES == STREAM_HEADER_SIZE &&
+ crypto_kx_SESSIONKEYBYTES >= STREAM_KEY_SIZE &&
+ crypto_kx_PUBLICKEYBYTES == PARTY_PUBKEY_SIZE &&
+ crypto_kx_SECRETKEYBYTES == PARTY_SECKEY_SIZE;
+}
+
+static void send_clienthello(
+ int fd,
+ const unsigned char pubkey[PARTY_PUBKEY_SIZE]
+) {
+ char buffer[16 + PARTY_PUBKEY_SIZE];
+ memcpy(buffer, "tomsg v01 client", 16);
+ memcpy(buffer + 16, pubkey, PARTY_PUBKEY_SIZE);
+ net_send_raw_text(fd, buffer, sizeof buffer);
+}
+
+static void send_serverhello(
+ int fd,
+ const unsigned char pubkey[PARTY_PUBKEY_SIZE],
+ const unsigned char server_tx_header[STREAM_HEADER_SIZE]
+) {
+ char buffer[16 + PARTY_PUBKEY_SIZE + STREAM_HEADER_SIZE];
+ memcpy(buffer, "tomsg v01 server", 16);
+ memcpy(buffer + 16, pubkey, PARTY_PUBKEY_SIZE);
+ memcpy(buffer + 16 + PARTY_PUBKEY_SIZE, server_tx_header, STREAM_HEADER_SIZE);
+ net_send_raw_text(fd, buffer, sizeof buffer);
+}
+
+static void send_clienthello2(
+ int fd,
+ const unsigned char client_tx_header[STREAM_HEADER_SIZE]
+) {
+ char buffer[8 + STREAM_HEADER_SIZE];
+ memcpy(buffer, "cheader0", 8);
+ memcpy(buffer + 8, client_tx_header, STREAM_HEADER_SIZE);
+ net_send_raw_text(fd, buffer, sizeof buffer);
+}
+
+
+enum phase {
+ WAITING_FOR_SERVERHELLO,
+ WAITING_FOR_CLIENTHELLO,
+ WAITING_FOR_CLIENTHELLO2,
+ WAITING_FOR_DATA, // handshake finished
+};
+
+struct transport {
+ int fd;
+ bool is_server;
+ enum phase phase;
+ unsigned char my_pubkey[PARTY_PUBKEY_SIZE];
+ unsigned char my_seckey[PARTY_SECKEY_SIZE];
+ unsigned char other_pubkey[PARTY_PUBKEY_SIZE];
+ crypto_secretstream_xchacha20poly1305_state pushstate;
+ crypto_secretstream_xchacha20poly1305_state pullstate;
+};
+
+struct transport* tsp_start(int fd, bool is_server) {
+ if (!check_libsodium_size_assumptions()) {
+ die("Transport protocol size assumptions do not hold!");
+ }
+
+ struct transport *tsp = calloc(1, struct transport);
+ tsp->fd = fd;
+ tsp->is_server = is_server;
+
+ crypto_kx_keypair(tsp->my_pubkey, tsp->my_seckey);
+
+ if (is_server) {
+ tsp->phase = WAITING_FOR_CLIENTHELLO;
+ } else {
+ send_clienthello(fd, tsp->my_pubkey);
+ tsp->phase = WAITING_FOR_SERVERHELLO;
+ }
+
+ return tsp;
+}
+
+void tsp_close(struct transport *tsp) {
+ if (tsp->phase == WAITING_FOR_DATA) {
+
+ }
+}
diff --git a/transport.h b/transport.h
new file mode 100644
index 0000000..21cebd3
--- /dev/null
+++ b/transport.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdbool.h>
+
+struct transport;
+
+// Takes ownership of socket.
+// Returns NULL and closes socket if setup fails.
+struct transport* tsp_start(int fd, bool is_server);
+
+// Also closes socket.
+void tsp_close(struct transport *tsp);
+
+// Get file descriptor that can be used in e.g. select(2) to detect possible
+// data on the transport. If there is data, the tsp_recv() function must be
+// used to handle it, because it may be protocol-level data for the integrity
+// of the transport.
+int tsp_select_fd(const struct transport *tsp);
+
+// If successful, returns true.
+// If unsuccessful, returns false and closes the transport (after which the
+// transport may not be used again).
+bool tsp_send(struct transport *tsp, const char *data, size_t length);
+
+struct received_data {
+ char *data;
+ size_t length;
+ bool error;
+};
+void received_data_nullify(struct received_data data);
+
+// If no data is avilable yet, returns {.data=NULL, length=0, error=false}.
+// If an error occurred, returns {.data=NULL, length=0, error=true}.
+// If data is avilable, returns {.data=..., length=(length of data), error=false}.
+// This function also handles protocol-level actions that do not actually
+// return application-level data, but that are indeed necessary.
+struct received_data tsp_recv(struct transport *tsp);