Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ TEST_C_SRC := $(wildcard tests/c/*.c)
test_c:
$(QUIET)$(ECHO) "[ ${CYAN}TEST${NC} ] Build and run C unit tests"
$(QUIET)mkdir -p $(BIN_DIR)
$(QUIET)$(CC) -I include -I src/cyclone/common -I src/cyclone/cyclone_tcp -I cyclone/common -I cyclone/cyclone_tcp -I cyclone/cyclone_ssl -I cyclone/cyclone_crypto -D GPL_LICENSE_TERMS_ACCEPTED -ffunction-sections -Wl,--gc-sections -o $(BIN_DIR)/test_c $(TEST_C_SRC) $(SRC_DIR)/os_ext.c $(SRC_DIR)/str_ext.c $(SRC_DIR)/fs_ext.c $(SRC_DIR)/esp32_port.c $(SRC_DIR)/platform/platform_linux.c $(SRC_DIR)/cyclone/common/os_port_posix.c
$(QUIET)$(CC) -I include -I src/cyclone/common -I src/cyclone/cyclone_tcp -I cyclone/common -I cyclone/cyclone_tcp -I cyclone/cyclone_ssl -I cyclone/cyclone_crypto -D GPL_LICENSE_TERMS_ACCEPTED -ffunction-sections -Wl,--gc-sections -o $(BIN_DIR)/test_c $(TEST_C_SRC) $(SRC_DIR)/os_ext.c $(SRC_DIR)/str_ext.c $(SRC_DIR)/fs_ext.c $(SRC_DIR)/esp32_port.c $(SRC_DIR)/platform/platform_linux.c $(SRC_DIR)/cyclone/common/os_port_posix.c $(SRC_DIR)/box_cert.c
$(QUIET)$(BIN_DIR)/test_c

.PHONY: test
Expand All @@ -820,7 +820,7 @@ test: test_c test_py test_auth_pool_reuse_with_server
PYTHON ?= python3
HARDENING_TESTS := Cache499 Multipart502 Firmware503 Encode501 Tap495 Crawler508 KeyPermissions512 Secrets505 Traversal513 Range515 Extract522 PrivateFetch Settings528
# every entry gets a fresh sandboxed server (see tests/py/with_server.sh), a crash must only fail its own test
PY_TESTS := tests/py/test_tonies_custom_json_api.py tests/py/test_cors_preflight.py tests/py/test_web_legacy_gone.py tests/py/test_box_sim.py tests/py/test_box_profiles.py tests/py/test_box_cert_upload.py tests/py/test_cloud_proxy.py tests/py/test_taf_length.py tests/py/test_sse_keepalive.py tests/py/test_sse_join.py \
PY_TESTS := tests/py/test_tonies_custom_json_api.py tests/py/test_cors_preflight.py tests/py/test_web_legacy_gone.py tests/py/test_box_sim.py tests/py/test_box_profiles.py tests/py/test_box_cert_check.py tests/py/test_box_cert_upload.py tests/py/test_cloud_proxy.py tests/py/test_taf_length.py tests/py/test_sse_keepalive.py tests/py/test_sse_join.py \
$(addprefix tests/py/test_hardening.py::,$(HARDENING_TESTS))

# make test_py TESTS=cors only entries containing "cors"
Expand Down
11 changes: 11 additions & 0 deletions include/box_cert.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#include <stdbool.h>

#define BOX_CERT_ID_SIZE 13

/* Issuer or subject of a box certificate: Boxine/tonies or TeddyCloud. */
bool box_cert_issuer_known(const char *issuer, const char *subject);

/* The box id (MAC) from the certificate subject "b'<mac>'" or "<mac>". */
bool box_cert_id(const char *subject, char id[BOX_CERT_ID_SIZE]);
10 changes: 10 additions & 0 deletions include/box_cert_check.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

#include <stdbool.h>

#include "settings.h"
#include "tls.h"

/* Checks the certificate of a box (core.boxCertAuth), pins it if needed and
* keeps the result in internal.boxCertStatus. False: refuse the box. */
bool box_cert_accepted(const TlsContext *tls, settings_t *settings);
3 changes: 3 additions & 0 deletions include/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ typedef struct
typedef struct
{
bool api_access;
char *certPin;
bool overrideCloud;
settings_box_generation boxGeneration;

Expand Down Expand Up @@ -304,6 +305,8 @@ typedef struct
time_t *last_ruid_time;
char *ip;
bool online;
char *boxCertStatus;
char *boxCertSha256;
} settings_internal_t;

typedef struct
Expand Down
4 changes: 4 additions & 0 deletions include/tls_adapter.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef __TLS_ADAPTER_H__
#define __TLS_ADAPTER_H__

#include <stdbool.h>
#include <stdint.h> // for uint8_t
#include "error.h"
#include "rng/yarrow.h" // for YarrowContext
Expand All @@ -16,4 +17,7 @@ extern YarrowContext yarrowContext;
void tls_context_key_log_init(TlsContext *context);
error_t load_cert(const char *dest_var, const char *src_file, const char *src_var, uint8_t settingsId);

/* SHA-256 (hex) and subject common name of a PEM certificate */
bool tls_cert_fingerprint(const char_t *pem, char_t sha256[65], char_t *subject, size_t subjectSize);

#endif
4 changes: 3 additions & 1 deletion include/tls_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
#define TLS_PRIVATE_CONTEXT \
char client_cert_issuer[128]; \
char client_cert_subject[32]; \
char client_cert_serial[64];
char client_cert_serial[64]; \
char client_cert_sha256[65]; \
bool_t client_cert_trusted;

// Desired trace level (for debugging purposes)
#define TLS_TRACE_LEVEL TRACE_LEVEL_WARNING
Expand Down
28 changes: 28 additions & 0 deletions src/box_cert.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "box_cert.h"

#include <string.h>

static bool contains(const char *text, const char *part)
{
return strstr(text, part) != NULL;
}

bool box_cert_issuer_known(const char *issuer, const char *subject)
{
return contains(issuer, "Boxine Factory SubCA") || contains(issuer, "Toniebox SubCA") ||
contains(issuer, "Toniebox Root CA") || contains(issuer, "TeddyCloud") || contains(subject, "TeddyCloud");
}

bool box_cert_id(const char *subject, char id[BOX_CERT_ID_SIZE])
{
size_t length = strlen(subject);
bool quoted = length == 15 && !strncmp(subject, "b'", 2) && subject[14] == '\'';

if (!quoted && length != 12)
{
return false;
}
memcpy(id, quoted ? &subject[2] : subject, 12);
id[12] = '\0';
return true;
}
68 changes: 68 additions & 0 deletions src/box_cert_check.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include "box_cert_check.h"

#include "box_cert.h"
#include "debug.h"
#include "os_port.h"
#include "tls_adapter.h"

/* The fingerprint of the client.der uploaded for this box, if it is the certificate of this box. */
static bool uploaded_cert_pin(settings_t *settings, char_t pin[65])
{
char_t subject[32];
char_t id[BOX_CERT_ID_SIZE];

return tls_cert_fingerprint(settings->internal.client.crt, pin, subject, sizeof(subject)) &&
box_cert_id(subject, id) && osStrcasecmp(id, settings->commonName) == 0;
}

static void pin_box_cert(settings_t *settings, const char_t *pin)
{
TRACE_INFO("Box %s: certificate pinned, SHA256 %s\r\n", settings->commonName, pin);
settings_set_string_id("toniebox.certPin", pin, settings->internal.overlayNumber);
settings_save();
}

/* Only called for boxes TeddyCloud knows or accepts (core.allowNewBox). The uploaded
* client.der of the box is its real certificate; without one, the first certificate
* the box presents is pinned, also a trusted one, so no other certificate takes its place. */
static const char *box_cert_status(const TlsContext *tls, settings_t *settings)
{
char_t uploaded[65];

if (uploaded_cert_pin(settings, uploaded))
{
if (osStrcasecmp(uploaded, settings->toniebox.certPin) != 0)
{
pin_box_cert(settings, uploaded);
}
}
else if (osStrlen(settings->toniebox.certPin) == 0)
{
pin_box_cert(settings, tls->client_cert_sha256);
}
if (tls->client_cert_trusted)
{
return "trusted";
}
return osStrcasecmp(settings->toniebox.certPin, tls->client_cert_sha256) == 0 ? "pinned" : "mismatch";
}

bool box_cert_accepted(const TlsContext *tls, settings_t *settings)
{
const char *status = box_cert_status(tls, settings);
bool verified = osStrcmp(status, "trusted") == 0 || osStrcmp(status, "pinned") == 0;

if (osStrcmp(settings->internal.boxCertStatus, status) != 0)
{
if (!verified)
{
TRACE_WARNING("Box %s: certificate %s, SHA256 %s\r\n", settings->commonName, status, tls->client_cert_sha256);
}
settings_set_string_id("internal.boxCertStatus", status, settings->internal.overlayNumber);
}
if (osStrcmp(settings->internal.boxCertSha256, tls->client_cert_sha256) != 0)
{
settings_set_string_id("internal.boxCertSha256", tls->client_cert_sha256, settings->internal.overlayNumber);
}
return verified || !settings->core.boxCertAuth;
}
89 changes: 53 additions & 36 deletions src/mqtt_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include "debug.h"
#include "settings.h"
#include "mqtt_server.h"
#include "box_cert.h"
#include "box_cert_check.h"
#include "tls.h"
#include "rand.h"
#include "tls_adapter.h"
Expand Down Expand Up @@ -411,52 +413,67 @@ static void mqtt_server_publish(client_ctx_t *client_ctx, const char *topic, con
}
}

