Prefer X25519 for legacy WSS full handshakes
Prioritize X25519 while retaining enabled curve fallbacks and log the negotiated ECDHE group. Extend ABI checks, crypto benchmarks and backend group probes, and document the updated APP package.
This commit is contained in:
@@ -13,14 +13,16 @@ gcc = toolchain / 'gcc-arm-none-eabi/bin/arm-none-eabi-gcc.exe'
|
||||
objcopy = gcc.with_name('arm-none-eabi-objcopy.exe')
|
||||
armcc = toolchain / 'ARM_Compiler_5/bin/armcc.exe'
|
||||
types = ['ssl_context', 'ssl_config', 'net_context', 'x509_crt', 'pk_context',
|
||||
'entropy_context', 'ctr_drbg_context', 'ecp_group', 'ecp_point', 'mpi']
|
||||
'entropy_context', 'ctr_drbg_context', 'ecp_group', 'ecp_point', 'mpi',
|
||||
'ssl_handshake_params', 'ecdh_context']
|
||||
fields = {'ssl_context': ['conf', 'state', 'in_buf', 'out_buf', 'session', 'handshake', 'hostname'],
|
||||
'ssl_config': ['ca_chain', 'cert_profile', 'f_rng', 'p_rng'],
|
||||
'net_context': ['fd', 'cid'], 'x509_crt': ['pk', 'sig', 'next']}
|
||||
'ssl_config': ['ca_chain', 'cert_profile', 'f_rng', 'p_rng', 'curve_list'],
|
||||
'net_context': ['fd', 'cid'], 'x509_crt': ['pk', 'sig', 'next'],
|
||||
'ssl_handshake_params': ['ecdh_ctx'], 'ecdh_context': ['grp_id']}
|
||||
labels = ['sizeof(mbedtls_' + t + ')' for t in types]
|
||||
labels += ['offsetof(mbedtls_' + t + ', ' + f + ')' for t, fs in fields.items() for f in fs]
|
||||
source = output / 'tls_layout.c'
|
||||
source.write_text('#include <stddef.h>\n#include "mbedtls/ssl.h"\n'
|
||||
source.write_text('#include <stddef.h>\n#include "mbedtls/ssl_internal.h"\n'
|
||||
'#include "mbedtls/net_sockets.h"\n#include "mbedtls/entropy.h"\n'
|
||||
'#include "mbedtls/ctr_drbg.h"\n'
|
||||
'const unsigned int talkingq_tls_layout[] = {\n' + ',\n'.join(labels) + '\n};\n')
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Probe TLS 1.2 full-handshake group negotiation with the firmware root CA."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = (root / 'ql-application/threadx/interface/custom_app/talkingq_root_ca.c').read_text()
|
||||
pem = ''.join(json.loads('"' + value + '"') for value in re.findall(r'"(.*?)"', source)
|
||||
if 'CERTIFICATE' in value or value.endswith(r'\n'))
|
||||
output = root / 'artifacts/wss/full-handshake'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
ca = output / 'compiled-root.pem'
|
||||
ca.write_text(pem)
|
||||
openssl = Path('C:/Program Files/Git/usr/bin/openssl.exe')
|
||||
groups = {
|
||||
'sdk_order': 'P-521:brainpoolP512r1:P-384:brainpoolP384r1:P-256:brainpoolP256r1:P-224:secp224k1:P-192:secp192k1:X25519:X448',
|
||||
'x25519_first': 'X25519:P-256:P-384:P-521',
|
||||
'p256_first': 'P-256:X25519:P-384:P-521',
|
||||
}
|
||||
firmware = (root / 'ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpriv.c').read_bytes().decode('latin1')
|
||||
array = re.search(r'rws_tls_preferred_curves\[\]\s*=\s*\{(.*?)\}', firmware, re.S).group(1)
|
||||
names = {'CURVE25519': 'X25519', 'CURVE448': 'X448',
|
||||
'SECP256R1': 'P-256', 'SECP384R1': 'P-384', 'SECP521R1': 'P-521',
|
||||
'SECP224R1': 'P-224', 'SECP192R1': 'P-192',
|
||||
'SECP224K1': 'secp224k1', 'SECP192K1': 'secp192k1',
|
||||
'BP512R1': 'brainpoolP512r1', 'BP384R1': 'brainpoolP384r1', 'BP256R1': 'brainpoolP256r1'}
|
||||
groups['firmware_policy'] = ':'.join(names[name] for name in re.findall(r'MBEDTLS_ECP_DP_(\w+)', array) if name != 'NONE')
|
||||
report = {}
|
||||
for name, group_list in groups.items():
|
||||
result = subprocess.run([str(openssl), 's_client', '-connect', 'banban.api.talkingq.com:443',
|
||||
'-servername', 'banban.api.talkingq.com', '-verify_hostname', 'banban.api.talkingq.com',
|
||||
'-verify_return_error', '-CAfile', str(ca), '-no-CApath', '-no-CAstore',
|
||||
'-tls1_2', '-no_ticket', '-groups', group_list, '-brief'],
|
||||
input='', text=True, capture_output=True, timeout=20)
|
||||
text = result.stdout + result.stderr
|
||||
report[name] = {'groups': group_list, 'returncode': result.returncode, 'output': text}
|
||||
print(name + ':\n' + text, flush=True)
|
||||
assert result.returncode == 0 and 'Verification: OK' in text, name
|
||||
(output / 'group-probe.json').write_text(json.dumps(report, indent=2))
|
||||
@@ -8,10 +8,26 @@
|
||||
#include "mbedtls/platform_time.h"
|
||||
#include "mbedtls/gcm.h"
|
||||
#include "mbedtls/ssl_ciphersuites.h"
|
||||
#include "mbedtls/ecdh.h"
|
||||
#include "mbedtls/ctr_drbg.h"
|
||||
#include "talkingq_wss.h"
|
||||
|
||||
void *mbedtls_calloc(size_t n, size_t size) { return calloc(n, size); }
|
||||
void mbedtls_free(void *p) { free(p); }
|
||||
typedef union { size_t bytes; long double align; void *ptr; } alloc_header;
|
||||
static size_t live_bytes, peak_bytes;
|
||||
void *mbedtls_calloc(size_t n, size_t size) {
|
||||
alloc_header *h;
|
||||
size_t bytes;
|
||||
if (size && n > (SIZE_MAX - sizeof(*h)) / size) return NULL;
|
||||
bytes = n * size;
|
||||
h = (alloc_header *)calloc(1, sizeof(*h) + bytes);
|
||||
if (!h) return NULL;
|
||||
h->bytes = bytes; live_bytes += bytes;
|
||||
if (live_bytes > peak_bytes) peak_bytes = live_bytes;
|
||||
return h + 1;
|
||||
}
|
||||
void mbedtls_free(void *p) {
|
||||
if (p) { alloc_header *h = (alloc_header *)p - 1; live_bytes -= h->bytes; free(h); }
|
||||
}
|
||||
static time_t test_time;
|
||||
static time_t read_test_time(time_t *p) { if (p) *p = test_time; return test_time; }
|
||||
mbedtls_time_t (*mbedtls_time)(mbedtls_time_t *) = read_test_time;
|
||||
@@ -26,6 +42,50 @@ int mbedtls_snprintf(char *buffer, size_t size, const char *format, ...)
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Deterministic entropy is confined to this host benchmark, never firmware. */
|
||||
static int test_entropy(void *ctx, unsigned char *out, size_t n)
|
||||
{
|
||||
size_t i; unsigned int *counter = (unsigned int *)ctx;
|
||||
for (i = 0; i < n; ++i) out[i] = (unsigned char)(++*counter);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void benchmark_ecdh(mbedtls_ecp_group_id id, const char *name)
|
||||
{
|
||||
mbedtls_ecp_group peer, client;
|
||||
mbedtls_ecp_point peer_q, client_q;
|
||||
mbedtls_mpi peer_d, client_d, secret, expected;
|
||||
mbedtls_ctr_drbg_context rng;
|
||||
unsigned int counter = 0;
|
||||
clock_t elapsed = 0, started;
|
||||
size_t base;
|
||||
int i;
|
||||
mbedtls_ctr_drbg_init(&rng);
|
||||
assert(mbedtls_ctr_drbg_seed(&rng, test_entropy, &counter, NULL, 0) == 0);
|
||||
mbedtls_ecp_group_init(&peer); mbedtls_ecp_point_init(&peer_q); mbedtls_mpi_init(&peer_d);
|
||||
assert(mbedtls_ecp_group_load(&peer, id) == 0);
|
||||
assert(mbedtls_ecdh_gen_public(&peer, &peer_d, &peer_q, mbedtls_ctr_drbg_random, &rng) == 0);
|
||||
base = live_bytes; peak_bytes = base;
|
||||
for (i = 0; i < 20; ++i) {
|
||||
mbedtls_ecp_group_init(&client); mbedtls_ecp_point_init(&client_q);
|
||||
mbedtls_mpi_init(&client_d); mbedtls_mpi_init(&secret); mbedtls_mpi_init(&expected);
|
||||
assert(mbedtls_ecp_group_load(&client, id) == 0);
|
||||
started = clock();
|
||||
assert(mbedtls_ecdh_gen_public(&client, &client_d, &client_q, mbedtls_ctr_drbg_random, &rng) == 0);
|
||||
assert(mbedtls_ecdh_compute_shared(&client, &secret, &peer_q, &client_d, mbedtls_ctr_drbg_random, &rng) == 0);
|
||||
elapsed += clock() - started;
|
||||
assert(mbedtls_ecdh_compute_shared(&peer, &expected, &client_q, &peer_d, mbedtls_ctr_drbg_random, &rng) == 0);
|
||||
assert(mbedtls_mpi_cmp_mpi(&secret, &expected) == 0);
|
||||
mbedtls_mpi_free(&expected); mbedtls_mpi_free(&secret); mbedtls_mpi_free(&client_d);
|
||||
mbedtls_ecp_point_free(&client_q); mbedtls_ecp_group_free(&client);
|
||||
assert(live_bytes == base);
|
||||
}
|
||||
printf("Host fresh ECDH %s: %.2f ms, extra peak heap %zu bytes (20 runs, includes peer check heap)\n",
|
||||
name, 1000.0 * elapsed / CLOCKS_PER_SEC / 20, peak_bytes - base);
|
||||
mbedtls_ecp_group_free(&peer); mbedtls_ecp_point_free(&peer_q); mbedtls_mpi_free(&peer_d);
|
||||
mbedtls_ctr_drbg_free(&rng);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
mbedtls_x509_crt root, chain;
|
||||
@@ -63,12 +123,14 @@ int main(int argc, char **argv)
|
||||
printf("SDK server chain + hostname verify: %d, flags=0x%08x\n", ret, (unsigned int)flags);
|
||||
assert(ret == 0 && flags == 0);
|
||||
verify_started = clock();
|
||||
peak_bytes = live_bytes;
|
||||
for (i = 0; i < 20; ++i) {
|
||||
ret = mbedtls_x509_crt_verify(&chain, &root, NULL, "banban.api.talkingq.com", &flags, NULL, NULL, 0);
|
||||
assert(ret == 0 && flags == 0);
|
||||
}
|
||||
printf("Host certificate chain verify average: %.2f ms (20 runs)\n",
|
||||
1000.0 * (clock() - verify_started) / CLOCKS_PER_SEC / 20);
|
||||
printf("Host certificate verify peak heap: %zu bytes, ECP window: %d\n", peak_bytes, MBEDTLS_ECP_WINDOW_SIZE);
|
||||
mbedtls_ecp_group_init(&group);
|
||||
assert(mbedtls_ecp_group_load(&group, MBEDTLS_ECP_DP_SECP384R1) == 0);
|
||||
#ifdef MBEDTLS_ECP_NIST_OPTIM
|
||||
@@ -96,6 +158,12 @@ int main(int argc, char **argv)
|
||||
mbedtls_gcm_free(&gcm);
|
||||
mbedtls_x509_crt_free(&chain);
|
||||
mbedtls_x509_crt_free(&root);
|
||||
assert(live_bytes == 0);
|
||||
benchmark_ecdh(MBEDTLS_ECP_DP_CURVE25519, "X25519");
|
||||
benchmark_ecdh(MBEDTLS_ECP_DP_SECP256R1, "P-256");
|
||||
benchmark_ecdh(MBEDTLS_ECP_DP_SECP384R1, "P-384");
|
||||
benchmark_ecdh(MBEDTLS_ECP_DP_SECP521R1, "P-521");
|
||||
assert(live_bytes == 0);
|
||||
puts("PASS: SDK ECDSA certificate chain, hostname, trust, dates, TLS suite and AES-GCM vector");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ root = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('chain', nargs='?', default=str(root / 'artifacts/wss/retest-443'))
|
||||
parser.add_argument('--compare-nist', action='store_true')
|
||||
parser.add_argument('--compare-windows', action='store_true')
|
||||
args = parser.parse_args()
|
||||
output = root / 'artifacts/wss/tls-speed/host-cert'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
@@ -25,9 +26,12 @@ command = ['cl', '/nologo', '/O2', '/Gy', '/TC', '/D_CRT_SECURE_NO_WARNINGS',
|
||||
str(root / 'tools/test_mbedtls_cert.c'),
|
||||
str(root / 'ql-application/threadx/interface/custom_app/talkingq_root_ca.c')]
|
||||
command += [str(root / ('ql-kernel/threadx/quec_mbedtls/' + name + '.c')) for name in sources]
|
||||
for variant in (['baseline', 'nist'] if args.compare_nist else ['nist']):
|
||||
variants = ['window4', 'window5', 'window6'] if args.compare_windows else (['baseline', 'nist'] if args.compare_nist else ['nist'])
|
||||
for variant in variants:
|
||||
exe = output / ('test_mbedtls_cert_' + variant + '.exe')
|
||||
flags = ['/DWSS_TEST_DISABLE_NIST_OPTIM'] if variant == 'baseline' else []
|
||||
if variant.startswith('window'):
|
||||
flags += ['/DWSS_TEST_ECP_WINDOW=' + variant[-1]]
|
||||
subprocess.run(command + flags + ['/Fe' + str(exe), '/link', '/OPT:REF'], cwd=output, check=True)
|
||||
print('Certificate test variant:', variant, flush=True)
|
||||
subprocess.run([str(exe), str(chain)], check=True)
|
||||
|
||||
@@ -10,14 +10,7 @@ harness = r'''
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
enum { MBEDTLS_SSL_CLIENT_HELLO, MBEDTLS_SSL_SERVER_HELLO,
|
||||
MBEDTLS_SSL_SERVER_CERTIFICATE, MBEDTLS_SSL_SERVER_KEY_EXCHANGE,
|
||||
MBEDTLS_SSL_CLIENT_KEY_EXCHANGE, MBEDTLS_SSL_SERVER_FINISHED,
|
||||
MBEDTLS_SSL_HANDSHAKE_OVER };
|
||||
#define MBEDTLS_ERR_SSL_WANT_READ -2
|
||||
#define MBEDTLS_ERR_SSL_WANT_WRITE -3
|
||||
#define MBEDTLS_ERR_SSL_TIMEOUT -4
|
||||
typedef struct { int state; } mbedtls_ssl_context;
|
||||
#include "mbedtls/ssl_internal.h"
|
||||
typedef struct { int ret, next_state; unsigned int duration; } step;
|
||||
static step steps[16];
|
||||
static int count, calls, sleeps;
|
||||
@@ -26,7 +19,7 @@ static void test_log(const char *fmt, ...) { (void)fmt; }
|
||||
#define LOG_I test_log
|
||||
static unsigned int ql_rtos_get_systicks(void) { return ticks; }
|
||||
static void rws_thread_sleep(unsigned int ms) { ticks += ms / 5; ++sleeps; }
|
||||
static int mbedtls_ssl_handshake_step(mbedtls_ssl_context *ctx) {
|
||||
int mbedtls_ssl_handshake_step(mbedtls_ssl_context *ctx) {
|
||||
step s;
|
||||
assert(ctx->state != MBEDTLS_SSL_HANDSHAKE_OVER);
|
||||
if (calls >= count) { ++calls; return MBEDTLS_ERR_SSL_WANT_READ; }
|
||||
@@ -36,11 +29,13 @@ static int mbedtls_ssl_handshake_step(mbedtls_ssl_context *ctx) {
|
||||
harness += body
|
||||
harness += r'''
|
||||
static void reset(mbedtls_ssl_context *ctx) {
|
||||
memset(ctx, 0, sizeof(*ctx));
|
||||
ctx->state = MBEDTLS_SSL_CLIENT_HELLO;
|
||||
count = calls = sleeps = 0; ticks = 0; memset(steps, 0, sizeof(steps));
|
||||
}
|
||||
int main(void) {
|
||||
mbedtls_ssl_context ctx;
|
||||
mbedtls_ssl_handshake_params handshake = {0};
|
||||
reset(&ctx);
|
||||
steps[0] = (step){0, MBEDTLS_SSL_SERVER_HELLO, 1};
|
||||
steps[1] = (step){MBEDTLS_ERR_SSL_WANT_READ, MBEDTLS_SSL_SERVER_HELLO, 0};
|
||||
@@ -50,6 +45,11 @@ int main(void) {
|
||||
steps[5] = (step){0, MBEDTLS_SSL_SERVER_FINISHED, 5};
|
||||
steps[6] = (step){0, MBEDTLS_SSL_HANDSHAKE_OVER, 1}; count = 7;
|
||||
assert(rws_tls_handshake_timed(&ctx) == 0 && calls == 7 && sleeps == 2 && ticks == 24);
|
||||
reset(&ctx); ctx.handshake = &handshake; ctx.state = MBEDTLS_SSL_SERVER_KEY_EXCHANGE;
|
||||
handshake.ecdh_ctx.grp_id = MBEDTLS_ECP_DP_CURVE25519;
|
||||
steps[0] = (step){0, MBEDTLS_SSL_CLIENT_KEY_EXCHANGE, 1};
|
||||
steps[1] = (step){0, MBEDTLS_SSL_HANDSHAKE_OVER, 1}; count = 2;
|
||||
assert(rws_tls_handshake_timed(&ctx) == 0 && calls == 2 && sleeps == 0);
|
||||
reset(&ctx); steps[0] = (step){-99, MBEDTLS_SSL_SERVER_CERTIFICATE, 1}; count = 1;
|
||||
assert(rws_tls_handshake_timed(&ctx) == -99 && calls == 1 && sleeps == 0);
|
||||
reset(&ctx);
|
||||
@@ -70,5 +70,6 @@ cfile = output / 'test_wss_handshake.c'
|
||||
cfile.write_text(harness, encoding='utf-8')
|
||||
exe = output / 'test_wss_handshake.exe'
|
||||
subprocess.run(['cl', '/nologo', '/W4', '/WX', '/TC', str(cfile),
|
||||
'/I' + str(root / 'ql-application/threadx/common/include'),
|
||||
'/Fo' + str(output / 'test_wss_handshake.obj'), '/Fe' + str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
|
||||
@@ -8,7 +8,7 @@ import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
package = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else root / 'artifacts/wss/tls-speed/EG800AKCN_11LG-legacy-wss-fast.zip'
|
||||
package = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else root / 'artifacts/wss/full-handshake/EG800AKCN_11LG-legacy-wss-x25519.zip'
|
||||
output = package.parent
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
objdump = root / 'ql-cross-tool/win32/owtoolchain/gcc-arm-none-eabi/bin/arm-none-eabi-objdump.exe'
|
||||
@@ -16,7 +16,8 @@ app = root / 'ql-application/threadx/build/app.elf'
|
||||
symbols = subprocess.check_output([str(objdump.with_name('arm-none-eabi-nm.exe')), str(app)], text=True)
|
||||
required = ['talkingq_parse_wss_url', 'talkingq_root_ca', 'rws_check_handshake', 'rws_ssl_conn',
|
||||
'mbedtls_ssl_set_hostname', 'mbedtls_ssl_get_verify_result', 'mbedtls_ssl_conf_authmode',
|
||||
'mbedtls_ctr_drbg_random', 'mbedtls_sha1', 'mbedtls_base64_encode', 'mbedtls_ssl_handshake_step']
|
||||
'mbedtls_ctr_drbg_random', 'mbedtls_sha1', 'mbedtls_base64_encode', 'mbedtls_ssl_handshake_step',
|
||||
'rws_tls_preferred_curves']
|
||||
for symbol in required:
|
||||
assert re.search(r'\b' + symbol + r'$', symbols, re.M), 'Missing linked symbol: ' + symbol
|
||||
kernel_symbols = subprocess.check_output([str(objdump.with_name('arm-none-eabi-nm.exe')),
|
||||
@@ -33,7 +34,7 @@ assert '<mbedtls_platform_gmtime_r>' in clock_body and '<GetCurrentCompTime>' no
|
||||
(output / 'x509-clock.disasm.txt').write_text(clock_body)
|
||||
report = {'package': package.name, 'sha256': hashlib.sha256(package.read_bytes()).hexdigest(),
|
||||
'linked_symbols': required, 'kernel_symbols': kernel_required,
|
||||
'hardware_test': '2026-09-16 14:20 device log: TLS and HTTP 101 passed; NIST acceleration and handshake timings await device test',
|
||||
'hardware_test': '2026-09-16 14:38 device log: NIST TLS handshake 1820 ms and HTTP 101 passed; X25519 preference awaits device test',
|
||||
'backend': '2026-09-16 11:32 CST: WSS 443 /ws HTTP 101; 8080 plaintext WS'}
|
||||
with zipfile.ZipFile(package) as archive:
|
||||
assert archive.testzip() is None, 'ZIP CRC failure'
|
||||
@@ -44,6 +45,7 @@ with zipfile.ZipFile(package) as archive:
|
||||
report[name] = {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
|
||||
assert b'wss://banban.api.talkingq.com:443/ws' in archive.read('customer_app.bin')
|
||||
assert b'WSS TLS handshake total=' in archive.read('customer_app.bin')
|
||||
assert b'WSS TLS full handshake: prefer X25519' in archive.read('customer_app.bin')
|
||||
report['download_manifest'] = json.loads(archive.read('download.json'))
|
||||
printf_address = int(re.search(r'^([0-9a-fA-F]+) T printf$', symbols, re.M).group(1), 16)
|
||||
for symbol, binary, expected in [
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
#ifdef WSS_TEST_DISABLE_NIST_OPTIM
|
||||
#undef MBEDTLS_ECP_NIST_OPTIM
|
||||
#endif
|
||||
#ifdef WSS_TEST_ECP_WINDOW
|
||||
#undef MBEDTLS_ECP_WINDOW_SIZE
|
||||
#define MBEDTLS_ECP_WINDOW_SIZE WSS_TEST_ECP_WINDOW
|
||||
#endif
|
||||
#undef MBEDTLS_HAVE_ASM
|
||||
#undef MBEDTLS_FS_IO
|
||||
#define QUECTEL_MBEDTLS_SUPPORT
|
||||
|
||||
Reference in New Issue
Block a user