Commit8b1c4658Recorded7 Sep 2026Repositorysigil-tls

Add TLS handshake diagnostics and pin the TLS 1.3 stack

Changed
 README.md                    |  16 ++++++++++++++--
 RELEASE_NOTES_v0.16.7.md     |  16 ++++++++++++++++
 native/tls.c                 | 161 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
 package.sgl                  |   6 ++++--
 sigil.lock                   |   5 ++---
 src/sigil/tls.sgl            |  30 +++++++++++++++++++++++++++---
 test/fixtures/README.md      |  29 +++++++++++++++++++++++++++++
 test/fixtures/tls-client.c   |  58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test/fixtures/tls-matrix.cjs |  93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test/test-tls.sgl            |  31 +++++++++++++++++++++++++++++++
 10 files changed, 416 insertions(+), 29 deletions(-)
Diff
README.mdmodified
@@ -2,7 +2,7 @@
2
3
TLS/SSL client connections for [Sigil](https://codeberg.org/sigil/sigil).
4
−5
Secure TCP connections using mbedTLS. Supports TLS 1.2 client connections
+5
Secure TCP connections using mbedTLS. Supports TLS 1.2 and TLS 1.3 client connections
6
with system CA certificate verification, non-blocking I/O, and STARTTLS
7
upgrades of existing sockets.
8
@@ -10,7 +10,7 @@ upgrades of existing sockets.
10
11
| Module | Purpose |
12
|----------------|-----------------------------------------|
−13
| `(sigil tls)` | TLS 1.2 client connections |
+13
| `(sigil tls)` | TLS 1.2 and TLS 1.3 client connections |
14
15
## API summary
16
@@ -26,6 +26,18 @@ upgrades of existing sockets.
26
| `tls-connection?` | Whether a value is a TLS connection |
27
| `tls-set-non-blocking!` | Toggle non-blocking mode on the underlying socket |
28
+29
## Connection diagnostics
+30
+31
`tls-connect/details` and `tls-upgrade/details` accept the same arguments as
+32
their `/status` counterparts and return a dictionary. The existing connection
+33
and `/status` APIs retain their return shapes.
+34
+35
`status:` gives the stable outcome and `connection:` is the connection or
+36
`#f`. `error-code:`, `error-message:`, and `handshake-state:` describe a
+37
failed handshake. `protocol:` and `cipher:` identify a successful negotiation.
+38
Details belong to that attempt and survive later connections. Handshake state
+39
names are diagnostic information; use `status:` for application decisions.
+40
41
## System prerequisites
42
43
None beyond a working C toolchain. mbedTLS is consumed transitively via
RELEASE_NOTES_v0.16.7.mdadded
@@ -0,0 +1,16 @@
+1
# 0.16.7
+2
+3
TLS connections now negotiate TLS 1.3 when the server supports it, while
+4
remaining compatible with TLS 1.2-only servers. This requires sigil-crypto
+5
0.16.6 or later in the 0.16 series, with Mbed TLS 3.6.7 and PSA enabled.
+6
+7
New tls-connect/details and tls-upgrade/details procedures return per-attempt
+8
handshake errors, state, verification flags and negotiated protocol/cipher.
+9
The existing connection and /status APIs retain their argument and return
+10
shapes. Bounded handshakes keep their existing total deadlines.
+11
+12
Validation: 22 TLS tests and 100 crypto tests passed. Native Linux and Android
+13
fixtures each passed forty encrypted roundtrips per TLS version, certificate
+14
rejection, and reconnect after rejection. Production certificate verification
+15
was also exercised with TLS 1.3 clients on both systems. These checks do not
+16
establish a terminal-latency or CPU improvement.
native/tls.cmodified
@@ -4,7 +4,8 @@
4
* This file implements TLS/SSL connections using mbedTLS.
5
* Provides secure TCP connections for HTTPS and other TLS protocols.
6
*
−7
* Currently supports TLS 1.2. TLS 1.3 requires additional PSA Crypto setup.
+7
* Supports TLS 1.2 and TLS 1.3 with the shared sigil-crypto configuration.
+8
* mbedTLS initializes PSA as part of its TLS 1.3 handshake.
9
*
10
* Certificate Verification:
11
* - By default, certificates are verified against system CA certificates
@@ -102,6 +103,16 @@ typedef struct {
103
long handshake_deadline_ms;
104
} TlsConnectionData;
105
+106
/* Per-attempt diagnostics are copied before connection cleanup. No shared
+107
* last-error slot: another connection cannot overwrite this attempt's result. */
+108
typedef struct {
+109
int error_code;
+110
const char *handshake_state;
+111
const char *protocol;
+112
const char *cipher;
+113
uint32_t verify_flags;
+114
} TlsDiagnostics;
+115
116
/* Symbol used as type tag for TLS connection foreign objects */
117
static Value tls_connection_type_tag = SIGIL_UNDEFINED;
118
@@ -450,6 +461,59 @@ static int sigil_tls_bio_send(void *ctx, const unsigned char *buf, size_t len)
461
}
462
#endif /* !_WIN32 */
463
+464
static const char *tls_handshake_state_name(int state)
+465
{
+466
switch (state) {
+467
case MBEDTLS_SSL_HELLO_REQUEST: return "hello-request";
+468
case MBEDTLS_SSL_CLIENT_HELLO: return "client-hello";
+469
case MBEDTLS_SSL_SERVER_HELLO: return "server-hello";
+470
case MBEDTLS_SSL_SERVER_CERTIFICATE: return "server-certificate";
+471
case MBEDTLS_SSL_SERVER_KEY_EXCHANGE: return "server-key-exchange";
+472
case MBEDTLS_SSL_CERTIFICATE_REQUEST: return "certificate-request";
+473
case MBEDTLS_SSL_SERVER_HELLO_DONE: return "server-hello-done";
+474
case MBEDTLS_SSL_CLIENT_CERTIFICATE: return "client-certificate";
+475
case MBEDTLS_SSL_CLIENT_KEY_EXCHANGE: return "client-key-exchange";
+476
case MBEDTLS_SSL_CERTIFICATE_VERIFY: return "certificate-verify";
+477
case MBEDTLS_SSL_CLIENT_CHANGE_CIPHER_SPEC: return "client-change-cipher-spec";
+478
case MBEDTLS_SSL_CLIENT_FINISHED: return "client-finished";
+479
case MBEDTLS_SSL_SERVER_CHANGE_CIPHER_SPEC: return "server-change-cipher-spec";
+480
case MBEDTLS_SSL_SERVER_FINISHED: return "server-finished";
+481
case MBEDTLS_SSL_FLUSH_BUFFERS: return "flush-buffers";
+482
case MBEDTLS_SSL_HANDSHAKE_WRAPUP: return "handshake-wrapup";
+483
case MBEDTLS_SSL_NEW_SESSION_TICKET: return "new-session-ticket";
+484
case MBEDTLS_SSL_SERVER_HELLO_VERIFY_REQUEST_SENT: return "server-hello-verify-request-sent";
+485
case MBEDTLS_SSL_HELLO_RETRY_REQUEST: return "hello-retry-request";
+486
case MBEDTLS_SSL_ENCRYPTED_EXTENSIONS: return "encrypted-extensions";
+487
case MBEDTLS_SSL_END_OF_EARLY_DATA: return "end-of-early-data";
+488
case MBEDTLS_SSL_CLIENT_CERTIFICATE_VERIFY: return "client-certificate-verify";
+489
case MBEDTLS_SSL_CLIENT_CCS_AFTER_SERVER_FINISHED: return "client-ccs-after-server-finished";
+490
case MBEDTLS_SSL_CLIENT_CCS_BEFORE_2ND_CLIENT_HELLO: return "client-ccs-before-2nd-client-hello";
+491
case MBEDTLS_SSL_SERVER_CCS_AFTER_SERVER_HELLO: return "server-ccs-after-server-hello";
+492
case MBEDTLS_SSL_CLIENT_CCS_AFTER_CLIENT_HELLO: return "client-ccs-after-client-hello";
+493
case MBEDTLS_SSL_SERVER_CCS_AFTER_HELLO_RETRY_REQUEST: return "server-ccs-after-hello-retry-request";
+494
case MBEDTLS_SSL_HANDSHAKE_OVER: return "handshake-over";
+495
case MBEDTLS_SSL_TLS1_3_NEW_SESSION_TICKET: return "tls1-3-new-session-ticket";
+496
case MBEDTLS_SSL_TLS1_3_NEW_SESSION_TICKET_FLUSH: return "tls1-3-new-session-ticket-flush";
+497
default: return "unknown";
+498
}
+499
}
+500
+501
static int tls_handshake_result(TlsConnectionData *conn, int error,
+502
TlsDiagnostics *details)
+503
{
+504
if (details) {
+505
details->error_code = error;
+506
details->handshake_state = tls_handshake_state_name(conn->ssl.MBEDTLS_PRIVATE(state));
+507
details->verify_flags = mbedtls_ssl_get_verify_result(&conn->ssl);
+508
if (error == 0) {
+509
details->protocol = mbedtls_ssl_get_version(&conn->ssl);
+510
details->cipher = mbedtls_ssl_get_ciphersuite(&conn->ssl);
+511
}
+512
}
+513
if (error == 0) return SIGIL_TLS_HS_OK;
+514
return error == MBEDTLS_ERR_SSL_TIMEOUT ? SIGIL_TLS_HS_TIMEOUT : SIGIL_TLS_HS_FAILED;
+515
}
+516
517
/*
518
* Run the TLS handshake on an already-connected conn->server_fd.
519
*
@@ -474,14 +538,13 @@ static int sigil_tls_bio_send(void *ctx, const unsigned char *buf, size_t len)
538
* subsequent tls-read and tls-write behave exactly as before. Only the
539
* handshake is bounded here.
540
*/
−477
static int sigil_tls_run_handshake(TlsConnectionData *conn, long handshake_timeout_ms)
+541
static int sigil_tls_run_handshake(TlsConnectionData *conn, long handshake_timeout_ms,
+542
TlsDiagnostics *details)
543
{
544
int ret;
545
546
#ifndef _WIN32
547
if (handshake_timeout_ms > 0) {
−483
int outcome = SIGIL_TLS_HS_OK;
−484
548
conn->handshake_deadline_ms = sigil_tls_now_ms() + handshake_timeout_ms;
549
mbedtls_ssl_conf_read_timeout(&conn->conf, (uint32_t)handshake_timeout_ms);
550
mbedtls_ssl_set_bio(&conn->ssl, conn,
@@ -494,18 +557,16 @@ static int sigil_tls_run_handshake(TlsConnectionData *conn, long handshake_timeo
557
break;
558
}
559
if (ret == MBEDTLS_ERR_SSL_TIMEOUT) {
−497
outcome = SIGIL_TLS_HS_TIMEOUT;
560
break;
561
}
562
if (ret != MBEDTLS_ERR_SSL_WANT_READ &&
563
ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
−502
outcome = SIGIL_TLS_HS_FAILED;
564
break;
565
}
566
/* WANT_* on a blocking BIO means the deadline is the only thing
567
* that can end this; check it so the retry cannot spin. */
568
if (conn->handshake_deadline_ms - sigil_tls_now_ms() <= 0) {
−508
outcome = SIGIL_TLS_HS_TIMEOUT;
+569
ret = MBEDTLS_ERR_SSL_TIMEOUT;
570
break;
571
}
572
}
@@ -515,7 +576,7 @@ static int sigil_tls_run_handshake(TlsConnectionData *conn, long handshake_timeo
576
mbedtls_ssl_conf_read_timeout(&conn->conf, 0);
577
mbedtls_ssl_set_bio(&conn->ssl, &conn->server_fd,
578
mbedtls_net_send, mbedtls_net_recv, NULL);
−518
return outcome;
+579
return tls_handshake_result(conn, ret, details);
580
}
581
#else
582
(void)handshake_timeout_ms;
@@ -526,10 +587,10 @@ static int sigil_tls_run_handshake(TlsConnectionData *conn, long handshake_timeo
587
588
while ((ret = mbedtls_ssl_handshake(&conn->ssl)) != 0) {
589
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
−529
return SIGIL_TLS_HS_FAILED;
+590
return tls_handshake_result(conn, ret, details);
591
}
592
}
−532
return SIGIL_TLS_HS_OK;
+593
return tls_handshake_result(conn, 0, details);
594
}
595
596
/*
@@ -539,9 +600,9 @@ static int sigil_tls_run_handshake(TlsConnectionData *conn, long handshake_timeo
600
*/
601
static Value make_tls_status(SigilVM *vm, const char *status, Value conn)
602
{
+603
sigil__gc_push_temp_root(vm, conn);
604
Value status_val = sigil_make_string(vm, status, strlen(status));
605
sigil__gc_push_temp_root(vm, status_val);
−544
sigil__gc_push_temp_root(vm, conn);
606
Value result = sigil_cons(vm, status_val, conn);
607
sigil__gc_pop_temp_root(vm);
608
sigil__gc_pop_temp_root(vm);
@@ -557,7 +618,7 @@ static Value make_tls_status(SigilVM *vm, const char *status, Value conn)
618
* (which surfaces it) run this, so the two can never drift apart.
619
*/
620
static Value tls_connect_core(SigilVM *vm, int argc, Value *args,
−560
const char **status_out)
+621
const char **status_out, TlsDiagnostics *details)
622
{
623
*status_out = "tls-init-failed";
624
@@ -670,7 +731,7 @@ static Value tls_connect_core(SigilVM *vm, int argc, Value *args,
731
}
732
733
/* Perform TLS handshake (bounded when handshake_timeout_ms > 0). */
−673
ret = sigil_tls_run_handshake(conn, handshake_timeout_ms);
+734
ret = sigil_tls_run_handshake(conn, handshake_timeout_ms, details);
735
if (ret == SIGIL_TLS_HS_TIMEOUT) {
736
*status_out = "handshake-timeout";
737
goto cleanup_error;
@@ -740,7 +801,7 @@ static Value native_tls_connect(SigilVM *vm, int argc, Value *args)
801
if (tls_connect_check_args(vm, args, "tls-connect") < 0) {
802
return SIGIL_UNDEFINED;
803
}
−743
return tls_connect_core(vm, argc, args, &status);
+804
return tls_connect_core(vm, argc, args, &status, NULL);
805
}
806
807
/*
@@ -766,10 +827,58 @@ static Value native_tls_connect_status(SigilVM *vm, int argc, Value *args)
827
return SIGIL_UNDEFINED;
828
}
829
−769
Value conn = tls_connect_core(vm, argc, args, &status);
+830
Value conn = tls_connect_core(vm, argc, args, &status, NULL);
831
return make_tls_status(vm, status, conn);
832
}
833
+834
/* Values are rooted while subsequent strings/keywords allocate. */
+835
static Value tls_detail_string(SigilVM *vm, const char *text)
+836
{
+837
return text ? sigil_make_string(vm, text, strlen(text)) : SIGIL_FALSE;
+838
}
+839
+840
static Value make_tls_details(SigilVM *vm, const char *status, Value conn,
+841
const TlsDiagnostics *details)
+842
{
+843
char message[256];
+844
if (details->error_code != 0)
+845
mbedtls_strerror(details->error_code, message, sizeof(message));
+846
const char *names[] = { "status", "connection", "error-code", "error-message",
+847
"handshake-state", "protocol", "cipher", "verify-flags" };
+848
Value pairs[16];
+849
sigil__gc_push_temp_root(vm, conn);
+850
for (int i = 0; i < 8; i++) {
+851
pairs[i * 2] = sigil_intern_keyword(vm, names[i], strlen(names[i]));
+852
sigil__gc_push_temp_root(vm, pairs[i * 2]);
+853
Value value;
+854
switch (i) {
+855
case 0: value = tls_detail_string(vm, status); break;
+856
case 1: value = conn; break;
+857
case 2: value = sigil_fixnum(details->error_code); break;
+858
case 3: value = tls_detail_string(vm, details->error_code ? message : NULL); break;
+859
case 4: value = tls_detail_string(vm, details->handshake_state); break;
+860
case 5: value = tls_detail_string(vm, details->protocol); break;
+861
case 6: value = tls_detail_string(vm, details->cipher); break;
+862
default: value = sigil_fixnum(details->verify_flags); break;
+863
}
+864
pairs[i * 2 + 1] = value;
+865
sigil__gc_push_temp_root(vm, value);
+866
}
+867
Value result = sigil_dict_create(vm, 8, pairs);
+868
for (int i = 0; i < 17; i++) sigil__gc_pop_temp_root(vm);
+869
return result;
+870
}
+871
+872
static Value native_tls_connect_details(SigilVM *vm, int argc, Value *args)
+873
{
+874
const char *status = "tls-init-failed";
+875
TlsDiagnostics details = {0};
+876
if (tls_connect_check_args(vm, args, "tls-connect/details") < 0)
+877
return SIGIL_UNDEFINED;
+878
Value conn = tls_connect_core(vm, argc, args, &status, &details);
+879
return make_tls_details(vm, status, conn, &details);
+880
}
+881
882
/*
883
* tls-read connection [max-bytes] -> string | #f | eof-object
884
* Read data from TLS connection.
@@ -1066,7 +1175,7 @@ static Value native_tls_set_non_blocking(SigilVM *vm, int argc, Value *args)
1175
* arguments have already been validated.
1176
*/
1177
static Value tls_upgrade_core(SigilVM *vm, int argc, Value *args,
−1069
const char **status_out)
+1178
const char **status_out, TlsDiagnostics *details)
1179
{
1180
*status_out = "tls-init-failed";
1181
@@ -1164,7 +1273,7 @@ static Value tls_upgrade_core(SigilVM *vm, int argc, Value *args,
1273
* handshake_timeout_ms > 0). An upgraded socket is ALREADY connected,
1274
* so every byte of its handshake sits past the connect phase — the
1275
* unbounded window here is strictly wider than tls-connect's. */
−1167
ret = sigil_tls_run_handshake(conn, handshake_timeout_ms);
+1276
ret = sigil_tls_run_handshake(conn, handshake_timeout_ms, details);
1277
if (ret == SIGIL_TLS_HS_TIMEOUT) {
1278
*status_out = "handshake-timeout";
1279
goto cleanup_error;
@@ -1243,7 +1352,7 @@ static Value native_tls_upgrade(SigilVM *vm, int argc, Value *args)
1352
if (tls_upgrade_check_args(vm, args, "tls-upgrade") < 0) {
1353
return SIGIL_UNDEFINED;
1354
}
−1246
return tls_upgrade_core(vm, argc, args, &status);
+1355
return tls_upgrade_core(vm, argc, args, &status, NULL);
1356
}
1357
1358
/*
@@ -1261,10 +1370,20 @@ static Value native_tls_upgrade_status(SigilVM *vm, int argc, Value *args)
1370
return SIGIL_UNDEFINED;
1371
}
1372
−1264
Value conn = tls_upgrade_core(vm, argc, args, &status);
+1373
Value conn = tls_upgrade_core(vm, argc, args, &status, NULL);
1374
return make_tls_status(vm, status, conn);
1375
}
1376
+1377
static Value native_tls_upgrade_details(SigilVM *vm, int argc, Value *args)
+1378
{
+1379
const char *status = "tls-init-failed";
+1380
TlsDiagnostics details = {0};
+1381
if (tls_upgrade_check_args(vm, args, "tls-upgrade/details") < 0)
+1382
return SIGIL_UNDEFINED;
+1383
Value conn = tls_upgrade_core(vm, argc, args, &status, &details);
+1384
return make_tls_details(vm, status, conn, &details);
+1385
}
+1386
1387
/*
1388
* Hook implementations for TLS integration with socket layer.
1389
* These are registered with sigil-lib during module init.
@@ -1312,6 +1431,8 @@ void sigil__init_sigil_tls_module(SigilVM *vm)
1431
SIGIL_ARITY_EXACT(1), "Check if value is a TLS connection");
1432
1433
/* Connection operations */
+1434
REGISTER_AND_EXPORT("tls-connect/details", native_tls_connect_details,
+1435
SIGIL_ARITY_RANGE(2, 4), "Connect to TLS with per-attempt diagnostics");
1436
REGISTER_AND_EXPORT("tls-connect", native_tls_connect,
1437
SIGIL_ARITY_RANGE(2, 4), "Connect to TLS server");
1438
REGISTER_AND_EXPORT("tls-connect/status", native_tls_connect_status,
@@ -1331,6 +1452,8 @@ void sigil__init_sigil_tls_module(SigilVM *vm)
1452
SIGIL_ARITY_RANGE(1, 2), "Set TLS connection to non-blocking mode");
1453
1454
/* STARTTLS support */
+1455
REGISTER_AND_EXPORT("tls-upgrade/details", native_tls_upgrade_details,
+1456
SIGIL_ARITY_RANGE(2, 3), "Upgrade to TLS with per-attempt diagnostics");
1457
REGISTER_AND_EXPORT("tls-upgrade", native_tls_upgrade,
1458
SIGIL_ARITY_RANGE(2, 3), "Upgrade existing TCP socket to TLS connection");
1459
REGISTER_AND_EXPORT("tls-upgrade/status", native_tls_upgrade_status,
package.sglmodified
@@ -9,7 +9,7 @@
9
10
(package
11
name: "sigil-tls"
−12
version: "0.16.6"
+12
version: "0.16.7"
13
sigil: "0.20"
14
description: "TLS/SSL connections for Sigil"
15
url: "https://codeberg.org/sigil/sigil-tls"
@@ -29,7 +29,9 @@
29
;; topics/sigil-crypto-017-pin-does-not-protect-native-codegen.
30
(from-git url: "codeberg:sigil/sigil"
31
package: "sigil-lib" version: ">=0.21")
−32
(from-git url: "codeberg:sigil/sigil-crypto" version: "^0.16"))
+32
;; Pin the tested TLS 1.3 stack for the upcoming Sigil release.
+33
(from-git url: "codeberg:sigil/sigil-crypto"
+34
commit: "71362137db0596e0695791b32c4f6e8faf11b39e"))
35
36
;; These must track the host runtime. At ^0.17 they resolved to
37
;; sigil-test-runner 0.17.17, whose core.sgb is bytecode v10, and a 0.19
sigil.lockmodified
@@ -26,9 +26,8 @@
26
version: "0.21.0")
27
(package name: "sigil-crypto"
28
url: "codeberg:sigil/sigil-crypto"
−29
ref: "^0.16"
−30
sha: "debba4462b7ff4de143db9e0792b8606fdc455bc"
−31
version: "0.16.5")
+29
sha: "71362137db0596e0695791b32c4f6e8faf11b39e"
+30
version: "0.16.6")
31
(package name: "sigil-json"
32
url: "codeberg:sigil/sigil-json"
33
ref: "^0.16.0"
src/sigil/tls.sglmodified
@@ -1,6 +1,6 @@
1
;;; (sigil tls) - TLS/SSL Connections
2
;;;
−3
;;; Secure TCP connections using mbedTLS. Supports TLS 1.2 client
+3
;;; Secure TCP connections using mbedTLS. Supports TLS 1.2 and TLS 1.3 client
4
;;; connections with system CA certificate verification.
5
;;;
6
;;; ```scheme
@@ -17,6 +17,7 @@
17
tls-connection?
18
tls-connect
19
tls-connect/status
+20
tls-connect/details
21
tls-read
22
tls-read-bytevector
23
tls-write
@@ -24,7 +25,8 @@
25
tls-closed?
26
tls-set-non-blocking!
27
tls-upgrade
−27
tls-upgrade/status)
+28
tls-upgrade/status
+29
tls-upgrade/details)
30
31
(begin
32
@@ -89,6 +91,22 @@
91
(define-native (tls-connect/status hostname port . timeouts-ms)
92
(: string? integer? -> pair?))
93
+94
;;; Connect with per-attempt handshake diagnostics.
+95
;;;
+96
;;; Takes the same arguments and deadlines as tls-connect/status.
+97
;;; Returns a dictionary with status:, connection:, error-code:,
+98
;;; error-message:, handshake-state:, protocol:, cipher:, verify-flags:.
+99
;;; The connection is #f on failure; close a successful one with tls-close.
+100
;;; Error codes/messages are mbedTLS handshake errors, with 0/#f when no
+101
;;; handshake error is available. Consult status: for pre-handshake failures.
+102
;;; Handshake state is #f before a handshake, otherwise a diagnostic state
+103
;;; name (not a stable application error category). Protocol and cipher are
+104
;;; populated only after success. Verify flags are mbedTLS X.509 flags;
+105
;;; they do not establish verification when insecure mode was requested.
+106
;;; Each result owns its details; later attempts cannot overwrite it.
+107
(define-native (tls-connect/details hostname port . timeouts-ms)
+108
(: string? integer? -> dict?))
+109
110
;;; Read data from a TLS connection.
111
;;;
112
;;; Returns a string with data, #f on error, or eof-object if the
@@ -184,4 +202,10 @@
202
;;; Like `tls-upgrade`, but returns `(status . connection-or-#f)`.
203
;;; Statuses match `tls-connect/status` minus the connect-phase ones.
204
(define-native (tls-upgrade/status socket hostname . handshake-timeout-ms)
−187
(: any? string? -> pair?))))
+205
(: any? string? -> pair?))
+206
+207
;;; Upgrade an existing socket, returning the same diagnostic dictionary
+208
;;; as tls-connect/details. Takes the same arguments as tls-upgrade/status
+209
;;; and owns the socket even when the handshake fails.
+210
(define-native (tls-upgrade/details socket hostname . handshake-timeout-ms)
+211
(: any? string? -> dict?))))
test/fixtures/README.mdadded
@@ -0,0 +1,29 @@
+1
# Native TLS integration matrix
+2
+3
The driver includes the production TLS implementation and invokes its public
+4
native entry points in a real Sigil VM. It installs only the test CA in that
+5
process, keeping certificate and hostname verification enabled. This is
+6
necessary because the current library searches system CA files before the
+7
SSL_CERT_FILE override. It does not set SIGIL_TLS_INSECURE.
+8
+9
Compile tls-client.c against the same sigil-lib and sigil-crypto headers and
+10
archives as the build under test, with MBEDTLS_CONFIG_FILE defined as
+11
"sigil_mbedtls_config.h". For example, with a configured C compiler:
+12
+13
```sh
+14
cc -O2 -DMBEDTLS_CONFIG_FILE='"sigil_mbedtls_config.h"' \
+15
-I"$SIGIL_LIB_DIR/include" -I"$SIGIL_LIB_DIR/src" \
+16
-I"$SIGIL_CRYPTO_DIR/vendor/mbedtls/include" \
+17
-I"$SIGIL_CRYPTO_DIR/vendor/mbedtls" \
+18
test/fixtures/tls-client.c "$SIGIL_LIB_ARCHIVE" "$SIGIL_CRYPTO_ARCHIVE" \
+19
-lm -lpthread -o /tmp/sigil-tls-client
+20
node test/fixtures/tls-matrix.cjs /tmp/sigil-tls-client
+21
```
+22
+23
Node and OpenSSL are fixture dependencies; OPENSSL may select an explicit
+24
OpenSSL executable. Each child has a 15-second watchdog. The matrix generates
+25
a private test certificate and checks both TLS 1.2 and TLS 1.3: forty verified
+26
connections with encrypted roundtrips, wrong-host and untrusted certificates,
+27
and a successful reconnect after certificate rejection. A failure exits
+28
nonzero. The ordinary Sigil suite separately covers handshake deadlines and
+29
socket ownership for both connect and upgrade.
test/fixtures/tls-client.cadded
@@ -0,0 +1,58 @@
+1
/* Exercise public native entry points and per-attempt result ownership. */
+2
#include "../../native/tls.c"
+3
static Value field(SigilVM *vm, Value result, const char *name) {
+4
return sigil_dict_ref(vm, result, sigil_intern_keyword(vm, name, strlen(name)));
+5
}
+6
static const char *text_value(Value value) {
+7
return sigil_is_string(value) ? ((SigilString *)sigil_as_ptr(value))->data : "none";
+8
}
+9
int main(int argc, char **argv)
+10
{
+11
if (argc < 3) return 64;
+12
SigilVM *vm = sigil_vm_create();
+13
if (!vm) return 70;
+14
sigil__init_sigil_tls_module(vm);
+15
if (argc > 3) {
+16
ensure_tls_initialized();
+17
mbedtls_x509_crt_free(&global_cacert);
+18
mbedtls_x509_crt_init(&global_cacert);
+19
if (mbedtls_x509_crt_parse_file(&global_cacert, argv[3]) != 0) return 65;
+20
global_cacert_loaded = 1;
+21
}
+22
int repeat = getenv("PROBE_REPEAT") ? atoi(getenv("PROBE_REPEAT")) : 1;
+23
int exit_code = 0;
+24
for (int i = 0; i < repeat; i++) {
+25
const char *host = i == 0 && getenv("PROBE_FIRST_BAD_HOST") ? "127.0.0.1" : argv[1];
+26
Value args[] = {sigil_make_string(vm, host, strlen(host)),
+27
sigil_fixnum(atoi(argv[2])), sigil_fixnum(3000), sigil_fixnum(1500)};
+28
sigil__gc_push_temp_root(vm, args[0]);
+29
long start = sigil_tls_now_ms();
+30
Value result = native_tls_connect_details(vm, 4, args);
+31
sigil__gc_push_temp_root(vm, result);
+32
Value conn = field(vm, result, "connection");
+33
printf("attempt=%d status=%s error=%ld state=%s protocol=%s verify=%ld elapsed=%ld\n",
+34
i, text_value(field(vm, result, "status")), (long)sigil_as_fixnum(field(vm, result, "error-code")),
+35
text_value(field(vm, result, "handshake-state")), text_value(field(vm, result, "protocol")),
+36
(long)sigil_as_fixnum(field(vm, result, "verify-flags")), sigil_tls_now_ms() - start);
+37
if (is_tls_connection(conn)) {
+38
if (getenv("PROBE_ECHO")) {
+39
const char *payload = "sigil-tls-roundtrip-0123456789";
+40
Value write_args[] = {conn, sigil_make_string(vm, payload, strlen(payload))};
+41
sigil__gc_push_temp_root(vm, write_args[1]);
+42
Value n = native_tls_write(vm, 2, write_args);
+43
if (!sigil_is_fixnum(n) || sigil_as_fixnum(n) != (int)strlen(payload)) return 71;
+44
sigil__gc_pop_temp_root(vm);
+45
Value read_args[] = {conn};
+46
Value reply = native_tls_read(vm, 1, read_args);
+47
if (!sigil_is_string(reply) || strcmp(text_value(reply), payload)) return 72;
+48
puts("echo=ok");
+49
}
+50
Value close_args[] = {conn};
+51
native_tls_close(vm, 1, close_args);
+52
} else if (!(i == 0 && getenv("PROBE_FIRST_BAD_HOST"))) exit_code = 1;
+53
sigil__gc_pop_temp_root(vm);
+54
sigil__gc_pop_temp_root(vm);
+55
}
+56
sigil_vm_destroy(vm);
+57
return exit_code;
+58
}
test/fixtures/tls-matrix.cjsadded
@@ -0,0 +1,93 @@
+1
const fs = require('fs');
+2
const tls = require('tls');
+3
const cp = require('child_process');
+4
const path = require('path');
+5
const os = require('os');
+6
+7
const binary = process.argv[2];
+8
if (!binary) {
+9
throw new Error('Usage: node test/fixtures/tls-matrix.cjs /path/to/tls-client');
+10
}
+11
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'sigil-tls-matrix-'));
+12
let failures = 0;
+13
process.on('exit', () => fs.rmSync(root, { recursive: true, force: true }));
+14
cp.execFileSync(process.env.OPENSSL || 'openssl', [
+15
'req', '-x509', '-newkey', 'rsa:2048', '-nodes',
+16
'-keyout', root + '/key.pem', '-out', root + '/cert.pem',
+17
'-days', '1', '-subj', '/CN=localhost', '-addext', 'subjectAltName=DNS:localhost',
+18
], { stdio: 'ignore' });
+19
+20
function probe(port, version, label, host, trust, extra, expect) {
+21
return new Promise((resolve, reject) => {
+22
const args = [host, String(port), ...(trust ? [root + '/cert.pem'] : [])];
+23
const env = { ...process.env, ...extra };
+24
delete env.SIGIL_TLS_INSECURE;
+25
const child = cp.spawn(binary, args, { env });
+26
let output = '';
+27
const timer = setTimeout(() => child.kill('SIGKILL'), 15000);
+28
child.stdout.on('data', data => { output += data; });
+29
child.stderr.on('data', data => { output += data; });
+30
child.on('error', error => {
+31
clearTimeout(timer);
+32
reject(error);
+33
});
+34
child.on('exit', (code, signal) => {
+35
clearTimeout(timer);
+36
const pass = !signal && expect(code, output);
+37
console.log(JSON.stringify({ version, label, code, signal, pass, output }));
+38
if (!pass) failures++;
+39
resolve();
+40
});
+41
});
+42
}
+43
+44
async function checkVersion(version) {
+45
const sockets = new Set();
+46
const server = tls.createServer({
+47
key: fs.readFileSync(root + '/key.pem'),
+48
cert: fs.readFileSync(root + '/cert.pem'),
+49
minVersion: version,
+50
maxVersion: version,
+51
}, socket => {
+52
socket.on('data', data => socket.write(data));
+53
});
+54
server.on('connection', socket => {
+55
sockets.add(socket);
+56
socket.on('error', () => {});
+57
socket.on('close', () => sockets.delete(socket));
+58
});
+59
// Certificate-negative arms deliberately terminate their TLS handshakes.
+60
server.on('tlsClientError', () => {});
+61
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+62
const port = server.address().port;
+63
try {
+64
await probe(port, version, 'verified-repeat-40', 'localhost', true,
+65
{ PROBE_ECHO: '1', PROBE_REPEAT: '40' }, (code, output) => {
+66
const attempts = output.split('\n').filter(line => line.startsWith('attempt='));
+67
return code === 0 && attempts.length === 40 &&
+68
attempts.every(line => line.includes('protocol=' + version)) &&
+69
(output.match(/echo=ok/g) || []).length === 40;
+70
});
+71
await probe(port, version, 'wrong-host', '127.0.0.1', true, {},
+72
(code, output) => code === 1 && output.includes('error=-9984') &&
+73
output.includes('verify=4'));
+74
await probe(port, version, 'untrusted', 'localhost', false, {},
+75
(code, output) => code === 1 && output.includes('error=-9984'));
+76
await probe(port, version, 'recover-after-rejection', 'localhost', true,
+77
{ PROBE_ECHO: '1', PROBE_REPEAT: '2', PROBE_FIRST_BAD_HOST: '1' },
+78
(code, output) => code === 0 && output.includes('error=-9984') &&
+79
output.includes('attempt=1 status=connected') && output.includes('echo=ok'));
+80
} finally {
+81
for (const socket of sockets) socket.destroy();
+82
await new Promise(resolve => server.close(resolve));
+83
}
+84
}
+85
+86
(async () => {
+87
await checkVersion('TLSv1.2');
+88
await checkVersion('TLSv1.3');
+89
process.exitCode = failures ? 1 : 0;
+90
})().catch(error => {
+91
console.error(error);
+92
process.exitCode = 1;
+93
});
test/test-tls.sglmodified
@@ -195,3 +195,34 @@
195
(assert-false (cdr r)))))
196
197
(run-tests)
+198
+199
+200
(test-group "per-attempt handshake diagnostics"
+201
(test "details distinguish a failed TCP connect from a handshake"
+202
(let ((result (tls-connect/details "127.0.0.1" 1 1000 1000)))
+203
(assert-equal "tcp-connect-failed" (dict-ref result status:))
+204
(assert-false (dict-ref result connection:))
+205
(assert-false (dict-ref result handshake-state:))
+206
(assert-false (dict-ref result protocol:))))
+207
+208
(test "stalled handshake preserves its raw timeout and phase"
+209
(let* ((listener (tcp-listen 0))
+210
(result (tls-connect/details "127.0.0.1" (stall-port listener) 1000 100)))
+211
(socket-close listener)
+212
(assert-equal "handshake-timeout" (dict-ref result status:))
+213
(assert-false (dict-ref result connection:))
+214
(assert-equal -26624 (dict-ref result error-code:))
+215
(assert-true (string? (dict-ref result error-message:)))
+216
(assert-equal "server-hello" (dict-ref result handshake-state:))
+217
(tls-connect/details "127.0.0.1" 1 1000 1000)
+218
(assert-equal -26624 (dict-ref result error-code:))))
+219
+220
(test "upgrade details preserve the handshake deadline and own the socket"
+221
(let* ((listener (tcp-listen 0))
+222
(socket (tcp-connect "127.0.0.1" (stall-port listener)))
+223
(result (tls-upgrade/details socket "localhost" 100)))
+224
(socket-close listener)
+225
(assert-equal "handshake-timeout" (dict-ref result status:))
+226
(assert-equal -26624 (dict-ref result error-code:))
+227
(assert-false (dict-ref result connection:))
+228
(assert-error (tls-upgrade/details socket "localhost" 100)))))