static void mqtt_connection_update_context_from_cert(MqttClientConnection *conn)
/* False when a box presents a certificate the box certificate check refuses. An unknown
* box only counts as a box while core.allowNewBox is on, like on the box port. */
static bool mqtt_connection_update_context_from_cert(MqttClientConnection *conn)
{
if (conn->tlsContext != NULL && osStrlen(conn->tlsContext->client_cert_subject) > 0)
if (conn->tlsContext == NULL)
{
char_t *subject = conn->tlsContext->client_cert_subject;
char_t *issuer = conn->tlsContext->client_cert_issuer;
return true;
}

if (osStrstr(issuer, "Boxine Factory SubCA") != NULL || osStrstr(issuer, "Toniebox SubCA") != NULL
|| osStrstr(issuer, "TeddyCloud") != NULL || osStrstr(subject, "TeddyCloud") != NULL || osStrstr(issuer, "Toniebox Root CA") != NULL)
{
char_t *commonName = NULL;
if (osStrlen(subject) == 15 && !osStrncmp(subject, "b'", 2) && subject[14] == '\'') // tonies standard cn with b'[MAC]'
{
commonName = strdup(&subject[2]);
commonName[osStrlen(commonName) - 1] = '\0';
} else if (osStrlen(subject) == 12) {
commonName = strdup(subject);
}
char_t *subject = conn->tlsContext->client_cert_subject;
char_t *issuer = conn->tlsContext->client_cert_issuer;
char_t boxId[BOX_CERT_ID_SIZE];

if (commonName != NULL) {
settings_t *box_settings = get_settings_cn(commonName);
if (box_settings != NULL && box_settings->internal.config_used)
{
conn->client_ctx.settings = box_settings;
conn->client_ctx.settingsNoOverlay = box_settings;
conn->client_ctx.state = get_toniebox_state_id(box_settings->internal.overlayNumber);
conn->client_ctx.state->box.id = conn->client_ctx.settings->commonName;
conn->client_ctx.state->box.name = conn->client_ctx.settings->boxName;
}
osFreeMem(commonName);
}
}
if (!box_cert_issuer_known(issuer, subject) || !box_cert_id(subject, boxId))
{
return true;
}

if (get_overlay_id(boxId) == 0 && !get_settings()->core.allowNewBox)
{
return true;
}
settings_t *box_settings = get_settings_cn(boxId);
if (box_settings == NULL || !box_settings->internal.config_used)
{
return true;
}
if (!box_cert_accepted(conn->tlsContext, box_settings))
{
return false;
}

conn->client_ctx.settings = box_settings;
conn->client_ctx.settingsNoOverlay = box_settings;
conn->client_ctx.state = get_toniebox_state_id(box_settings->internal.overlayNumber);
conn->client_ctx.state->box.id = conn->client_ctx.settings->commonName;
conn->client_ctx.state->box.name = conn->client_ctx.settings->boxName;
return true;
}

