Adapt WSS to new boards and use MQTT domain for eSIM connectivity
Preserve main GNSS and volume behavior while porting verified TLS, receive fixes and full-handshake optimizations. Add MQTT connection diagnostics and route MQTT through banban.api.talkingq.com:1883. Include migration documentation and regression tools. Firmware builds and package checks passed; user confirmed new-board connectivity after switching the MQTT host to the domain.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
"""Check actual ARMCC kernel / GCC APP TLS layouts and enabled features."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
output = root / 'artifacts/wss/new-board'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
toolchain = root / 'ql-cross-tool/win32/owtoolchain'
|
||||
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',
|
||||
'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', '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_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')
|
||||
env = os.environ.copy()
|
||||
env['ARMLMD_LICENSE_FILE'] = str(toolchain / 'license.dat')
|
||||
layouts, macros = {}, {}
|
||||
for target in ['kernel', 'app']:
|
||||
include = root / ('ql-kernel' if target == 'kernel' else 'ql-application') / 'threadx/common/include'
|
||||
obj = output / (target + '-tls-layout.o')
|
||||
flags = ['-I' + str(include), '-DQUECTEL_OPEN_CPU']
|
||||
if target == 'kernel':
|
||||
command = [str(armcc), '--cpu', 'Cortex-R5F', '--fpu=softvfp+vfpv3_d16',
|
||||
'--gnu', '--thumb', '-c', str(source), '-o', str(obj)] + flags
|
||||
section = '.constdata'
|
||||
else:
|
||||
command = [str(gcc), '-mcpu=cortex-r5', '-mthumb', '-mfloat-abi=softfp', '-mfpu=vfpv3-d16',
|
||||
'-c', str(source), '-o', str(obj)] + flags
|
||||
section = '.rodata'
|
||||
subprocess.run(command, env=env, check=True)
|
||||
binary = obj.with_suffix('.bin')
|
||||
subprocess.run([str(objcopy), '-O', 'binary', '-j', section, str(obj), str(binary)], check=True)
|
||||
data = binary.read_bytes()
|
||||
assert len(data) == len(labels) * 4, (target, len(data))
|
||||
layouts[target] = dict(zip(labels, struct.unpack('<' + 'I' * len(labels), data)))
|
||||
defines = subprocess.check_output([str(gcc), '-E', '-dM', '-x', 'c', '-include', 'mbedtls/config.h',
|
||||
'NUL'] + flags, text=True)
|
||||
macros[target] = sorted(line for line in defines.splitlines() if line.startswith('#define MBEDTLS_')
|
||||
or line.startswith('#define QUECTEL_MBEDTLS_'))
|
||||
assert layouts['kernel'] == layouts['app'], json.dumps(layouts, indent=2)
|
||||
assert macros['kernel'] == macros['app'], json.dumps({k: sorted(set(macros[k]) - set(macros['app' if k == 'kernel' else 'kernel'])) for k in macros})
|
||||
for feature in ['ECDSA_C', 'ECP_NIST_OPTIM', 'KEY_EXCHANGE_ECDHE_ECDSA_ENABLED', 'GCM_C', 'HAVE_TIME_DATE', 'PLATFORM_TIME_ALT']:
|
||||
assert any(line.startswith('#define MBEDTLS_' + feature + ' ') for line in macros['kernel'])
|
||||
report = {'result': 'PASS', 'layout': layouts['kernel'], 'features': macros['kernel']}
|
||||
(output / 'tls-abi.json').write_text(json.dumps(report, indent=2))
|
||||
print('PASS: ARMCC kernel / GCC APP TLS sizes, field offsets and feature macros agree')
|
||||
print(json.dumps(layouts['kernel'], indent=2))
|
||||
@@ -0,0 +1,59 @@
|
||||
param([string]$OutputDir = "$PSScriptRoot/../artifacts/wss", [ValidateSet(443, 8080)][int]$ServerPort = 443)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$serverHost = 'banban.api.talkingq.com'
|
||||
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
|
||||
$OutputDir = (Resolve-Path -LiteralPath $OutputDir).Path
|
||||
$tcp = New-Object System.Net.Sockets.TcpClient
|
||||
$tls = $null
|
||||
try {
|
||||
$pending = $tcp.BeginConnect($serverHost, $serverPort, $null, $null)
|
||||
if (-not $pending.AsyncWaitHandle.WaitOne(15000)) { throw 'TCP connect timeout' }
|
||||
$tcp.EndConnect($pending)
|
||||
$tcp.ReceiveTimeout = 15000
|
||||
$tcp.SendTimeout = 15000
|
||||
# Default SslStream validation checks the trusted chain and server hostname.
|
||||
$tls = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false)
|
||||
$tls.ReadTimeout = 15000
|
||||
$tls.WriteTimeout = 15000
|
||||
$tls.AuthenticateAsClient($serverHost, $null, [System.Security.Authentication.SslProtocols]::Tls12, $false)
|
||||
$leaf = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($tls.RemoteCertificate)
|
||||
$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
|
||||
$chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
|
||||
if (-not $chain.Build($leaf)) { throw 'Trusted certificate chain could not be built' }
|
||||
$certificates = @()
|
||||
for ($i = 0; $i -lt $chain.ChainElements.Count; $i++) {
|
||||
$cert = $chain.ChainElements[$i].Certificate
|
||||
$pem = "-----BEGIN CERTIFICATE-----`n" + [Convert]::ToBase64String($cert.RawData, [Base64FormattingOptions]::InsertLineBreaks).Replace("`r`n", "`n") + "`n-----END CERTIFICATE-----`n"
|
||||
[IO.File]::WriteAllText((Join-Path $OutputDir "chain-$i.pem"), $pem, [Text.Encoding]::ASCII)
|
||||
$certificates += [ordered]@{subject=$cert.Subject; issuer=$cert.Issuer; not_before=$cert.NotBefore.ToUniversalTime().ToString('o'); not_after=$cert.NotAfter.ToUniversalTime().ToString('o'); thumbprint=$cert.Thumbprint}
|
||||
if ($i -eq $chain.ChainElements.Count - 1) {
|
||||
if ($cert.Subject -ne $cert.Issuer) { throw 'Chain did not terminate at a root CA' }
|
||||
[IO.File]::WriteAllText((Join-Path $OutputDir 'root-ca.pem'), $pem, [Text.Encoding]::ASCII)
|
||||
}
|
||||
}
|
||||
$nonce = New-Object byte[] 16
|
||||
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
|
||||
$rng.GetBytes($nonce)
|
||||
$rng.Dispose()
|
||||
$key = [Convert]::ToBase64String($nonce)
|
||||
$request = "GET /ws HTTP/1.1`r`nHost: ${serverHost}:${serverPort}`r`nUpgrade: websocket`r`nConnection: Upgrade`r`nSec-WebSocket-Key: $key`r`nSec-WebSocket-Version: 13`r`n`r`n"
|
||||
$bytes = [Text.Encoding]::ASCII.GetBytes($request)
|
||||
$tls.Write($bytes, 0, $bytes.Length)
|
||||
$response = ''
|
||||
while (-not $response.EndsWith("`r`n`r`n") -and $response.Length -lt 16384) {
|
||||
$next = $tls.ReadByte()
|
||||
if ($next -lt 0) { break }
|
||||
$response += [char]$next
|
||||
}
|
||||
$sha1 = [Security.Cryptography.SHA1]::Create()
|
||||
$expected = [Convert]::ToBase64String($sha1.ComputeHash([Text.Encoding]::ASCII.GetBytes($key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')))
|
||||
$sha1.Dispose()
|
||||
$accepted = $response -match '^HTTP/1\.1 101 ' -and $response -match ('(?im)^Sec-WebSocket-Accept:\s*' + [regex]::Escape($expected) + '\s*$')
|
||||
$report = [ordered]@{url="wss://${serverHost}:${serverPort}/ws"; tested_utc=[DateTime]::UtcNow.ToString('o'); tls_protocol=$tls.SslProtocol.ToString(); cipher=$tls.CipherAlgorithm.ToString(); cipher_bits=$tls.CipherStrength; certificate_validation='System trust and hostname validation passed'; certificates=$certificates; websocket_upgrade=$accepted; response_headers=$response; device_auth_sent=$false}
|
||||
$report | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -LiteralPath (Join-Path $OutputDir 'server-probe.json')
|
||||
$report | ConvertTo-Json -Depth 6
|
||||
if (-not $accepted) { throw 'WebSocket upgrade did not return a valid HTTP 101 response' }
|
||||
} finally {
|
||||
if ($tls) { $tls.Dispose() }
|
||||
$tcp.Close()
|
||||
}
|
||||
@@ -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))
|
||||
@@ -0,0 +1,36 @@
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
|
||||
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'))
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
context.load_verify_locations(cadata=pem)
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
context.maximum_version = ssl.TLSVersion.TLSv1_2
|
||||
server = 'banban.api.talkingq.com'
|
||||
report = {'root_sha256': hashlib.sha256(ssl.PEM_cert_to_DER_cert(pem)).hexdigest(), 'trust': 'only the compiled root CA; no OS trust or intermediate download'}
|
||||
with socket.create_connection((server, 443), timeout=15) as tcp:
|
||||
with context.wrap_socket(tcp, server_hostname=server) as tls:
|
||||
report['tls'] = tls.version()
|
||||
report['cipher'] = tls.cipher()
|
||||
report['root_only_validation'] = 'PASS'
|
||||
try:
|
||||
with socket.create_connection((server, 443), timeout=15) as tcp:
|
||||
with context.wrap_socket(tcp, server_hostname='wrong.invalid'):
|
||||
raise AssertionError('wrong hostname accepted')
|
||||
except ssl.SSLCertVerificationError:
|
||||
report['wrong_hostname_rejected'] = True
|
||||
empty = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
try:
|
||||
with socket.create_connection((server, 443), timeout=15) as tcp:
|
||||
with empty.wrap_socket(tcp, server_hostname=server):
|
||||
raise AssertionError('untrusted CA accepted')
|
||||
except ssl.SSLCertVerificationError:
|
||||
report['untrusted_ca_rejected'] = True
|
||||
(root / 'artifacts/wss/root-only-probe.json').write_text(json.dumps(report, indent=2))
|
||||
print(json.dumps(report, indent=2))
|
||||
@@ -0,0 +1,169 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <stdarg.h>
|
||||
#include "mbedtls/x509_crt.h"
|
||||
#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"
|
||||
|
||||
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;
|
||||
void CPUartLogPrintf(const char *format, ...) { (void)format; }
|
||||
int mbedtls_snprintf(char *buffer, size_t size, const char *format, ...)
|
||||
{
|
||||
int result;
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
result = vsnprintf(buffer, size, format, args);
|
||||
va_end(args);
|
||||
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;
|
||||
mbedtls_gcm_context gcm;
|
||||
uint32_t flags;
|
||||
int ret, i;
|
||||
char path[1024];
|
||||
char pem[16384];
|
||||
FILE *file;
|
||||
size_t length;
|
||||
clock_t verify_started;
|
||||
mbedtls_ecp_group group;
|
||||
unsigned char key[16] = {0}, iv[12] = {0}, plain[16] = {0}, cipher[16], tag[16];
|
||||
const unsigned char expected_cipher[16] = {0x03,0x88,0xda,0xce,0x60,0xb6,0xa3,0x92,0xf3,0x28,0xc2,0xb9,0x71,0xb2,0xfe,0x78};
|
||||
const unsigned char expected_tag[16] = {0xab,0x6e,0x47,0xd4,0x2c,0xec,0x13,0xbd,0xf5,0x3a,0x67,0xb2,0x12,0x57,0xbd,0xdf};
|
||||
assert(argc == 2);
|
||||
test_time = time(NULL);
|
||||
mbedtls_x509_crt_init(&root);
|
||||
mbedtls_x509_crt_init(&chain);
|
||||
ret = mbedtls_x509_crt_parse(&root, (const unsigned char *)talkingq_root_ca, talkingq_root_ca_size);
|
||||
printf("SDK root CA parse: %d (previous device failure: -9774)\n", ret);
|
||||
assert(ret == 0);
|
||||
for (i = 0; i < 3; ++i) {
|
||||
snprintf(path, sizeof(path), "%s/chain-%d.pem", argv[1], i);
|
||||
file = fopen(path, "rb");
|
||||
assert(file != NULL);
|
||||
length = fread(pem, 1, sizeof(pem)-1, file);
|
||||
assert(feof(file) && !ferror(file));
|
||||
fclose(file);
|
||||
pem[length] = 0;
|
||||
ret = mbedtls_x509_crt_parse(&chain, (const unsigned char *)pem, length + 1);
|
||||
assert(ret == 0);
|
||||
}
|
||||
ret = mbedtls_x509_crt_verify(&chain, &root, NULL, "banban.api.talkingq.com", &flags, NULL, NULL, 0);
|
||||
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
|
||||
assert(group.modp != NULL);
|
||||
#else
|
||||
assert(group.modp == NULL);
|
||||
#endif
|
||||
mbedtls_ecp_group_free(&group);
|
||||
ret = mbedtls_x509_crt_verify(&chain, &root, NULL, "wrong.invalid", &flags, NULL, NULL, 0);
|
||||
assert(ret != 0 && (flags & MBEDTLS_X509_BADCERT_CN_MISMATCH));
|
||||
ret = mbedtls_x509_crt_verify(&chain, NULL, NULL, "banban.api.talkingq.com", &flags, NULL, NULL, 0);
|
||||
assert(ret != 0 && (flags & MBEDTLS_X509_BADCERT_NOT_TRUSTED));
|
||||
test_time = 1900000000; /* 2030: this server certificate is expired. */
|
||||
ret = mbedtls_x509_crt_verify(&chain, &root, NULL, "banban.api.talkingq.com", &flags, NULL, NULL, 0);
|
||||
assert(ret != 0 && (flags & MBEDTLS_X509_BADCERT_EXPIRED));
|
||||
test_time = 1700000000; /* 2023: this server certificate is not yet valid. */
|
||||
ret = mbedtls_x509_crt_verify(&chain, &root, NULL, "banban.api.talkingq.com", &flags, NULL, NULL, 0);
|
||||
assert(ret != 0 && (flags & MBEDTLS_X509_BADCERT_FUTURE));
|
||||
assert(mbedtls_ssl_ciphersuite_from_id(MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384) != NULL);
|
||||
mbedtls_gcm_init(&gcm);
|
||||
assert(mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 128) == 0);
|
||||
assert(mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, 16, iv, 12, NULL, 0,
|
||||
plain, cipher, 16, tag) == 0);
|
||||
assert(!memcmp(cipher, expected_cipher, 16) && !memcmp(tag, expected_tag, 16));
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Build the SDK's real crypto/X.509 sources with MSVC and test the server chain.
|
||||
Run from a Visual Studio developer shell. Chain PEMs come from the WSS probe.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import subprocess
|
||||
|
||||
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/new-board/host-cert'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
chain = Path(args.chain).resolve()
|
||||
sources = '''asn1parse asn1write bignum base64 pem oid pk pkparse pk_wrap rsa rsa_internal
|
||||
ecp ecp_curves ecdsa md md5 sha1 sha256 sha512 aes des gcm cipher cipher_wrap
|
||||
chacha20 chachapoly poly1305 constant_time ctr_drbg x509 x509_crt platform_util
|
||||
ecdh ssl_ciphersuites'''.split()
|
||||
command = ['cl', '/nologo', '/O2', '/Gy', '/TC', '/D_CRT_SECURE_NO_WARNINGS',
|
||||
'/DMBEDTLS_CONFIG_FILE="wss_tls_host_config.h"',
|
||||
'/I' + str(root / 'tools'), '/I' + str(root / 'tools/wss_host_stubs'),
|
||||
'/I' + str(root / 'ql-kernel/threadx/common/include'),
|
||||
'/I' + str(root / 'ql-application/threadx/interface/custom_app/inc'),
|
||||
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]
|
||||
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)
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Exercise actual MQTT diagnostic callbacks without a broker or credentials."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
output = root / 'artifacts/wss/new-board/mqtt-diag/tests'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
(output / 'MQTTClient.h').write_text(r'''
|
||||
#ifndef TEST_MQTT_CLIENT_H
|
||||
#define TEST_MQTT_CLIENT_H
|
||||
typedef struct Network Network;
|
||||
struct Network {
|
||||
int (*mqttread)(Network *, unsigned char *, int, int);
|
||||
int (*mqttwrite)(Network *, unsigned char *, int, int);
|
||||
};
|
||||
typedef struct { Network *ipstack; unsigned int command_timeout_ms; int isconnected; } MQTTClient;
|
||||
typedef struct { unsigned char MQTTVersion, cleansession; unsigned short keepAliveInterval; } MQTTPacket_connectData;
|
||||
typedef struct { unsigned char rc, sessionPresent; } MQTTConnackData;
|
||||
int MQTTConnectWithResults(MQTTClient *, MQTTPacket_connectData *, MQTTConnackData *);
|
||||
unsigned int ql_rtos_get_systicks(void);
|
||||
#endif
|
||||
''')
|
||||
(output / 'sockets.h').write_text('int ql_get_soc_errno(void);\n')
|
||||
harness = r'''
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "mqtt_connect_diag.h"
|
||||
static unsigned int ticks;
|
||||
static int mode, offset, writes, reads;
|
||||
unsigned int ql_rtos_get_systicks(void) { return ticks; }
|
||||
int ql_get_soc_errno(void) { return 104; }
|
||||
static int fake_write(Network *n, unsigned char *buf, int size, int timeout) {
|
||||
(void)n;
|
||||
assert(timeout == 30000 && size == 19);
|
||||
assert(memcmp(buf, "PRIVATE_TEST_SECRET", 19) == 0);
|
||||
++writes; ticks += 2;
|
||||
return mode == 2 ? -1 : size;
|
||||
}
|
||||
static int fake_read(Network *n, unsigned char *buf, int size, int timeout) {
|
||||
unsigned char response[] = {0x20, 2, 0, 0};
|
||||
(void)n;
|
||||
assert(timeout == 30000 && (size == 1 || size == 2));
|
||||
++reads; ticks += 3;
|
||||
if (mode == 3) return -1;
|
||||
if (mode == 4) return 0;
|
||||
if (mode == 1) response[3] = 5;
|
||||
if (mode == 5) response[0] = 0x30;
|
||||
if (mode == 7) { response[2] = 1; response[3] = 5; }
|
||||
memcpy(buf, response + offset, size); offset += size;
|
||||
return size;
|
||||
}
|
||||
int MQTTConnectWithResults(MQTTClient *client, MQTTPacket_connectData *options, MQTTConnackData *ack) {
|
||||
unsigned char packet[] = "PRIVATE_TEST_SECRET";
|
||||
unsigned char response[4] = {0};
|
||||
int result;
|
||||
assert(options->MQTTVersion == 3);
|
||||
assert(ack->rc == 255);
|
||||
if (mode == 6) return -1;
|
||||
result = client->ipstack->mqttwrite(client->ipstack, packet, 19, 30000);
|
||||
if (result != 19) { assert(result == -1); return -1; }
|
||||
/* SDK reads fixed header, remaining length, then the two-byte body. */
|
||||
result = client->ipstack->mqttread(client->ipstack, response, 1, 30000);
|
||||
if (mode == 3 || mode == 4) { assert(result == (mode == 3 ? -1 : 0)); return -1; }
|
||||
assert(result == 1);
|
||||
assert(client->ipstack->mqttread(client->ipstack, response + 1, 1, 30000) == 1);
|
||||
assert(client->ipstack->mqttread(client->ipstack, response + 2, 2, 30000) == 2);
|
||||
assert(response[1] == 2);
|
||||
return mode == 0 ? 0 : (mode == 1 ? 5 : -1);
|
||||
}
|
||||
int main(void) {
|
||||
Network network = {fake_read, fake_write};
|
||||
MQTTClient client = {&network, 30000, 0};
|
||||
MQTTPacket_connectData options = {3, 0, 60};
|
||||
for (mode = 0; mode < 8; ++mode) {
|
||||
int expected = mode == 0 ? 0 : (mode == 1 ? 5 : -1);
|
||||
ticks = mode == 0 ? 0xfffffffeU : 0; offset = writes = reads = 0;
|
||||
printf("CASE %d\n", mode);
|
||||
assert(talkingq_mqtt_connect_diagnosed(&client, &options) == expected);
|
||||
assert(network.mqttread == fake_read && network.mqttwrite == fake_write);
|
||||
assert(writes == (mode == 6 ? 0 : 1));
|
||||
if (mode == 6 || mode == 2) assert(reads == 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
(output / 'test.c').write_text(harness)
|
||||
include = root / 'ql-application/threadx/interface/custom_app/inc'
|
||||
source = root / 'ql-application/threadx/interface/custom_app/mqtt_connect_diag.c'
|
||||
subprocess.run(['cl', '/nologo', '/W4', '/WX', '/TC', '/I' + str(output), '/I' + str(include),
|
||||
str(source), str(output / 'test.c'), '/Fe' + str(output / 'test.exe')], cwd=output, check=True)
|
||||
result = subprocess.check_output([str(output / 'test.exe')], text=True)
|
||||
assert 'PRIVATE_TEST_SECRET' not in result
|
||||
cases = [part for part in result.split('CASE ')[1:]]
|
||||
assert len(cases) == 8
|
||||
assert 'CONNACK code=0 reason=accepted' in cases[0] and 'elapsed_ms=55' in cases[0]
|
||||
assert 'CONNACK code=5 reason=not_authorized' in cases[1]
|
||||
assert 'write request=19 rc=-1' in cases[2]
|
||||
assert 'read request=1 rc=-1' in cases[3]
|
||||
assert 'read request=1 rc=0' in cases[4]
|
||||
assert 'no valid initial CONNACK' in cases[5]
|
||||
assert 'failed before transport write' in cases[6]
|
||||
assert 'no valid initial CONNACK' in cases[7]
|
||||
for case in cases[2:]:
|
||||
assert '[MQTT-DIAG] CONNACK code=' not in case
|
||||
(output / 'test-output.log').write_text(result)
|
||||
print('PASS: transport arguments/return values, callback restoration, split CONNACK, rejection, invalid response, errors, no-I/O failure, tick wrap and credential redaction')
|
||||
@@ -0,0 +1,58 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "talkingq_wss.h"
|
||||
#include "rws_handshake.h"
|
||||
|
||||
static const char accept[] = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
|
||||
static const char good[] = "HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"uPgRaDe: WebSocket\r\nConnection: keep-alive, Upgrade\r\n"
|
||||
"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n";
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char host[64], path[128], data[RWS_HANDSHAKE_MAX + 20];
|
||||
int port, status;
|
||||
size_t i, header;
|
||||
const char *bad[] = {"ws://example.com/ws", "https://example.com/ws", "wss:///ws",
|
||||
"wss://user@example.com/ws", "wss://example.com:0/ws", "wss://example.com:65536/ws",
|
||||
"wss://example.com:-1/ws", "wss://example.com:/ws", "wss://example.com:abc/ws",
|
||||
"wss://example.com/ws#frag", "wss://example.com/ws\r\nInjected:yes",
|
||||
"wss://example.com:99999999999999999999999/ws", "wss://example.com/bad path"};
|
||||
assert(!talkingq_parse_wss_url(TALKINGQ_WSS_URL, host, sizeof(host), &port, path, sizeof(path)));
|
||||
assert(!strcmp(host, "banban.api.talkingq.com") && port == 443 && !strcmp(path, "/ws"));
|
||||
assert(!talkingq_parse_wss_url("wss://example.com", host, sizeof(host), &port, path, sizeof(path)));
|
||||
assert(port == 443 && !strcmp(path, "/ws"));
|
||||
assert(!talkingq_parse_wss_url("wss://example.com:8080/ws?x=y", host, sizeof(host), &port, path, sizeof(path)));
|
||||
assert(port == 8080 && !strcmp(path, "/ws?x=y"));
|
||||
for (i = 0; i < sizeof(bad)/sizeof(bad[0]); ++i)
|
||||
assert(talkingq_parse_wss_url(bad[i], host, sizeof(host), &port, path, sizeof(path)) == -1);
|
||||
assert(talkingq_parse_wss_url(TALKINGQ_WSS_URL, host, 3, &port, path, sizeof(path)) == -1);
|
||||
assert(talkingq_parse_wss_url(TALKINGQ_WSS_URL, host, sizeof(host), &port, path, 3) == -1);
|
||||
assert(talkingq_is_legacy_ws_url("ws://101.35.224.118:8080"));
|
||||
assert(talkingq_is_legacy_ws_url("ws://101.35.224.118:8080/ws"));
|
||||
assert(!talkingq_is_legacy_ws_url(TALKINGQ_WSS_URL));
|
||||
assert(!talkingq_is_legacy_ws_url("ws://custom.example:8080/ws"));
|
||||
/* Every possible TLS fragment boundary, with no NUL terminator available. */
|
||||
for (i = 0; i < sizeof(good)-1; ++i) {
|
||||
memset(data, 'X', sizeof(data));
|
||||
memcpy(data, good, i);
|
||||
assert(rws_check_handshake(data, i, accept, &header, &status) == 0);
|
||||
}
|
||||
memcpy(data, good, sizeof(good)-1);
|
||||
((unsigned char *)data)[sizeof(good)-1] = 0x81; data[sizeof(good)] = 0;
|
||||
assert(rws_check_handshake(data, sizeof(good)+1, accept, &header, &status) == 1);
|
||||
assert(status == 101 && header == sizeof(good)-1);
|
||||
assert(rws_check_handshake(data, sizeof(good)-1, "wrong", &header, &status) == -1);
|
||||
memcpy(data + 9, "404", 3);
|
||||
assert(rws_check_handshake(data, sizeof(good)-1, accept, &header, &status) == -1 && status == 404);
|
||||
strcpy(data, "HTTP/1.1 101 OK\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n");
|
||||
assert(rws_check_handshake(data, strlen(data), accept, &header, &status) == -1);
|
||||
memcpy(data, good, sizeof(good));
|
||||
strcpy(data + sizeof(good)-3, "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n");
|
||||
assert(rws_check_handshake(data, strlen(data), accept, &header, &status) == -1);
|
||||
memset(data, 'X', sizeof(data));
|
||||
assert(rws_check_handshake(data, sizeof(data), accept, &header, &status) == -1);
|
||||
puts("PASS: WSS URL policy, legacy migration, fragmented upgrade, status, accept, headers and limits");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@echo off
|
||||
setlocal
|
||||
cd /d "%~dp0.."
|
||||
if not exist artifacts\wss mkdir artifacts\wss
|
||||
where cl >nul 2>nul
|
||||
if errorlevel 1 call "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat" >nul
|
||||
if errorlevel 1 exit /b 1
|
||||
cl /nologo /W4 /WX /D_CRT_SECURE_NO_WARNINGS /TC /Iql-application\threadx\interface\custom_app\inc /Iql-application\threadx\interface\custom_app\lib\librws\inc tools\test_wss.c ql-application\threadx\interface\custom_app\talkingq_wss.c ql-application\threadx\interface\custom_app\lib\librws\src\rws_handshake.c /Foartifacts\wss\ /Feartifacts\wss\test_wss.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
artifacts\wss\test_wss.exe
|
||||
if errorlevel 1 exit /b 1
|
||||
ql-cross-tool\win32\owtoolchain\Python\python.exe tools\test_wss_io.py
|
||||
if errorlevel 1 exit /b 1
|
||||
ql-cross-tool\win32\owtoolchain\Python\python.exe tools\test_wss_receive.py
|
||||
if errorlevel 1 exit /b 1
|
||||
ql-cross-tool\win32\owtoolchain\Python\python.exe tools\test_wss_handshake.py
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Test the actual timed handshake loop with scripted SDK steps and 5 ms ticks."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = (root / 'ql-application/threadx/interface/custom_app/lib/librws/src/rws_socketpriv.c').read_bytes().decode('latin1')
|
||||
body = source[source.index('static const char *rws_tls_phase_name('):].split('int rws_ssl_conn(', 1)[0]
|
||||
harness = r'''
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include "mbedtls/ssl_internal.h"
|
||||
typedef struct { int ret, next_state; unsigned int duration; } step;
|
||||
static step steps[16];
|
||||
static int count, calls, sleeps;
|
||||
static unsigned int ticks;
|
||||
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; }
|
||||
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; }
|
||||
s = steps[calls++]; ticks += s.duration; ctx->state = s.next_state; return s.ret;
|
||||
}
|
||||
'''
|
||||
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};
|
||||
steps[2] = (step){0, MBEDTLS_SSL_SERVER_CERTIFICATE, 2};
|
||||
steps[3] = (step){0, MBEDTLS_SSL_CLIENT_KEY_EXCHANGE, 10};
|
||||
steps[4] = (step){MBEDTLS_ERR_SSL_WANT_WRITE, MBEDTLS_SSL_CLIENT_KEY_EXCHANGE, 1};
|
||||
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);
|
||||
assert(rws_tls_handshake_timed(&ctx) == MBEDTLS_ERR_SSL_TIMEOUT && ticks == 4000 && calls < 2100);
|
||||
reset(&ctx); ticks = (unsigned int)-1000;
|
||||
assert(rws_tls_handshake_timed(&ctx) == MBEDTLS_ERR_SSL_TIMEOUT && calls < 2100);
|
||||
reset(&ctx); steps[0] = (step){0, MBEDTLS_SSL_SERVER_CERTIFICATE, 4001}; count = 1;
|
||||
assert(rws_tls_handshake_timed(&ctx) == MBEDTLS_ERR_SSL_TIMEOUT && calls == 1 && sleeps == 0);
|
||||
reset(&ctx); ctx.state = MBEDTLS_SSL_HANDSHAKE_OVER;
|
||||
assert(rws_tls_handshake_timed(&ctx) == 0 && calls == 0);
|
||||
puts("PASS: timed SDK handshake loop: progress, WANT_READ/WRITE, no progress sleeps, fatal, deadline, tick wrap, complete state");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
output = root / 'artifacts/wss'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
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)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Compile actual SDK-independent function bodies with scripted TLS/RTOS stubs."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
lib = root / 'ql-application/threadx/interface/custom_app/lib/librws/src'
|
||||
private = (lib / 'rws_socketpriv.c').read_bytes().decode('latin1')
|
||||
public = (lib / 'rws_socketpub.c').read_bytes().decode('latin1')
|
||||
send = private[private.index('static int rws_tls_send_all('):].split('#endif', 1)[0]
|
||||
cancel = public[public.index('void rws_socket_disconnect_and_release('):].split('rws_bool rws_socket_send_text(', 1)[0]
|
||||
harness = r'''
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#define MBEDTLS_ERR_SSL_WANT_READ -2
|
||||
#define MBEDTLS_ERR_SSL_WANT_WRITE -3
|
||||
#define COMMAND_NONE 0
|
||||
#define COMMAND_DISCONNECT 6
|
||||
#define COMMAND_END 9999
|
||||
typedef struct {int ssl_ctx;} tls;
|
||||
typedef struct {tls *ssl; int work_mutex, is_connected, worker_started, work_thread, command;} _rws_socket;
|
||||
typedef _rws_socket *rws_socket;
|
||||
static int replies[16], calls, reply_count, expected_offset, freed, locks;
|
||||
static unsigned int ticks;
|
||||
static const unsigned char bytes[] = "abcdef";
|
||||
static unsigned int ql_rtos_get_systicks(void) { return ticks; }
|
||||
static void rws_thread_sleep(int ms) { ticks += (unsigned int)ms; }
|
||||
static int mbedtls_ssl_write(int *ctx, const unsigned char *data, size_t size) {
|
||||
int ret;
|
||||
(void)ctx;
|
||||
assert(data == bytes + expected_offset);
|
||||
assert(size == 6 - (size_t)expected_offset);
|
||||
ret = calls < reply_count ? replies[calls] : MBEDTLS_ERR_SSL_WANT_WRITE;
|
||||
++calls;
|
||||
if (ret > 0) expected_offset += ret;
|
||||
return ret;
|
||||
}
|
||||
static void rws_mutex_lock(int mutex) {(void)mutex; ++locks;}
|
||||
static void rws_mutex_unlock(int mutex) {(void)mutex; --locks;}
|
||||
static void rws_socket_delete(_rws_socket *s) {(void)s; assert(locks == 0); ++freed;}
|
||||
'''
|
||||
harness += send + cancel
|
||||
harness += r'''
|
||||
int main(void) {
|
||||
tls ctx = {0};
|
||||
_rws_socket s = {0};
|
||||
s.ssl = &ctx;
|
||||
replies[0] = -2; replies[1] = 2; replies[2] = -3; replies[3] = 4; reply_count = 4;
|
||||
assert(rws_tls_send_all(&s, bytes, 6) == 6 && calls == 4 && expected_offset == 6);
|
||||
calls = 0; expected_offset = 0; replies[0] = 2; replies[1] = -99; reply_count = 2;
|
||||
assert(rws_tls_send_all(&s, bytes, 6) == -1 && calls == 2);
|
||||
calls = 0; expected_offset = 0; replies[0] = 0; reply_count = 1;
|
||||
assert(rws_tls_send_all(&s, bytes, 6) == -1 && calls == 1);
|
||||
calls = 0; expected_offset = 0; reply_count = 0; ticks = 0;
|
||||
assert(rws_tls_send_all(&s, bytes, 6) == -1 && ticks >= 1000 && calls < 1000);
|
||||
calls = 0; ticks = (unsigned int)-500;
|
||||
assert(rws_tls_send_all(&s, bytes, 6) == -1 && calls < 1000);
|
||||
s.ssl = NULL; calls = 0;
|
||||
assert(rws_tls_send_all(&s, bytes, 6) == -1 && calls == 0);
|
||||
s.is_connected = 1; rws_socket_disconnect_and_release(&s);
|
||||
assert(s.command == COMMAND_DISCONNECT && locks == 0 && !freed);
|
||||
s.is_connected = 0; s.worker_started = 1; s.command = 1;
|
||||
rws_socket_disconnect_and_release(&s);
|
||||
assert(s.command == COMMAND_END && locks == 0 && !freed);
|
||||
s.worker_started = 0; s.command = COMMAND_NONE;
|
||||
rws_socket_disconnect_and_release(&s);
|
||||
assert(freed == 1 && locks == 0);
|
||||
rws_socket_disconnect_and_release(NULL);
|
||||
puts("PASS: TLS partial writes, WANT retry, fatal/EOF, timeout, tick wrap, null context, worker cancellation");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
output = root / 'artifacts/wss'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
cfile = output / 'test_wss_io.c'
|
||||
cfile.write_text(harness, encoding='utf-8')
|
||||
exe = output / 'test_wss_io.exe'
|
||||
subprocess.run(['cl', '/nologo', '/W4', '/WX', '/TC', str(cfile),
|
||||
'/Fo' + str(output / 'test_wss_io.obj'), '/Fe' + str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Exercise the real receive/upgrade loop with delayed TLS records."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
lib = root / 'ql-application/threadx/interface/custom_app/lib/librws'
|
||||
source = (lib / 'src/rws_socketpriv.c').read_bytes().decode('latin1')
|
||||
receive = source[source.index('rws_bool rws_socket_recv('):].split('/* RT-Thread Team add */', 1)[0]
|
||||
upgrade = source[source.index('void rws_socket_wait_handshake_responce('):].split('void rws_socket_send_disconnect(', 1)[0]
|
||||
harness = r'''
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "rws_handshake.h"
|
||||
#define LIBRWS_USING_MBED_TLS
|
||||
#define MBEDTLS_ERR_SSL_WANT_READ -2
|
||||
#define MBEDTLS_ERR_SSL_WANT_WRITE -3
|
||||
#define MBEDTLS_ERR_NET_RECV_FAILED -4
|
||||
#define WSAEWOULDBLOCK 11
|
||||
#define WSAEINPROGRESS 115
|
||||
#define RWS_RECEIVE_BUFF_SIZE 1024
|
||||
#define COMMAND_WAIT_HANDSHAKE_RESPONCE 3
|
||||
#define COMMAND_INFORM_CONNECTED 4
|
||||
#define COMMAND_INFORM_DISCONNECTED 5
|
||||
#define rws_error_code_read_write_socket 4
|
||||
#define rws_error_code_parse_handshake 5
|
||||
#define LOG_E(...) ((void)0)
|
||||
#define LOG_D(...) ((void)0)
|
||||
#define LOG_I(...) ((void)0)
|
||||
typedef int rws_bool;
|
||||
enum { rws_false, rws_true };
|
||||
typedef struct { int ssl_ctx; } tls;
|
||||
typedef struct {
|
||||
const char *scheme, *expected_accept;
|
||||
tls *ssl;
|
||||
char *payload_buff, *received;
|
||||
size_t received_size, received_len;
|
||||
int socket, command, is_connected;
|
||||
const char *error;
|
||||
} _rws_socket;
|
||||
static int replies[16], reply_count, calls, closes;
|
||||
static size_t offset;
|
||||
static unsigned int ticks;
|
||||
static const char response[] =
|
||||
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\nDATA";
|
||||
static unsigned int ql_rtos_get_systicks(void) { return ticks; }
|
||||
static void rws_thread_sleep(int ms) { ticks += (unsigned int)ms; }
|
||||
static void rws_error_delete_clean(const char **e) { *e = NULL; }
|
||||
static const char *rws_error_new_code_descr(int code, const char *text) { (void)code; return text; }
|
||||
static void rws_socket_close(_rws_socket *s) { (void)s; ++closes; }
|
||||
static void rws_socket_resize_received(_rws_socket *s, size_t size) {
|
||||
s->received = (char *)realloc(s->received, size);
|
||||
assert(s->received); s->received_size = size;
|
||||
}
|
||||
static int recv(int fd, char *buf, int size, int flags) {
|
||||
(void)fd; (void)buf; (void)size; (void)flags;
|
||||
assert(0 && "WSS must use TLS receive"); return -1;
|
||||
}
|
||||
static int mbedtls_ssl_read(int *ctx, unsigned char *buf, size_t size) {
|
||||
int ret = calls < reply_count ? replies[calls] : MBEDTLS_ERR_SSL_WANT_READ;
|
||||
(void)ctx; ++calls;
|
||||
if (ret > 0) {
|
||||
assert((size_t)ret <= size && offset + (size_t)ret <= sizeof(response) - 1);
|
||||
memcpy(buf, response + offset, (size_t)ret); offset += (size_t)ret;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
'''
|
||||
harness += receive + upgrade
|
||||
harness += r'''
|
||||
static void reset(_rws_socket *s, tls *ctx) {
|
||||
free(s->received); memset(s, 0, sizeof(*s));
|
||||
s->scheme = "wss"; s->ssl = ctx;
|
||||
s->expected_accept = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
|
||||
s->payload_buff = (char *)malloc(RWS_RECEIVE_BUFF_SIZE); assert(s->payload_buff);
|
||||
s->command = COMMAND_WAIT_HANDSHAKE_RESPONCE;
|
||||
calls = closes = reply_count = 0; offset = 0; ticks = 0;
|
||||
}
|
||||
int main(void) {
|
||||
tls ctx = {0}; _rws_socket s = {0}; int want;
|
||||
for (want = -2; want >= -3; --want) {
|
||||
reset(&s, &ctx); replies[0] = want; reply_count = 1;
|
||||
assert(!rws_socket_recv(&s));
|
||||
assert(!s.error && s.command == COMMAND_WAIT_HANDSHAKE_RESPONCE && closes == 0);
|
||||
replies[1] = 17; replies[2] = want; replies[3] = (int)sizeof(response) - 1 - 17;
|
||||
reply_count = 4;
|
||||
rws_socket_wait_handshake_responce(&s);
|
||||
assert(s.is_connected && !s.error && s.command == COMMAND_INFORM_CONNECTED);
|
||||
assert(calls == 4 && closes == 0 && s.received_len == 4);
|
||||
assert(!memcmp(s.received, "DATA", 4)); free(s.payload_buff);
|
||||
}
|
||||
reset(&s, &ctx);
|
||||
rws_socket_wait_handshake_responce(&s);
|
||||
assert(!s.is_connected && s.error && closes == 1 && ticks >= 2000 && calls < 300);
|
||||
free(s.payload_buff);
|
||||
reset(&s, &ctx); ticks = (unsigned int)-1000;
|
||||
rws_socket_wait_handshake_responce(&s);
|
||||
assert(!s.is_connected && s.error && closes == 1 && calls < 300); free(s.payload_buff);
|
||||
for (want = 0; want >= -99; want -= 99) {
|
||||
reset(&s, &ctx); replies[0] = want; reply_count = 1;
|
||||
rws_socket_wait_handshake_responce(&s);
|
||||
assert(!s.is_connected && s.error && closes == 1 && calls == 1);
|
||||
assert(s.command == COMMAND_INFORM_DISCONNECTED); free(s.payload_buff);
|
||||
}
|
||||
reset(&s, NULL); rws_socket_wait_handshake_responce(&s);
|
||||
assert(s.error && closes == 1 && calls == 0); free(s.payload_buff);
|
||||
reset(&s, &ctx); s.expected_accept = "wrong";
|
||||
replies[0] = (int)sizeof(response) - 1; reply_count = 1;
|
||||
rws_socket_wait_handshake_responce(&s);
|
||||
assert(!s.is_connected && s.error && closes == 1); free(s.payload_buff);
|
||||
free(s.received);
|
||||
puts("PASS: actual TLS receive/upgrade loop: WANT_READ/WRITE, split HTTP 101, trailing frame bytes, timeout/wrap, EOF/fatal, null TLS, invalid Accept");
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
output = root / 'artifacts/wss'
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
cfile = output / 'test_wss_receive.c'
|
||||
cfile.write_text(harness, encoding='utf-8')
|
||||
exe = output / 'test_wss_receive.exe'
|
||||
subprocess.run(['cl', '/nologo', '/W4', '/WX', '/wd4018', '/TC',
|
||||
'/I' + str(lib / 'inc'), str(cfile), str(lib / 'src/rws_handshake.c'),
|
||||
'/Fo' + str(output) + '/', '/Fe' + str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Verify the new-board WSS package, including enabled GNSS task entry points."""
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
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/new-board/EG800AKCN_11LG-new-board-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'
|
||||
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',
|
||||
'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')),
|
||||
str(root / 'ql-kernel/threadx/build/kernel.axf')], text=True)
|
||||
kernel_required = ['ecdsa_verify_restartable', 'mbedtls_ecdsa_read_signature',
|
||||
'mbedtls_gcm_crypt_and_tag', 'mbedtls_x509_time_is_future',
|
||||
'mbedtls_x509_time_is_past', 'mbedtls_platform_gmtime_r', 'mbedtls_time',
|
||||
'ecp_mod_p256', 'ecp_mod_p384', 'ecp_mod_p521']
|
||||
for symbol in kernel_required:
|
||||
assert re.search(r'\b' + symbol + r'$', kernel_symbols, re.M), 'Missing kernel capability: ' + symbol
|
||||
clock_body = subprocess.check_output([str(objdump), '-d', '--disassemble=x509_get_current_time',
|
||||
str(root / 'ql-kernel/threadx/build/kernel.axf')], text=True)
|
||||
assert '<mbedtls_platform_gmtime_r>' in clock_body and '<GetCurrentCompTime>' not in clock_body
|
||||
(output / 'x509-clock.disasm.txt').write_text(clock_body)
|
||||
report = {'package': package.name, 'sha256': hashlib.sha256(package.read_bytes()).hexdigest(),
|
||||
'board': 'new board with GNSS enabled',
|
||||
'linked_symbols': required, 'kernel_symbols': kernel_required,
|
||||
'hardware_test': 'New-board package awaits device testing; legacy board verified X25519 full handshake at 1455 ms on 2026-09-16',
|
||||
'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'
|
||||
for name, source in {'cp.bin': 'ql-kernel/threadx/build/release_kernel.bin',
|
||||
'customer_app.bin': 'ql-application/threadx/build/app.bin'}.items():
|
||||
data = archive.read(name)
|
||||
assert data == (root / source).read_bytes(), 'Stale image: ' + name
|
||||
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')
|
||||
assert b'Legacy board:' not in archive.read('customer_app.bin')
|
||||
assert b'Legacy board:' not in archive.read('cp.bin')
|
||||
assert b'gnss_Task_Ref' in archive.read('customer_app.bin')
|
||||
report['download_manifest'] = json.loads(archive.read('download.json'))
|
||||
task_create_address = int(re.search(r'^([0-9a-fA-F]+) T ql_rtos_task_create$', symbols, re.M).group(1), 16)
|
||||
for symbol, binary, expected in [
|
||||
('quec_get_gnssid_task', root / 'ql-kernel/threadx/build/kernel.axf', '<quec_rtos_task_create>'),
|
||||
('app_gnss_init', app, '0x%08x' % (task_create_address | 1)),
|
||||
('set_gnss_mode', app, 'strb')]:
|
||||
body = subprocess.check_output([str(objdump), '-d', '--disassemble=' + symbol, str(binary)], text=True)
|
||||
assert '<' + symbol + '>:' in body and expected in body, 'GNSS entry point disabled or unexpected code: ' + symbol
|
||||
(output / (symbol + '.disasm.txt')).write_text(body)
|
||||
for symbol, table in [('quec_get_ocname_by_gnss_task', kernel_symbols), ('quec_gnss_test', symbols)]:
|
||||
assert re.search(r'\b' + symbol + r'$', table, re.M), 'Missing GNSS worker: ' + symbol
|
||||
report['verification'] = 'PASS: ZIP CRC, current APP/kernel identity, GNSS task creation, WSS URL, ECDSA/GCM/RTC/NIST kernel symbols and handshake timing logs'
|
||||
(output / 'verification.json').write_text(json.dumps(report, indent=2))
|
||||
print(report['verification'])
|
||||
print('SHA256:', report['sha256'])
|
||||
@@ -0,0 +1 @@
|
||||
void CPUartLogPrintf(const char *format, ...);
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Host adapter: crypto/X.509 features are the firmware's shared profile.
|
||||
* Only CPU assembly and the OS allocator/clock implementation differ. */
|
||||
#include "../ql-config/config/talkingq_mbedtls_config.h"
|
||||
#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
|
||||
#include "mbedtls/check_config.h"
|
||||
Reference in New Issue
Block a user