880 lines
26 KiB
Diff
880 lines
26 KiB
Diff
|
From cc521f3d5f3a0c758163c871e75f5e533e86771b Mon Sep 17 00:00:00 2001
|
||
|
From: Guus Sliepen <guus@tinc-vpn.org>
|
||
|
Date: Mon, 2 Aug 2021 23:53:13 +0200
|
||
|
Subject: [PATCH 01/10] Add AES-256-GCM support to SPTPS.
|
||
|
|
||
|
This also adds a simple cipher suite negotiation, where peers announce the
|
||
|
ciphers they support, and their preferred cipher. Since we need to bump the
|
||
|
SPTPS version anyway, also prefer little endian over network byte order.
|
||
|
---
|
||
|
doc/SPTPS | 45 +++++--
|
||
|
src/invitation.c | 11 +-
|
||
|
src/protocol_auth.c | 25 +++-
|
||
|
src/protocol_key.c | 30 ++++-
|
||
|
src/sptps.c | 310 +++++++++++++++++++++++++++++++++-----------
|
||
|
src/sptps.h | 44 ++++++-
|
||
|
src/sptps_test.c | 13 +-
|
||
|
7 files changed, 382 insertions(+), 96 deletions(-)
|
||
|
|
||
|
diff --git a/doc/SPTPS b/doc/SPTPS
|
||
|
index 2d8fee5b..2da27604 100644
|
||
|
--- a/doc/SPTPS
|
||
|
+++ b/doc/SPTPS
|
||
|
@@ -18,8 +18,8 @@ Stream record layer
|
||
|
|
||
|
A record consists of these fields:
|
||
|
|
||
|
-- uint32_t seqno (network byte order)
|
||
|
-- uint16_t length (network byte order)
|
||
|
+- uint32_t seqno (little endian)
|
||
|
+- uint16_t length (little endian)
|
||
|
- uint8_t type
|
||
|
- opaque data[length]
|
||
|
- opaque hmac[HMAC_SIZE] (HMAC over all preceding fields)
|
||
|
@@ -45,8 +45,8 @@ Datagram record layer
|
||
|
|
||
|
A record consists of these fields:
|
||
|
|
||
|
-- uint16_t length (network byte order)
|
||
|
-- uint32_t seqno (network byte order)
|
||
|
+- uint16_t length (little endian)
|
||
|
+- uint32_t seqno (little endian)
|
||
|
- uint8_t type
|
||
|
- opaque data[length]
|
||
|
- opaque hmac[HMAC_SIZE] (HMAC over all preceding fields)
|
||
|
@@ -75,7 +75,7 @@ SIG ->
|
||
|
...encrypt and HMAC using session keys from now on...
|
||
|
|
||
|
App ->
|
||
|
- <- App
|
||
|
+ <- App
|
||
|
...
|
||
|
...
|
||
|
|
||
|
@@ -91,7 +91,7 @@ ACK ->
|
||
|
...encrypt and HMAC using new session keys from now on...
|
||
|
|
||
|
App ->
|
||
|
- <- App
|
||
|
+ <- App
|
||
|
...
|
||
|
...
|
||
|
---------------------
|
||
|
@@ -102,7 +102,11 @@ connection.
|
||
|
|
||
|
Key EXchange message:
|
||
|
|
||
|
-- uint8_t kex_version (always 0 in this version of SPTPS)
|
||
|
+- uint8_t kex_version (always 1 in this version of SPTPS)
|
||
|
+- uint8_t
|
||
|
+ - high 4 bits: public key algorithm
|
||
|
+ - low 4 bits: preferred cipher suite
|
||
|
+- uint16_t bitmask of cipher suites supported
|
||
|
- opaque nonce[32] (random number)
|
||
|
- opaque ecdh_key[ECDH_SIZE]
|
||
|
|
||
|
@@ -162,9 +166,34 @@ The expanded key is used as follows:
|
||
|
Where initiator_cipher_key is the key used by session initiator to encrypt
|
||
|
messages sent to the responder.
|
||
|
|
||
|
+Public key suites
|
||
|
+-----------------
|
||
|
+
|
||
|
+0: Ed25519 + SHA512
|
||
|
+1: Ed448 + SHAKE256?
|
||
|
+
|
||
|
+Symmetric cipher suites
|
||
|
+-----------------------
|
||
|
+
|
||
|
+Value in parentheses is the static priority used to break ties in cipher suite
|
||
|
+negotiation. We favor those algorithms that run faster without hardware
|
||
|
+acceleration.
|
||
|
+
|
||
|
+0: Chacha20-Poly1305 (1)
|
||
|
+1: AES256-GCM (0)
|
||
|
+
|
||
|
+Cipher suite selection
|
||
|
+----------------------
|
||
|
+
|
||
|
+Public key suites are required to match on both sides. The symmetric suite is chosen as follows:
|
||
|
+
|
||
|
+1. AND the supported cipher suite bitmasks
|
||
|
+2. If both preferred cipher suites are possible, choose the one with the highest static priority.
|
||
|
+3. If only one is possible, choose that one.
|
||
|
+4. If none is possible, choose the suite from the resulting bitmask that has the highest static priority.
|
||
|
+
|
||
|
TODO:
|
||
|
-----
|
||
|
|
||
|
- Document format of ECDH public key, ECDSA signature
|
||
|
-- Document how CTR mode is used
|
||
|
- Refer to TLS RFCs where appropriate
|
||
|
diff --git a/src/invitation.c b/src/invitation.c
|
||
|
index cff9e727..6c49af48 100644
|
||
|
--- a/src/invitation.c
|
||
|
+++ b/src/invitation.c
|
||
|
@@ -1399,7 +1399,16 @@ next:
|
||
|
}
|
||
|
|
||
|
// Start an SPTPS session
|
||
|
- if(!sptps_start(&sptps, NULL, true, false, key, hiskey, "tinc invitation", 15, invitation_send, invitation_receive)) {
|
||
|
+ sptps_params_t params = {
|
||
|
+ .initiator = true,
|
||
|
+ .mykey = key,
|
||
|
+ .hiskey = hiskey,
|
||
|
+ .label = "tinc invitation",
|
||
|
+ .send_data = invitation_send,
|
||
|
+ .receive_record = invitation_receive,
|
||
|
+ };
|
||
|
+
|
||
|
+ if(!sptps_start(&sptps, ¶ms)) {
|
||
|
ecdsa_free(hiskey);
|
||
|
ecdsa_free(key);
|
||
|
return 1;
|
||
|
diff --git a/src/protocol_auth.c b/src/protocol_auth.c
|
||
|
index 22254575..050b266c 100644
|
||
|
--- a/src/protocol_auth.c
|
||
|
+++ b/src/protocol_auth.c
|
||
|
@@ -412,7 +412,17 @@ bool id_h(connection_t *c, const char *request) {
|
||
|
|
||
|
c->protocol_minor = 2;
|
||
|
|
||
|
- return sptps_start(&c->sptps, c, false, false, invitation_key, c->ecdsa, "tinc invitation", 15, send_meta_sptps, receive_invitation_sptps);
|
||
|
+ sptps_params_t params = {
|
||
|
+ .handle = c,
|
||
|
+ .initiator = false,
|
||
|
+ .mykey = invitation_key,
|
||
|
+ .hiskey = c->ecdsa,
|
||
|
+ .label = "tinc invitation",
|
||
|
+ .send_data = send_meta_sptps,
|
||
|
+ .receive_record = receive_invitation_sptps,
|
||
|
+ };
|
||
|
+
|
||
|
+ return sptps_start(&c->sptps, ¶ms);
|
||
|
}
|
||
|
|
||
|
/* Check if identity is a valid name */
|
||
|
@@ -507,7 +517,18 @@ bool id_h(connection_t *c, const char *request) {
|
||
|
snprintf(label, labellen, "tinc TCP key expansion %s %s", c->name, myself->name);
|
||
|
}
|
||
|
|
||
|
- return sptps_start(&c->sptps, c, c->outgoing, false, myself->connection->ecdsa, c->ecdsa, label, labellen, send_meta_sptps, receive_meta_sptps);
|
||
|
+ sptps_params_t params = {
|
||
|
+ .handle = c,
|
||
|
+ .initiator = c->outgoing,
|
||
|
+ .mykey = myself->connection->ecdsa,
|
||
|
+ .hiskey = c->ecdsa,
|
||
|
+ .label = label,
|
||
|
+ .labellen = sizeof(label),
|
||
|
+ .send_data = send_meta_sptps,
|
||
|
+ .receive_record = receive_meta_sptps,
|
||
|
+ };
|
||
|
+
|
||
|
+ return sptps_start(&c->sptps, ¶ms);
|
||
|
} else {
|
||
|
return send_metakey(c);
|
||
|
}
|
||
|
diff --git a/src/protocol_key.c b/src/protocol_key.c
|
||
|
index 740d2fb4..da53c16c 100644
|
||
|
--- a/src/protocol_key.c
|
||
|
+++ b/src/protocol_key.c
|
||
|
@@ -128,7 +128,20 @@ bool send_req_key(node_t *to) {
|
||
|
to->status.waitingforkey = true;
|
||
|
to->last_req_key = now.tv_sec;
|
||
|
to->incompression = myself->incompression;
|
||
|
- return sptps_start(&to->sptps, to, true, true, myself->connection->ecdsa, to->ecdsa, label, labellen, send_initial_sptps_data, receive_sptps_record);
|
||
|
+
|
||
|
+ sptps_params_t params = {
|
||
|
+ .handle = to,
|
||
|
+ .initiator = true,
|
||
|
+ .datagram = true,
|
||
|
+ .mykey = myself->connection->ecdsa,
|
||
|
+ .hiskey = to->ecdsa,
|
||
|
+ .label = label,
|
||
|
+ .labellen = sizeof(label),
|
||
|
+ .send_data = send_initial_sptps_data,
|
||
|
+ .receive_record = receive_sptps_record,
|
||
|
+ };
|
||
|
+
|
||
|
+ return sptps_start(&to->sptps, ¶ms);
|
||
|
}
|
||
|
|
||
|
return send_request(to->nexthop->connection, "%d %s %s", REQ_KEY, myself->name, to->name);
|
||
|
@@ -249,7 +262,20 @@ static bool req_key_ext_h(connection_t *c, const char *request, node_t *from, no
|
||
|
from->status.validkey = false;
|
||
|
from->status.waitingforkey = true;
|
||
|
from->last_req_key = now.tv_sec;
|
||
|
- sptps_start(&from->sptps, from, false, true, myself->connection->ecdsa, from->ecdsa, label, labellen, send_sptps_data_myself, receive_sptps_record);
|
||
|
+
|
||
|
+ sptps_params_t params = {
|
||
|
+ .handle = from,
|
||
|
+ .initiator = false,
|
||
|
+ .datagram = true,
|
||
|
+ .mykey = myself->connection->ecdsa,
|
||
|
+ .hiskey = from->ecdsa,
|
||
|
+ .label = label,
|
||
|
+ .labellen = sizeof(label),
|
||
|
+ .send_data = send_sptps_data_myself,
|
||
|
+ .receive_record = receive_sptps_record,
|
||
|
+ };
|
||
|
+
|
||
|
+ sptps_start(&from->sptps, ¶ms);
|
||
|
sptps_receive_data(&from->sptps, buf, len);
|
||
|
send_mtu_info(myself, from, MTU);
|
||
|
return true;
|
||
|
diff --git a/src/sptps.c b/src/sptps.c
|
||
|
index a0483c34..33c41424 100644
|
||
|
--- a/src/sptps.c
|
||
|
+++ b/src/sptps.c
|
||
|
@@ -28,6 +28,10 @@
|
||
|
#include "sptps.h"
|
||
|
#include "random.h"
|
||
|
|
||
|
+#ifdef HAVE_OPENSSL
|
||
|
+#include <openssl/evp.h>
|
||
|
+#endif
|
||
|
+
|
||
|
unsigned int sptps_replaywin = 16;
|
||
|
|
||
|
/*
|
||
|
@@ -90,25 +94,159 @@ static void warning(sptps_t *s, const char *format, ...) {
|
||
|
va_end(ap);
|
||
|
}
|
||
|
|
||
|
+static bool cipher_init(uint8_t suite, void **ctx, const uint8_t *key, bool key_half) {
|
||
|
+ switch(suite) {
|
||
|
+ case SPTPS_CHACHA_POLY1305:
|
||
|
+ *ctx = chacha_poly1305_init();
|
||
|
+ return ctx && chacha_poly1305_set_key(*ctx, key + (key_half ? CHACHA_POLY1305_KEYLEN : 0));
|
||
|
+
|
||
|
+ case SPTPS_AES256_GCM:
|
||
|
+#ifdef HAVE_OPENSSL
|
||
|
+ *ctx = EVP_CIPHER_CTX_new();
|
||
|
+
|
||
|
+ if(!ctx) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ return EVP_EncryptInit_ex(*ctx, EVP_aes_256_gcm(), NULL, NULL, NULL)
|
||
|
+ && EVP_CIPHER_CTX_ctrl(*ctx, EVP_CTRL_AEAD_SET_IVLEN, 4, NULL)
|
||
|
+ && EVP_EncryptInit_ex(*ctx, NULL, NULL, key + (key_half ? 32 : 0), key);
|
||
|
+#endif
|
||
|
+
|
||
|
+ default:
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+}
|
||
|
+
|
||
|
+static void cipher_exit(uint8_t suite, void *ctx) {
|
||
|
+ switch(suite) {
|
||
|
+ case SPTPS_CHACHA_POLY1305:
|
||
|
+ chacha_poly1305_exit(ctx);
|
||
|
+ break;
|
||
|
+
|
||
|
+ case SPTPS_AES256_GCM:
|
||
|
+#ifdef HAVE_OPENSSL
|
||
|
+ EVP_CIPHER_CTX_free(ctx);
|
||
|
+ break;
|
||
|
+#endif
|
||
|
+
|
||
|
+ default:
|
||
|
+ break;
|
||
|
+ }
|
||
|
+}
|
||
|
+
|
||
|
+static bool cipher_encrypt(uint8_t suite, void *ctx, uint32_t seqno, const uint8_t *in, size_t inlen, uint8_t *out, size_t *outlen) {
|
||
|
+ switch(suite) {
|
||
|
+ case SPTPS_CHACHA_POLY1305:
|
||
|
+ chacha_poly1305_encrypt(ctx, seqno, in, inlen, out, outlen);
|
||
|
+ return true;
|
||
|
+
|
||
|
+ case SPTPS_AES256_GCM:
|
||
|
+#ifdef HAVE_OPENSSL
|
||
|
+ {
|
||
|
+ if(!EVP_EncryptInit_ex(ctx, NULL, NULL, NULL, (uint8_t *)&seqno)) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ int outlen1 = 0, outlen2 = 0;
|
||
|
+
|
||
|
+ if(!EVP_EncryptUpdate(ctx, out, &outlen1, in, (int)inlen)) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ if(!EVP_EncryptFinal_ex(ctx, out + outlen1, &outlen2)) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ outlen1 += outlen2;
|
||
|
+
|
||
|
+ if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, out + outlen1)) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ outlen1 += 16;
|
||
|
+
|
||
|
+ if(outlen) {
|
||
|
+ *outlen = outlen1;
|
||
|
+ }
|
||
|
+
|
||
|
+ return true;
|
||
|
+ }
|
||
|
+
|
||
|
+#endif
|
||
|
+
|
||
|
+ default:
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+}
|
||
|
+
|
||
|
+static bool cipher_decrypt(uint8_t suite, void *ctx, uint32_t seqno, const uint8_t *in, size_t inlen, uint8_t *out, size_t *outlen) {
|
||
|
+ switch(suite) {
|
||
|
+ case SPTPS_CHACHA_POLY1305:
|
||
|
+ return chacha_poly1305_decrypt(ctx, seqno, in, inlen, out, outlen);
|
||
|
+
|
||
|
+ case SPTPS_AES256_GCM:
|
||
|
+#ifdef HAVE_OPENSSL
|
||
|
+ {
|
||
|
+ if(inlen < 16) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ inlen -= 16;
|
||
|
+
|
||
|
+ if(!EVP_DecryptInit_ex(ctx, NULL, NULL, NULL, (uint8_t *)&seqno)) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ int outlen1 = 0, outlen2 = 0;
|
||
|
+
|
||
|
+ if(!EVP_DecryptUpdate(ctx, out, &outlen1, in, (int)inlen)) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (void *)(in + inlen))) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ if(!EVP_DecryptFinal_ex(ctx, out + outlen1, &outlen2)) {
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+
|
||
|
+ if(outlen) {
|
||
|
+ *outlen = outlen1 + outlen2;
|
||
|
+ }
|
||
|
+
|
||
|
+ return true;
|
||
|
+ }
|
||
|
+
|
||
|
+#endif
|
||
|
+
|
||
|
+ default:
|
||
|
+ return false;
|
||
|
+ }
|
||
|
+}
|
||
|
+
|
||
|
+
|
||
|
// Send a record (datagram version, accepts all record types, handles encryption and authentication).
|
||
|
static bool send_record_priv_datagram(sptps_t *s, uint8_t type, const void *data, uint16_t len) {
|
||
|
- uint8_t *buffer = alloca(len + 21UL);
|
||
|
-
|
||
|
+ uint8_t *buffer = alloca(len + SPTPS_DATAGRAM_OVERHEAD);
|
||
|
// Create header with sequence number, length and record type
|
||
|
uint32_t seqno = s->outseqno++;
|
||
|
- uint32_t netseqno = ntohl(seqno);
|
||
|
|
||
|
- memcpy(buffer, &netseqno, 4);
|
||
|
+ memcpy(buffer, &seqno, 4);
|
||
|
buffer[4] = type;
|
||
|
memcpy(buffer + 5, data, len);
|
||
|
|
||
|
if(s->outstate) {
|
||
|
// If first handshake has finished, encrypt and HMAC
|
||
|
- chacha_poly1305_encrypt(s->outcipher, seqno, buffer + 4, len + 1, buffer + 4, NULL);
|
||
|
- return s->send_data(s->handle, type, buffer, len + 21UL);
|
||
|
+ if(!cipher_encrypt(s->cipher_suite, s->outcipher, seqno, buffer + 4, len + 1, buffer + 4, NULL)) {
|
||
|
+ return error(s, EINVAL, "Failed to encrypt message");
|
||
|
+ }
|
||
|
+
|
||
|
+ return s->send_data(s->handle, type, buffer, len + SPTPS_DATAGRAM_OVERHEAD);
|
||
|
} else {
|
||
|
// Otherwise send as plaintext
|
||
|
- return s->send_data(s->handle, type, buffer, len + 5UL);
|
||
|
+ return s->send_data(s->handle, type, buffer, len + SPTPS_DATAGRAM_HEADER);
|
||
|
}
|
||
|
}
|
||
|
// Send a record (private version, accepts all record types, handles encryption and authentication).
|
||
|
@@ -117,11 +255,11 @@ static bool send_record_priv(sptps_t *s, uint8_t type, const void *data, uint16_
|
||
|
return send_record_priv_datagram(s, type, data, len);
|
||
|
}
|
||
|
|
||
|
- uint8_t *buffer = alloca(len + 19UL);
|
||
|
+ uint8_t *buffer = alloca(len + SPTPS_OVERHEAD);
|
||
|
|
||
|
// Create header with sequence number, length and record type
|
||
|
uint32_t seqno = s->outseqno++;
|
||
|
- uint16_t netlen = htons(len);
|
||
|
+ uint16_t netlen = len;
|
||
|
|
||
|
memcpy(buffer, &netlen, 2);
|
||
|
buffer[2] = type;
|
||
|
@@ -129,11 +267,14 @@ static bool send_record_priv(sptps_t *s, uint8_t type, const void *data, uint16_
|
||
|
|
||
|
if(s->outstate) {
|
||
|
// If first handshake has finished, encrypt and HMAC
|
||
|
- chacha_poly1305_encrypt(s->outcipher, seqno, buffer + 2, len + 1, buffer + 2, NULL);
|
||
|
- return s->send_data(s->handle, type, buffer, len + 19UL);
|
||
|
+ if(!cipher_encrypt(s->cipher_suite, s->outcipher, seqno, buffer + 2, len + 1, buffer + 2, NULL)) {
|
||
|
+ return error(s, EINVAL, "Failed to encrypt message");
|
||
|
+ }
|
||
|
+
|
||
|
+ return s->send_data(s->handle, type, buffer, len + SPTPS_OVERHEAD);
|
||
|
} else {
|
||
|
// Otherwise send as plaintext
|
||
|
- return s->send_data(s->handle, type, buffer, len + 3UL);
|
||
|
+ return s->send_data(s->handle, type, buffer, len + SPTPS_HEADER);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@@ -161,7 +302,7 @@ static bool send_kex(sptps_t *s) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
- s->mykex = realloc(s->mykex, 1 + 32 + keylen);
|
||
|
+ s->mykex = realloc(s->mykex, 4 + 32 + keylen);
|
||
|
|
||
|
if(!s->mykex) {
|
||
|
return error(s, errno, strerror(errno));
|
||
|
@@ -169,16 +310,18 @@ static bool send_kex(sptps_t *s) {
|
||
|
|
||
|
// Set version byte to zero.
|
||
|
s->mykex[0] = SPTPS_VERSION;
|
||
|
+ s->mykex[1] = s->preferred_suite;
|
||
|
+ memcpy(s->mykex + 2, &s->cipher_suites, 2);
|
||
|
|
||
|
// Create a random nonce.
|
||
|
- randomize(s->mykex + 1, 32);
|
||
|
+ randomize(s->mykex + 4, 32);
|
||
|
|
||
|
// Create a new ECDH public key.
|
||
|
- if(!(s->ecdh = ecdh_generate_public(s->mykex + 1 + 32))) {
|
||
|
+ if(!(s->ecdh = ecdh_generate_public(s->mykex + 4 + 32))) {
|
||
|
return error(s, EINVAL, "Failed to generate ECDH public key");
|
||
|
}
|
||
|
|
||
|
- return send_record_priv(s, SPTPS_HANDSHAKE, s->mykex, 1 + 32 + keylen);
|
||
|
+ return send_record_priv(s, SPTPS_HANDSHAKE, s->mykex, 4 + 32 + keylen);
|
||
|
}
|
||
|
|
||
|
// Send a SIGnature record, containing an Ed25519 signature over both KEX records.
|
||
|
@@ -192,9 +335,9 @@ static bool send_sig(sptps_t *s) {
|
||
|
uint8_t *sig = alloca(siglen);
|
||
|
|
||
|
msg[0] = s->initiator;
|
||
|
- memcpy(msg + 1, s->mykex, 1 + 32 + keylen);
|
||
|
- memcpy(msg + 1 + 33 + keylen, s->hiskex, 1 + 32 + keylen);
|
||
|
- memcpy(msg + 1 + 2 * (33 + keylen), s->label, s->labellen);
|
||
|
+ memcpy(msg + 1, s->mykex, 4 + 32 + keylen);
|
||
|
+ memcpy(msg + 1 + (4 + 32 + keylen), s->hiskex, 4 + 32 + keylen);
|
||
|
+ memcpy(msg + 1 + 2 * (4 + 32 + keylen), s->label, s->labellen);
|
||
|
|
||
|
// Sign the result.
|
||
|
if(!ecdsa_sign(s->mykey, msg, msglen, sig)) {
|
||
|
@@ -207,16 +350,6 @@ static bool send_sig(sptps_t *s) {
|
||
|
|
||
|
// Generate key material from the shared secret created from the ECDHE key exchange.
|
||
|
static bool generate_key_material(sptps_t *s, const uint8_t *shared, size_t len) {
|
||
|
- // Initialise cipher and digest structures if necessary
|
||
|
- if(!s->outstate) {
|
||
|
- s->incipher = chacha_poly1305_init();
|
||
|
- s->outcipher = chacha_poly1305_init();
|
||
|
-
|
||
|
- if(!s->incipher || !s->outcipher) {
|
||
|
- return error(s, EINVAL, "Failed to open cipher");
|
||
|
- }
|
||
|
- }
|
||
|
-
|
||
|
// Allocate memory for key material
|
||
|
size_t keylen = 2 * CHACHA_POLY1305_KEYLEN;
|
||
|
|
||
|
@@ -261,14 +394,8 @@ static bool receive_ack(sptps_t *s, const uint8_t *data, uint16_t len) {
|
||
|
return error(s, EIO, "Invalid ACK record length");
|
||
|
}
|
||
|
|
||
|
- if(s->initiator) {
|
||
|
- if(!chacha_poly1305_set_key(s->incipher, s->key)) {
|
||
|
- return error(s, EINVAL, "Failed to set counter");
|
||
|
- }
|
||
|
- } else {
|
||
|
- if(!chacha_poly1305_set_key(s->incipher, s->key + CHACHA_POLY1305_KEYLEN)) {
|
||
|
- return error(s, EINVAL, "Failed to set counter");
|
||
|
- }
|
||
|
+ if(!cipher_init(s->cipher_suite, &s->incipher, s->key, s->initiator)) {
|
||
|
+ return error(s, EINVAL, "Failed to initialize cipher");
|
||
|
}
|
||
|
|
||
|
free(s->key);
|
||
|
@@ -278,14 +405,51 @@ static bool receive_ack(sptps_t *s, const uint8_t *data, uint16_t len) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
+static uint8_t select_cipher_suite(uint16_t mask, uint8_t pref1, uint8_t pref2) {
|
||
|
+ // Check if there is a viable preference, if so select the lowest one
|
||
|
+ uint8_t selection = 255;
|
||
|
+
|
||
|
+ if(mask & (1U << pref1)) {
|
||
|
+ selection = pref1;
|
||
|
+ }
|
||
|
+
|
||
|
+ if(pref2 < selection && (mask & (1U << pref2))) {
|
||
|
+ selection = pref2;
|
||
|
+ }
|
||
|
+
|
||
|
+ // Otherwise, select the lowest cipher suite both sides support
|
||
|
+ if(selection == 255) {
|
||
|
+ selection = 0;
|
||
|
+
|
||
|
+ while(!(mask & 1U)) {
|
||
|
+ selection++;
|
||
|
+ mask >>= 1;
|
||
|
+ }
|
||
|
+ }
|
||
|
+
|
||
|
+ return selection;
|
||
|
+}
|
||
|
+
|
||
|
// Receive a Key EXchange record, respond by sending a SIG record.
|
||
|
static bool receive_kex(sptps_t *s, const uint8_t *data, uint16_t len) {
|
||
|
// Verify length of the HELLO record
|
||
|
- if(len != 1 + 32 + ECDH_SIZE) {
|
||
|
+ if(len != 4 + 32 + ECDH_SIZE) {
|
||
|
return error(s, EIO, "Invalid KEX record length");
|
||
|
}
|
||
|
|
||
|
- // Ignore version number for now.
|
||
|
+ if(data[0] != SPTPS_VERSION) {
|
||
|
+ return error(s, EIO, "Incompatible SPTPS version");
|
||
|
+ }
|
||
|
+
|
||
|
+ uint16_t suites;
|
||
|
+ memcpy(&suites, data + 2, 2);
|
||
|
+ suites &= s->cipher_suites;
|
||
|
+
|
||
|
+ if(!suites) {
|
||
|
+ return error(s, EIO, "No matching cipher suites");
|
||
|
+ }
|
||
|
+
|
||
|
+ s->cipher_suite = select_cipher_suite(suites, s->preferred_suite, data[1] & 0xf);
|
||
|
|
||
|
// Make a copy of the KEX message, send_sig() and receive_sig() need it
|
||
|
if(s->hiskex) {
|
||
|
@@ -322,9 +486,9 @@ static bool receive_sig(sptps_t *s, const uint8_t *data, uint16_t len) {
|
||
|
uint8_t *msg = alloca(msglen);
|
||
|
|
||
|
msg[0] = !s->initiator;
|
||
|
- memcpy(msg + 1, s->hiskex, 1 + 32 + keylen);
|
||
|
- memcpy(msg + 1 + 33 + keylen, s->mykex, 1 + 32 + keylen);
|
||
|
- memcpy(msg + 1 + 2 * (33 + keylen), s->label, s->labellen);
|
||
|
+ memcpy(msg + 1, s->hiskex, 4 + 32 + keylen);
|
||
|
+ memcpy(msg + 1 + (4 + 32 + keylen), s->mykex, 4 + 32 + keylen);
|
||
|
+ memcpy(msg + 1 + 2 * (4 + 32 + keylen), s->label, s->labellen);
|
||
|
|
||
|
// Verify signature.
|
||
|
if(!ecdsa_verify(s->hiskey, msg, msglen, data)) {
|
||
|
@@ -334,7 +498,7 @@ static bool receive_sig(sptps_t *s, const uint8_t *data, uint16_t len) {
|
||
|
// Compute shared secret.
|
||
|
uint8_t shared[ECDH_SHARED_SIZE];
|
||
|
|
||
|
- if(!ecdh_compute_shared(s->ecdh, s->hiskex + 1 + 32, shared)) {
|
||
|
+ if(!ecdh_compute_shared(s->ecdh, s->hiskex + 4 + 32, shared)) {
|
||
|
return error(s, EINVAL, "Failed to compute ECDH shared secret");
|
||
|
}
|
||
|
|
||
|
@@ -360,15 +524,8 @@ static bool receive_sig(sptps_t *s, const uint8_t *data, uint16_t len) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
- // TODO: only set new keys after ACK has been set/received
|
||
|
- if(s->initiator) {
|
||
|
- if(!chacha_poly1305_set_key(s->outcipher, s->key + CHACHA_POLY1305_KEYLEN)) {
|
||
|
- return error(s, EINVAL, "Failed to set key");
|
||
|
- }
|
||
|
- } else {
|
||
|
- if(!chacha_poly1305_set_key(s->outcipher, s->key)) {
|
||
|
- return error(s, EINVAL, "Failed to set key");
|
||
|
- }
|
||
|
+ if(!cipher_init(s->cipher_suite, &s->outcipher, s->key, !s->initiator)) {
|
||
|
+ return error(s, EINVAL, "Failed to initialize cipher");
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
@@ -518,15 +675,13 @@ bool sptps_verify_datagram(sptps_t *s, const void *vdata, size_t len) {
|
||
|
const uint8_t *data = vdata;
|
||
|
uint32_t seqno;
|
||
|
memcpy(&seqno, data, 4);
|
||
|
- seqno = ntohl(seqno);
|
||
|
|
||
|
if(!sptps_check_seqno(s, seqno, false)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
uint8_t *buffer = alloca(len);
|
||
|
- size_t outlen;
|
||
|
- return chacha_poly1305_decrypt(s->incipher, seqno, data + 4, len - 4, buffer, &outlen);
|
||
|
+ return cipher_decrypt(s->cipher_suite, s->incipher, seqno, data + 4, len - 4, buffer, NULL);
|
||
|
}
|
||
|
|
||
|
// Receive incoming data, datagram version.
|
||
|
@@ -537,7 +692,6 @@ static bool sptps_receive_data_datagram(sptps_t *s, const uint8_t *data, size_t
|
||
|
|
||
|
uint32_t seqno;
|
||
|
memcpy(&seqno, data, 4);
|
||
|
- seqno = ntohl(seqno);
|
||
|
data += 4;
|
||
|
len -= 4;
|
||
|
|
||
|
@@ -563,7 +717,7 @@ static bool sptps_receive_data_datagram(sptps_t *s, const uint8_t *data, size_t
|
||
|
uint8_t *buffer = alloca(len);
|
||
|
size_t outlen;
|
||
|
|
||
|
- if(!chacha_poly1305_decrypt(s->incipher, seqno, data, len, buffer, &outlen)) {
|
||
|
+ if(!cipher_decrypt(s->cipher_suite, s->incipher, seqno, data, len, buffer, &outlen)) {
|
||
|
return error(s, EIO, "Failed to decrypt and verify packet");
|
||
|
}
|
||
|
|
||
|
@@ -635,10 +789,9 @@ size_t sptps_receive_data(sptps_t *s, const void *vdata, size_t len) {
|
||
|
// Get the length bytes
|
||
|
|
||
|
memcpy(&s->reclen, s->inbuf, 2);
|
||
|
- s->reclen = ntohs(s->reclen);
|
||
|
|
||
|
// If we have the length bytes, ensure our buffer can hold the whole request.
|
||
|
- s->inbuf = realloc(s->inbuf, s->reclen + 19UL);
|
||
|
+ s->inbuf = realloc(s->inbuf, s->reclen + SPTPS_OVERHEAD);
|
||
|
|
||
|
if(!s->inbuf) {
|
||
|
return error(s, errno, strerror(errno));
|
||
|
@@ -651,7 +804,7 @@ size_t sptps_receive_data(sptps_t *s, const void *vdata, size_t len) {
|
||
|
}
|
||
|
|
||
|
// Read up to the end of the record.
|
||
|
- size_t toread = s->reclen + (s->instate ? 19UL : 3UL) - s->buflen;
|
||
|
+ size_t toread = s->reclen + (s->instate ? SPTPS_OVERHEAD : SPTPS_HEADER) - s->buflen;
|
||
|
|
||
|
if(toread > len) {
|
||
|
toread = len;
|
||
|
@@ -662,7 +815,7 @@ size_t sptps_receive_data(sptps_t *s, const void *vdata, size_t len) {
|
||
|
s->buflen += toread;
|
||
|
|
||
|
// If we don't have a whole record, exit.
|
||
|
- if(s->buflen < s->reclen + (s->instate ? 19UL : 3UL)) {
|
||
|
+ if(s->buflen < s->reclen + (s->instate ? SPTPS_OVERHEAD : SPTPS_HEADER)) {
|
||
|
return total_read;
|
||
|
}
|
||
|
|
||
|
@@ -672,13 +825,13 @@ size_t sptps_receive_data(sptps_t *s, const void *vdata, size_t len) {
|
||
|
|
||
|
// Check HMAC and decrypt.
|
||
|
if(s->instate) {
|
||
|
- if(!chacha_poly1305_decrypt(s->incipher, seqno, s->inbuf + 2UL, s->reclen + 17UL, s->inbuf + 2UL, NULL)) {
|
||
|
+ if(!cipher_decrypt(s->cipher_suite, s->incipher, seqno, s->inbuf + 2UL, s->reclen + 17UL, s->inbuf + 2UL, NULL)) {
|
||
|
return error(s, EINVAL, "Failed to decrypt and verify record");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Append a NULL byte for safety.
|
||
|
- s->inbuf[s->reclen + 3UL] = 0;
|
||
|
+ s->inbuf[s->reclen + SPTPS_HEADER] = 0;
|
||
|
|
||
|
uint8_t type = s->inbuf[2];
|
||
|
|
||
|
@@ -704,16 +857,18 @@ size_t sptps_receive_data(sptps_t *s, const void *vdata, size_t len) {
|
||
|
}
|
||
|
|
||
|
// Start a SPTPS session.
|
||
|
-bool sptps_start(sptps_t *s, void *handle, bool initiator, bool datagram, ecdsa_t *mykey, ecdsa_t *hiskey, const void *label, size_t labellen, send_data_t send_data, receive_record_t receive_record) {
|
||
|
+bool sptps_start(sptps_t *s, const sptps_params_t *params) {
|
||
|
// Initialise struct sptps
|
||
|
memset(s, 0, sizeof(*s));
|
||
|
|
||
|
- s->handle = handle;
|
||
|
- s->initiator = initiator;
|
||
|
- s->datagram = datagram;
|
||
|
- s->mykey = mykey;
|
||
|
- s->hiskey = hiskey;
|
||
|
+ s->handle = params->handle;
|
||
|
+ s->initiator = params->initiator;
|
||
|
+ s->datagram = params->datagram;
|
||
|
+ s->mykey = params->mykey;
|
||
|
+ s->hiskey = params->hiskey;
|
||
|
s->replaywin = sptps_replaywin;
|
||
|
+ s->cipher_suites = params->cipher_suites ? params->cipher_suites & SPTPS_ALL_CIPHER_SUITES : SPTPS_ALL_CIPHER_SUITES;
|
||
|
+ s->preferred_suite = params->preferred_suite;
|
||
|
|
||
|
if(s->replaywin) {
|
||
|
s->late = malloc(s->replaywin);
|
||
|
@@ -725,13 +880,16 @@ bool sptps_start(sptps_t *s, void *handle, bool initiator, bool datagram, ecdsa_
|
||
|
memset(s->late, 0, s->replaywin);
|
||
|
}
|
||
|
|
||
|
- s->label = malloc(labellen);
|
||
|
+ s->labellen = params->labellen ? params->labellen : strlen(params->label);
|
||
|
+ s->label = malloc(s->labellen);
|
||
|
|
||
|
if(!s->label) {
|
||
|
return error(s, errno, strerror(errno));
|
||
|
}
|
||
|
|
||
|
- if(!datagram) {
|
||
|
+ memcpy(s->label, params->label, s->labellen);
|
||
|
+
|
||
|
+ if(!s->datagram) {
|
||
|
s->inbuf = malloc(7);
|
||
|
|
||
|
if(!s->inbuf) {
|
||
|
@@ -741,11 +899,9 @@ bool sptps_start(sptps_t *s, void *handle, bool initiator, bool datagram, ecdsa_
|
||
|
s->buflen = 0;
|
||
|
}
|
||
|
|
||
|
- memcpy(s->label, label, labellen);
|
||
|
- s->labellen = labellen;
|
||
|
|
||
|
- s->send_data = send_data;
|
||
|
- s->receive_record = receive_record;
|
||
|
+ s->send_data = params->send_data;
|
||
|
+ s->receive_record = params->receive_record;
|
||
|
|
||
|
// Do first KEX immediately
|
||
|
s->state = SPTPS_KEX;
|
||
|
@@ -755,8 +911,8 @@ bool sptps_start(sptps_t *s, void *handle, bool initiator, bool datagram, ecdsa_
|
||
|
// Stop a SPTPS session.
|
||
|
bool sptps_stop(sptps_t *s) {
|
||
|
// Clean up any resources.
|
||
|
- chacha_poly1305_exit(s->incipher);
|
||
|
- chacha_poly1305_exit(s->outcipher);
|
||
|
+ cipher_exit(s->cipher_suite, s->incipher);
|
||
|
+ cipher_exit(s->cipher_suite, s->outcipher);
|
||
|
ecdh_free(s->ecdh);
|
||
|
free(s->inbuf);
|
||
|
free(s->mykex);
|
||
|
diff --git a/src/sptps.h b/src/sptps.h
|
||
|
index 96edc366..b9ec11fd 100644
|
||
|
--- a/src/sptps.h
|
||
|
+++ b/src/sptps.h
|
||
|
@@ -26,7 +26,7 @@
|
||
|
#include "ecdh.h"
|
||
|
#include "ecdsa.h"
|
||
|
|
||
|
-#define SPTPS_VERSION 0
|
||
|
+#define SPTPS_VERSION 1
|
||
|
|
||
|
// Record types
|
||
|
#define SPTPS_HANDSHAKE 128 // Key exchange and authentication
|
||
|
@@ -34,7 +34,10 @@
|
||
|
#define SPTPS_CLOSE 130 // Application closed the connection
|
||
|
|
||
|
// Overhead for datagrams
|
||
|
-#define SPTPS_DATAGRAM_OVERHEAD 21
|
||
|
+static const size_t SPTPS_OVERHEAD = 19;
|
||
|
+static const size_t SPTPS_HEADER = 3;
|
||
|
+static const size_t SPTPS_DATAGRAM_OVERHEAD = 21;
|
||
|
+static const size_t SPTPS_DATAGRAM_HEADER = 5;
|
||
|
|
||
|
typedef bool (*send_data_t)(void *handle, uint8_t type, const void *data, size_t len);
|
||
|
typedef bool (*receive_record_t)(void *handle, uint8_t type, const void *data, uint16_t len);
|
||
|
@@ -47,9 +50,40 @@ typedef enum sptps_state_t {
|
||
|
SPTPS_ACK = 4, // Waiting for an ACKnowledgement record
|
||
|
} sptps_state_t;
|
||
|
|
||
|
+// Public key suites
|
||
|
+enum {
|
||
|
+ SPTPS_ED25519 = 0,
|
||
|
+};
|
||
|
+
|
||
|
+// Cipher suites
|
||
|
+enum {
|
||
|
+ SPTPS_CHACHA_POLY1305 = 0,
|
||
|
+ SPTPS_AES256_GCM = 1,
|
||
|
+ SPTPS_ALL_CIPHER_SUITES = 0x3,
|
||
|
+};
|
||
|
+
|
||
|
+typedef struct sptps_params {
|
||
|
+ void *handle;
|
||
|
+ bool initiator;
|
||
|
+ bool datagram;
|
||
|
+ uint8_t preferred_suite;
|
||
|
+ uint16_t cipher_suites;
|
||
|
+ ecdsa_t *mykey;
|
||
|
+ ecdsa_t *hiskey;
|
||
|
+ const void *label;
|
||
|
+ size_t labellen;
|
||
|
+ send_data_t send_data;
|
||
|
+ receive_record_t receive_record;
|
||
|
+} sptps_params_t;
|
||
|
+
|
||
|
typedef struct sptps {
|
||
|
bool initiator;
|
||
|
bool datagram;
|
||
|
+ uint8_t preferred_suite;
|
||
|
+ uint16_t cipher_suites;
|
||
|
+
|
||
|
+ uint8_t pk_suite;
|
||
|
+ uint8_t cipher_suite;
|
||
|
sptps_state_t state;
|
||
|
|
||
|
uint8_t *inbuf;
|
||
|
@@ -57,7 +91,7 @@ typedef struct sptps {
|
||
|
uint16_t reclen;
|
||
|
|
||
|
bool instate;
|
||
|
- chacha_poly1305_ctx_t *incipher;
|
||
|
+ void *incipher;
|
||
|
uint32_t inseqno;
|
||
|
uint32_t received;
|
||
|
unsigned int replaywin;
|
||
|
@@ -65,7 +99,7 @@ typedef struct sptps {
|
||
|
uint8_t *late;
|
||
|
|
||
|
bool outstate;
|
||
|
- chacha_poly1305_ctx_t *outcipher;
|
||
|
+ void *outcipher;
|
||
|
uint32_t outseqno;
|
||
|
|
||
|
ecdsa_t *mykey;
|
||
|
@@ -87,7 +121,7 @@ extern unsigned int sptps_replaywin;
|
||
|
extern void sptps_log_quiet(sptps_t *s, int s_errno, const char *format, va_list ap);
|
||
|
extern void sptps_log_stderr(sptps_t *s, int s_errno, const char *format, va_list ap);
|
||
|
extern void (*sptps_log)(sptps_t *s, int s_errno, const char *format, va_list ap);
|
||
|
-extern bool sptps_start(sptps_t *s, void *handle, bool initiator, bool datagram, ecdsa_t *mykey, ecdsa_t *hiskey, const void *label, size_t labellen, send_data_t send_data, receive_record_t receive_record);
|
||
|
+extern bool sptps_start(sptps_t *s, const struct sptps_params *params);
|
||
|
extern bool sptps_stop(sptps_t *s);
|
||
|
extern bool sptps_send_record(sptps_t *s, uint8_t type, const void *data, uint16_t len);
|
||
|
extern size_t sptps_receive_data(sptps_t *s, const void *data, size_t len);
|
||
|
diff --git a/src/sptps_test.c b/src/sptps_test.c
|
||
|
index 249f2e4f..e77ab9c7 100644
|
||
|
--- a/src/sptps_test.c
|
||
|
+++ b/src/sptps_test.c
|
||
|
@@ -562,7 +562,18 @@ static int run_test(int argc, char *argv[]) {
|
||
|
|
||
|
sptps_t s;
|
||
|
|
||
|
- if(!sptps_start(&s, &sock, initiator, datagram, mykey, hiskey, "sptps_test", 10, send_data, receive_record)) {
|
||
|
+ sptps_params_t params = {
|
||
|
+ .handle = &sock,
|
||
|
+ .initiator = initiator,
|
||
|
+ .datagram = datagram,
|
||
|
+ .mykey = mykey,
|
||
|
+ .hiskey = hiskey,
|
||
|
+ .label = "sptps_test",
|
||
|
+ .send_data = send_data,
|
||
|
+ .receive_record = receive_record,
|
||
|
+ };
|
||
|
+
|
||
|
+ if(!sptps_start(&s, ¶ms)) {
|
||
|
free(mykey);
|
||
|
free(hiskey);
|
||
|
return 1;
|
||
|
--
|
||
|
2.36.0
|
||
|
|