static error_t handle_mqtt_disconnect(MqttClientConnection *conn, MqttMessageType type, const char *topic, const uint8_t *payload, size_t payload_len);

static error_t handle_mqtt_connect(MqttClientConnection *conn, MqttMessageType type, const char *topic, const uint8_t *payload, size_t payload_len)
{
mqtt_connection_update_context_from_cert(conn);
TRACE_INFO("MQTT: connection established for %s\r\n", conn->client_ctx.settings->commonName);

uint8_t connack[] = {0x20, 0x02, 0x00, 0x00};
bool accepted = mqtt_connection_update_context_from_cert(conn);
if (accepted)
{
TRACE_INFO("MQTT: connection established for %s\r\n", conn->client_ctx.settings->commonName);
}

uint8_t connack[] = {0x20, 0x02, 0x00, accepted ? 0x00 : 0x05}; // 0x05: not authorized
size_t written = 0;
if (conn->tlsContext)
return tlsWrite(conn->tlsContext, connack, sizeof(connack), &written, 0);
else
return socketSend(conn->socket, connack, sizeof(connack), &written, 0);
error_t error = conn->tlsContext ? tlsWrite(conn->tlsContext, connack, sizeof(connack), &written, 0)
: socketSend(conn->socket, connack, sizeof(connack), &written, 0);
if (!accepted)
{
TRACE_WARNING("MQTT: box certificate refused\r\n");
handle_mqtt_disconnect(conn, MQTT_MSG_DISCONNECT, NULL, NULL, 0);
return ERROR_ACCESS_DENIED;
}
return error;
}

