Add verified WSS transport for legacy boards

Configure the default WSS endpoint and ISRG Root X2 trust anchor, preserve device authentication, enforce TLS and WebSocket handshake validation, and handle partial I/O and connection cleanup. Add host tests, server probes, package verification and integration documentation. Backend 443 /ws still returns HTTP 404; on-device validation remains pending.
This commit is contained in:
chenjintao
2026-09-16 11:23:33 +08:00
parent 4b565958f9
commit 3e463f57b6
23 changed files with 809 additions and 396 deletions
+59
View File
@@ -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()
}
+36
View File
@@ -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))
+58
View File
@@ -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;
}
+13
View File
@@ -0,0 +1,13 @@
@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
exit /b %errorlevel%
+80
View File
@@ -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)
+46
View File
@@ -0,0 +1,46 @@
"""Verify the WSS flash package against the current legacy-board build."""
import hashlib
import json
import re
import subprocess
import zipfile
from pathlib import Path
root = Path(__file__).resolve().parents[1]
output = root / 'artifacts/wss'
package = output / 'EG800AKCN_11LG-legacy-wss.zip'
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']
for symbol in required:
assert re.search(r'\b' + symbol + r'$', symbols, re.M), 'Missing linked symbol: ' + symbol
report = {'package': package.name, 'sha256': hashlib.sha256(package.read_bytes()).hexdigest(),
'linked_symbols': required, 'hardware_test': 'Not performed',
'backend': '443 /ws returns HTTP 404; 8080 is 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')
report['download_manifest'] = json.loads(archive.read('download.json'))
with zipfile.ZipFile(root / 'artifacts/legacy-no-gnss/EG800AKCN_11LG-legacy-no-gnss.zip') as old:
with zipfile.ZipFile(package) as new:
assert old.read('cp.bin') == new.read('cp.bin'), 'Legacy kernel changed unexpectedly'
printf_address = int(re.search(r'^([0-9a-fA-F]+) T printf$', symbols, re.M).group(1), 16)
for symbol, binary, expected in [
('quec_get_gnssid_task', root / 'ql-kernel/threadx/build/kernel.axf', '<RTI_LOG>'),
('app_gnss_init', app, '0x%08x' % (printf_address | 1)),
('set_gnss_mode', app, 'bx\tlr')]:
body = subprocess.check_output([str(objdump), '-d', '--disassemble=' + symbol, str(binary)], text=True)
assert '<' + symbol + '>:' in body and expected in body and 'task_create' not in body
(output / (symbol + '.disasm.txt')).write_text(body)
report['verification'] = 'PASS: ZIP CRC, current APP/kernel identity, unchanged legacy kernel, GNSS bypass, WSS URL and TLS symbols'
(output / 'verification.json').write_text(json.dumps(report, indent=2))
print(report['verification'])
print('SHA256:', report['sha256'])