nx_tls13_sig_algs_audit.nx source
↩ module page · 193 lines · 8033 B
1// nx_tls13_sig_algs_audit.nx -- V-HOST-2+1-3 Path B: ClientHello
2// signature_algorithms extension parser.
3//
4// Per RFC 8446 §4.2.3: client offers a list of signature_scheme values
5// indicating which sig algs it can verify. This module parses that
6// list defensively + tells the caller which of the substrate's
7// supported sig algs (ecdsa_secp256r1_sha256, ed25519) the client
8// supports.
9//
10// COMPOSES (per "avoid duplicate primitives"):
11// nx_syscalls (no allocation; caller-supplied buffer reads)
12//
13// COMPOSED BY:
14// future TLS server handshake validation (V-HOST-2+1-4 smoke uses
15// this for handshake-failure prediction)
16// future client-capability probes
17// sovereign nx_pages_https_daemon vhost-pick logic V+1
18//
19// V-HOST-2+1-3 SCOPE:
20// - Parse extension type 0x000D (signature_algorithms) from
21// ClientHello extension block
22// - Walk the sig-alg list; flag whether ecdsa_secp256r1_sha256 (0x0403)
23// OR ed25519 (0x0807) appear
24// - Return verdict + boolean flags
25//
26// V-HOST-2+1-3 NON-SCOPE:
27// - Block the handshake (substrate-friendly: substrate-caller decides)
28// - Parse other sig schemes (rsa_pkcs1_*, ecdsa_secp384r1_*, etc.)
29// - PSK / cert_authorities extensions
30//
31// Status: V-HOST-2+1-3 Path B. 2026-05-27.
32
33import "nx_syscalls.nx"
34
35// ===== Sealed verdict surface (codes 3700-3709) =================================================
36const NX_SAA_OK: i64 = 0
37const NX_SAA_BAD_INPUT: i64 = 3700
38const NX_SAA_TRUNCATED: i64 = 3701
39const NX_SAA_NOT_TLS13: i64 = 3702
40const NX_SAA_NO_SIG_ALGS_EXT: i64 = 3703
41const NX_SAA_LOOP_BUDGET: i64 = 3704
42
43// ===== Sealed sig_scheme values (per RFC 8446 §4.2.3) =================================================
44
45const NX_SAA_SCHEME_RSA_PKCS1_SHA256: i64 = 0x0401
46const NX_SAA_SCHEME_ECDSA_SECP256R1_SHA256: i64 = 0x0403
47const NX_SAA_SCHEME_ECDSA_SECP384R1_SHA384: i64 = 0x0503
48const NX_SAA_SCHEME_RSA_PSS_RSAE_SHA256: i64 = 0x0804
49const NX_SAA_SCHEME_RSA_PSS_RSAE_SHA384: i64 = 0x0805
50const NX_SAA_SCHEME_RSA_PSS_RSAE_SHA512: i64 = 0x0806
51const NX_SAA_SCHEME_ED25519: i64 = 0x0807
52const NX_SAA_SCHEME_ED448: i64 = 0x0808
53const NX_SAA_SCHEME_RSA_PSS_PSS_SHA256: i64 = 0x0809
54
55const NX_SAA_EXT_TYPE_SIGNATURE_ALGORITHMS: i64 = 0x000D
56
57// ===== Named constants (M7) =================================================
58const NX_SAA_MAX_EXT_DATA_LEN: i64 = 65535 // 16-bit length cap per spec
59const NX_SAA_LOOP_BUDGET_CAP: i64 = 100000
60
61// ===== Result struct =================================================
62
63struct NxSigAlgsAuditResult {
64 found_sig_algs_ext: i64 // 1 if extension type 0x000D was present
65 supports_ecdsa_p256: i64 // 1 if 0x0403 in the list
66 supports_ed25519: i64 // 1 if 0x0807 in the list
67 total_schemes_offered: i64 // # of u16 entries in the list
68 valid: i64
69}
70
71func nx_saa_result_init(r: *NxSigAlgsAuditResult) -> i64 {
72 if (r as i64) == 0 { return 0 - NX_SAA_BAD_INPUT }
73 r.found_sig_algs_ext = 0
74 r.supports_ecdsa_p256 = 0
75 r.supports_ed25519 = 0
76 r.total_schemes_offered = 0
77 r.valid = 1
78 return NX_SAA_OK
79}
80
81// ===== Parse extensions block + find signature_algorithms =================================================
82//
83// ClientHello extensions block layout (per RFC 8446 §4.1.2):
84// u16 extensions_length
85// per extension:
86// u16 extension_type
87// u16 extension_data_length
88// extension_data (extension_data_length bytes)
89//
90// signature_algorithms extension data layout (per RFC 8446 §4.2.3):
91// u16 supported_signature_algorithms_length
92// per sig_scheme:
93// u16 sig_scheme
94
95func nx_tls13_sig_algs_audit(ch_body: *u8, ch_body_n: i64,
96 ext_block_offset: i64,
97 result: *NxSigAlgsAuditResult) -> i64 {
98 if (ch_body as i64) == 0 { return 0 - NX_SAA_BAD_INPUT }
99 if ch_body_n < 0 { return 0 - NX_SAA_BAD_INPUT }
100 if ext_block_offset < 0 { return 0 - NX_SAA_BAD_INPUT }
101 if ext_block_offset + 2 > ch_body_n { return 0 - NX_SAA_TRUNCATED }
102 let rc_r: i64 = nx_saa_result_init(result)
103 if rc_r != NX_SAA_OK { return rc_r }
104
105 // Read u16 extensions_length
106 let exts_len: i64 = ((ch_body[ext_block_offset] as i64) << 8)
107 | (ch_body[ext_block_offset + 1] as i64)
108 if ext_block_offset + 2 + exts_len > ch_body_n { return 0 - NX_SAA_TRUNCATED }
109 if exts_len > NX_SAA_MAX_EXT_DATA_LEN { return 0 - NX_SAA_TRUNCATED }
110
111 var ext_off: i64 = ext_block_offset + 2
112 let ext_end: i64 = ext_off + exts_len
113 var iter: i64 = 0
114
115 while ext_off < ext_end {
116 if iter >= NX_SAA_LOOP_BUDGET_CAP { return 0 - NX_SAA_LOOP_BUDGET }
117 iter = iter + 1
118 if ext_off + 4 > ext_end { return 0 - NX_SAA_TRUNCATED }
119
120 let ext_type: i64 = ((ch_body[ext_off] as i64) << 8)
121 | (ch_body[ext_off + 1] as i64)
122 let ext_data_len: i64 = ((ch_body[ext_off + 2] as i64) << 8)
123 | (ch_body[ext_off + 3] as i64)
124 if ext_off + 4 + ext_data_len > ext_end { return 0 - NX_SAA_TRUNCATED }
125
126 if ext_type == NX_SAA_EXT_TYPE_SIGNATURE_ALGORITHMS {
127 result.found_sig_algs_ext = 1
128 // Parse the sig_alg list inside ext_data
129 let sa_off: i64 = ext_off + 4
130 if ext_data_len < 2 { return 0 - NX_SAA_TRUNCATED }
131 let list_len: i64 = ((ch_body[sa_off] as i64) << 8)
132 | (ch_body[sa_off + 1] as i64)
133 if sa_off + 2 + list_len > ext_off + 4 + ext_data_len { return 0 - NX_SAA_TRUNCATED }
134 if (list_len % 2) != 0 { return 0 - NX_SAA_TRUNCATED }
135 let n_schemes: i64 = list_len / 2
136 result.total_schemes_offered = n_schemes
137
138 var i: i64 = 0
139 while i < n_schemes {
140 if iter >= NX_SAA_LOOP_BUDGET_CAP { return 0 - NX_SAA_LOOP_BUDGET }
141 iter = iter + 1
142 let scheme: i64 = ((ch_body[sa_off + 2 + i * 2] as i64) << 8)
143 | (ch_body[sa_off + 2 + i * 2 + 1] as i64)
144 if scheme == NX_SAA_SCHEME_ECDSA_SECP256R1_SHA256 {
145 result.supports_ecdsa_p256 = 1
146 }
147 if scheme == NX_SAA_SCHEME_ED25519 {
148 result.supports_ed25519 = 1
149 }
150 i = i + 1
151 }
152 }
153 ext_off = ext_off + 4 + ext_data_len
154 }
155
156 if result.found_sig_algs_ext == 0 { return 0 - NX_SAA_NO_SIG_ALGS_EXT }
157 return NX_SAA_OK
158}
159
160// ===== Convenience: pick a sig alg from result =================================================
161//
162// Returns the substrate-supported sig alg that BOTH:
163// 1. the client offered, AND
164// 2. the operator's cert key supports
165//
166// Returns NX_SAA_SCHEME_* on success; 0 if no overlap.
167// caller_cert_kind: 0 = ECDSA-only; 1 = Ed25519-only; 2 = either-OK
168//
169// Per RFC 8446 §4.4.3: if no overlap, server MUST send HANDSHAKE_FAILURE
170// alert. Caller handles that decision; this fn just reports.
171
172func nx_saa_pick_sig_alg(result: *NxSigAlgsAuditResult,
173 caller_cert_kind: i64) -> i64 {
174 if result.valid != 1 { return 0 }
175 if result.found_sig_algs_ext == 0 { return 0 }
176 // Cert kind 0 = ECDSA only
177 if caller_cert_kind == 0 {
178 if result.supports_ecdsa_p256 == 1 { return NX_SAA_SCHEME_ECDSA_SECP256R1_SHA256 }
179 return 0
180 }
181 // Cert kind 1 = Ed25519 only
182 if caller_cert_kind == 1 {
183 if result.supports_ed25519 == 1 { return NX_SAA_SCHEME_ED25519 }
184 return 0
185 }
186 // Cert kind 2 = either; prefer Ed25519 (modern; smaller; per RFC 8446 modernization)
187 if caller_cert_kind == 2 {
188 if result.supports_ed25519 == 1 { return NX_SAA_SCHEME_ED25519 }
189 if result.supports_ecdsa_p256 == 1 { return NX_SAA_SCHEME_ECDSA_SECP256R1_SHA256 }
190 return 0
191 }
192 return 0
193}