static error_t handle_mqtt_pingreq(MqttClientConnection *conn, MqttMessageType type, const char *topic, const uint8_t *payload, size_t payload_len)
Expand Down
47 changes: 17 additions & 30 deletions src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
#include "mqtt_server.h" // for mqtt_server_init, mqtt_server_task
#include "stdbool.h" // for true, bool, false
#include "tls.h" // for _TlsContext, tlsLoadCertificate
#include "box_cert.h" // for box_cert_id, box_cert_issuer_known
#include "box_cert_check.h" // for box_cert_accepted
#include "tls_adapter.h" // for tls_context_key_log_init, tlsCache
#include "toniebox_state.h" // for get_toniebox_state, get_toniebox_s...
#include "toniebox_state_type.h" // for toniebox_state_box_t, toniebox_sta...
Expand Down Expand Up @@ -341,6 +343,7 @@ error_t httpServerRequestCallback(HttpConnection *connection, const char_t *uri,
TRACE_DEBUG(" Issuer: '%s'\r\n", connection->tlsContext->client_cert_issuer);
TRACE_DEBUG(" Subject: '%s'\r\n", connection->tlsContext->client_cert_subject);
TRACE_DEBUG(" Serial: '%s'\r\n", connection->tlsContext->client_cert_serial);
TRACE_DEBUG(" SHA256: '%s'\r\n", connection->tlsContext->client_cert_sha256);
request_source = connection->tlsContext->client_cert_subject;
}
else
Expand All @@ -364,44 +367,28 @@ error_t httpServerRequestCallback(HttpConnection *connection, const char_t *uri,
char_t *issuer = connection->tlsContext->client_cert_issuer;
uint32_t boxGen = GENERATION_UNKNOWN;

if (osStrstr(issuer, "Boxine Factory SubCA") != NULL || osStrstr(issuer, "Toniebox SubCA") != NULL
|| osStrstr(issuer, "TeddyCloud") != NULL || osStrstr(subject, "TeddyCloud") != NULL || osStrstr(issuer, "Toniebox Root CA") != NULL)
if (box_cert_issuer_known(issuer, subject))
{
char_t *commonName = NULL;
if (osStrlen(subject) == 15 && !osStrncmp(subject, "b'", 2) && subject[14] == '\'') // tonies standard cn with b'[MAC]'
char_t boxId[BOX_CERT_ID_SIZE];
bool hasBoxId = box_cert_id(subject, boxId);
const char_t *commonName = hasBoxId ? boxId : subject;

if (hasBoxId && get_overlay_id(commonName) == 0)
{
commonName = strdup(&subject[2]);
commonName[osStrlen(commonName) - 1] = '\0';
} else if (osStrlen(subject) == 12) {
commonName = strdup(subject);
}
if (commonName != NULL) {
if (get_overlay_id(commonName) == 0)
if (client_ctx->settings->core.allowNewBox)
{
if (client_ctx->settings->core.allowNewBox)
{
TRACE_INFO("Added new client certificate with CN=%s\n", commonName);
}
else
{
TRACE_WARNING("Found unknown client certificate with CN=%s\n", commonName);
}
TRACE_INFO("Added new client certificate with CN=%s\n", commonName);
}
if (get_overlay_id(commonName) > 0 || client_ctx->settings->core.allowNewBox)
else
{
client_ctx->settings = get_settings_cn(commonName);
connection->private.authenticated = client_ctx->settings->toniebox.api_access;
TRACE_WARNING("Found unknown client certificate with CN=%s\n", commonName);
}
osFreeMem(commonName);
// TODO: CHECK THE CERTIFICATES FOR REAL!!!!!
}
else
if (get_overlay_id(commonName) > 0 || client_ctx->settings->core.allowNewBox)
{
if (get_overlay_id(subject) > 0 || client_ctx->settings->core.allowNewBox)
{
client_ctx->settings = get_settings_cn(subject);
connection->private.authenticated = client_ctx->settings->toniebox.api_access;
}
client_ctx->settings = get_settings_cn(commonName);
connection->private.authenticated = client_ctx->settings->toniebox.api_access &&
box_cert_accepted(connection->tlsContext, client_ctx->settings);
}
client_ctx->state = get_toniebox_state_id(client_ctx->settings->internal.overlayNumber);

Expand Down
3 changes: 3 additions & 0 deletions src/settings.c
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ static void option_map_init(uint8_t settingsId)
OPTION_INTERNAL_STRING("internal.toniebox_firmware.uaEsp32Firmware", &settings->internal.toniebox_firmware.uaEsp32Firmware, "", "Firmware version from User Agent (esp32)", LEVEL_NONE)
OPTION_INTERNAL_STRING("internal.toniebox_firmware.rtnlVersion", &settings->internal.toniebox_firmware.rtnlVersion, "", "Firmware version from RTNL", LEVEL_NONE)
OPTION_INTERNAL_STRING("internal.toniebox_firmware.rtnlFullVersion", &settings->internal.toniebox_firmware.rtnlFullVersion, "", "Firmware full version from RTNL", LEVEL_NONE)
OPTION_INTERNAL_STRING("internal.boxCertStatus", &settings->internal.boxCertStatus, "", "Result of the box certificate check", LEVEL_NONE)
OPTION_INTERNAL_STRING("internal.boxCertSha256", &settings->internal.boxCertSha256, "", "SHA-256 of the certificate the box presented last", LEVEL_NONE)
OPTION_INTERNAL_STRING("internal.toniebox_firmware.rtnlDetail", &settings->internal.toniebox_firmware.rtnlDetail, "", "Firmware detail information", LEVEL_NONE)
OPTION_INTERNAL_STRING("internal.toniebox_firmware.rtnlRegion", &settings->internal.toniebox_firmware.rtnlRegion, "", "Firmware region", LEVEL_NONE)
OPTION_INTERNAL_UNSIGNED("internal.toniebox_firmware.otaVersionSfx", &settings->internal.toniebox_firmware.otaVersionSfx, 0, 0, UINT64_MAX, " ota version", LEVEL_NONE)
Expand Down Expand Up @@ -379,6 +381,7 @@ static void option_map_init(uint8_t settingsId)

OPTION_TREE_DESC("toniebox", "Toniebox", LEVEL_BASIC)
OPTION_BOOL("toniebox.api_access", &settings->toniebox.api_access, TRUE, "API access", "Grant access to the API (default value for new boxes)", LEVEL_EXPERT)
OPTION_STRING("toniebox.certPin", &settings->toniebox.certPin, "", "Certificate pin", "SHA-256 of the certificate this box is known by. Empty: taken on the next connection, from the uploaded client.der of the box or from the box itself", LEVEL_EXPERT)
OPTION_BOOL("toniebox.overrideCloud", &settings->toniebox.overrideCloud, TRUE, "Override cloud settings", "Override tonies cloud settings for the toniebox with those set here", LEVEL_BASIC)
OPTION_UNSIGNED("toniebox.boxGeneration", &settings->toniebox.boxGeneration, GENERATION_UNKNOWN, GENERATION_UNKNOWN, GENERATION_TB2, "Box Generation", "Generation of the box (TB1/TB2)", LEVEL_EXPERT)
// TB1 specific
Expand Down
Loading
Loading