code wiki / _hdl_build / nx_tls13_transcript384_gate.nx
nx_tls13_transcript384_gate.nx source
↩ module page · 95 lines · 4613 B
1// nx_tls13_transcript384_gate.nx -- proves the dual-hash transcript (R9, 2026-08-05).
2//
3// WHY: TLS 1.3 picks the transcript hash FROM THE CIPHER SUITE, known only at ServerHello.
4// A single streaming SHA-256 therefore can never serve 0x1302 (AES-256-GCM-SHA384), which is
5// nginx's DEFAULT preference -- so those hosts were unreachable and the crawler then RETIRED
6// them. The transcript now runs BOTH hashes from the first byte.
7//
8// HONEST PROVING METHOD: no invented goldens. The SHA-384 transcript is proven by DEFINITIONAL
9// IDENTITY against sha384_digest -- which is itself NIST-KAT'd in nx_sha384_kat_test.nx. Feeding
10// the same bytes to the streaming transcript and to the one-shot digest MUST agree bit-for-bit.
11// The SHA-256 half is proven UNCHANGED against sha256_digest, which is the regression control:
12// this change must be invisible to every existing caller (client, server and mTLS all share
13// this object).
14// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
15import "nx_syscalls.nx"
16import "nx_gate_verdict.nx"
17import "nx_tls13_transcript.nx"
18import "nx_sha512.nx"
19
20func tg_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
21
22func main(argc: i64, argv: *i64) -> i64 {
23 let ctr: *i64 = gv_ctr()
24 ctr[0] = 0
25 ctr[1] = 0
26 tg_puts("=== nx_tls13_transcript384 gate (dual-hash transcript for 0x1302) ===\n" as *u8)
27
28 // three chunks, fed as separate updates -- exercises the STREAMING path, not a one-shot
29 let m1: *u8 = "abc" as *u8
30 let m2: *u8 = "defghijklmnop" as *u8
31 let m3: *u8 = "0123456789qrstuvwxyz-the-transcript-spans-updates" as *u8
32 let all: *u8 = sys_mmap(256)
33 var n: i64 = 0
34 var i: i64 = 0
35 while m1[i] != (0 as u8) { all[n] = m1[i]; n = n + 1; i = i + 1 }
36 i = 0
37 while m2[i] != (0 as u8) { all[n] = m2[i]; n = n + 1; i = i + 1 }
38 i = 0
39 while m3[i] != (0 as u8) { all[n] = m3[i]; n = n + 1; i = i + 1 }
40
41 let tx: *u8 = nx_tls13_transcript_new()
42 var l1: i64 = 0
43 while m1[l1] != (0 as u8) { l1 = l1 + 1 }
44 var l2: i64 = 0
45 while m2[l2] != (0 as u8) { l2 = l2 + 1 }
46 var l3: i64 = 0
47 while m3[l3] != (0 as u8) { l3 = l3 + 1 }
48 nx_tls13_transcript_update(tx, m1, l1)
49 nx_tls13_transcript_update(tx, m2, l2)
50 nx_tls13_transcript_update(tx, m3, l3)
51
52 // T1 SHA-384 streaming transcript == one-shot sha384_digest (NIST-KAT'd primitive)
53 let got384: *u8 = sys_mmap(64)
54 nx_tls13_transcript_snapshot384(tx, got384)
55 let want384: *u8 = sys_mmap(64)
56 sha384_digest(all, n, want384)
57 var ok384: i64 = 1
58 i = 0
59 while i < 48 { if got384[i] != want384[i] { ok384 = 0 } i = i + 1 }
60 gv_check("T1 SHA-384 transcript == sha384_digest over the same bytes (definitional identity)" as *u8, ok384, ctr)
61
62 // T2 REGRESSION CONTROL: the SHA-256 half must be UNCHANGED -- client, server and mTLS
63 // all share this object, so an existing caller must not observe any difference.
64 let got256: *u8 = sys_mmap(64)
65 nx_tls13_transcript_snapshot(tx, got256)
66 let want256: *u8 = sys_mmap(64)
67 sha256_digest(all, n, want256)
68 var ok256: i64 = 1
69 i = 0
70 while i < 32 { if got256[i] != want256[i] { ok256 = 0 } i = i + 1 }
71 gv_check("T2 CONTROL SHA-256 half UNCHANGED (existing callers see no difference)" as *u8, ok256, ctr)
72
73 // T3 NON-DESTRUCTIVE: snapshotting must not disturb the running state, so a further
74 // update still tracks the one-shot. This is the bug that once corrupted the SHA-256 path.
75 nx_tls13_transcript_update(tx, m1, l1)
76 i = 0
77 while i < l1 { all[n] = m1[i]; n = n + 1; i = i + 1 }
78 let got384b: *u8 = sys_mmap(64)
79 nx_tls13_transcript_snapshot384(tx, got384b)
80 let want384b: *u8 = sys_mmap(64)
81 sha384_digest(all, n, want384b)
82 var ok384b: i64 = 1
83 i = 0
84 while i < 48 { if got384b[i] != want384b[i] { ok384b = 0 } i = i + 1 }
85 gv_check("T3 snapshot384 is NON-DESTRUCTIVE (update after snapshot still tracks)" as *u8, ok384b, ctr)
86
87 // T4 NEGATIVE CONTROL: the two digests must DIFFER, proving T1 is not comparing a buffer
88 // with itself (the vacuity trap -- two failures that agree are not an equivalence).
89 var differ: i64 = 0
90 i = 0
91 while i < 32 { if got384[i] != got256[i] { differ = 1 } i = i + 1 }
92 gv_check("T4 NEG SHA-384 and SHA-256 snapshots DIFFER (T1/T2 are not the same buffer)" as *u8, differ, ctr)
93
94 let rc: i64 = gv_verdict("TLS13-TRANSCRIPT384-GATE" as *u8, ctr, "dual-hash transcript: SHA-384 by definitional identity, SHA-256 unchanged, non-destructive, distinct" as *u8)
95 return rc
96}