From bbb8bc475593b9ff481ec214c4391fe6aff854f4 Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Sun, 21 Jun 2020 22:32:27 +0200 Subject: WIP secure transport using libsodium secretstream --- .gitignore | 2 + protocol.md | 9 ++-- protocol_transport.md | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++ transport.c | 99 ++++++++++++++++++++++++++++++++++++++ transport.h | 38 +++++++++++++++ 5 files changed, 273 insertions(+), 3 deletions(-) create mode 100644 protocol_transport.md create mode 100644 transport.c create mode 100644 transport.h 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 +#include +#include +#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 +#include + +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); -- cgit v1.2.3