nx_ssh_kex.nx source
↩ module page · 232 lines · 9952 B
1// nx_ssh_kex.nx -- sovereign SSH-2 key exchange (phase 1 of the bits-up SSH
2// client). Implements the ONE ciphersuite the NAS prefers (verified by
3// nx_ssh_probe): curve25519-sha256 KEX + ssh-ed25519 host key. Performs the
4// full client KEX against a real OpenSSH server and VERIFIES the server's
5// Ed25519 signature over our independently-computed exchange hash H.
6//
7// If that signature verifies, every byte was right: our version string, our
8// KEXINIT (I_C), the server KEXINIT we captured (I_S), the X25519 shared
9// secret K, and the RFC-8731 exchange-hash construction. That is a 1:1
10// interop proof against the real server -- not a self-test.
11//
12// H = SHA256( string(V_C) || string(V_S) || string(I_C) || string(I_S)
13// || string(K_S) || string(Q_C) || string(Q_S) || mpint(K) )
14//
15// Bits-up: raw syscalls + our own X25519/Ed25519/SHA-256. No openssh/libssl.
16//
17// license_tier: ORIGINAL
18
19import "nx_syscalls.nx"
20import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
21import "nx_x25519_ephemeral.nx"
22import "nx_ed25519_signature.nx"
23import "nx_sha256.nx"
24const K_MAGIC_4096: i64 = 4096
25const K_MAGIC_1024: i64 = 1024
26
27func P(s: *u8, n: i64) -> i64 { sys_write(1, s, n); return 0 }
28func phex(b: *u8, n: i64) -> i64 {
29 let hx: *u8 = "0123456789abcdef" as *u8
30 var i: i64 = 0
31 while i < n {
32 let o: *u8 = sys_mmap(2)
33 o[0] = hx[(b[i] as i64 >> 4) & 0xf]; o[1] = hx[(b[i] as i64) & 0xf]
34 sys_write(1, o, 2); i = i + 1
35 }
36 return 0
37}
38
39func ssh_sockaddr(sa: *u8, ip: i64, port: i64) -> i64 {
40 sa[0] = (AF_INET & 0xff) as u8; sa[1] = ((AF_INET >> 8) & 0xff) as u8
41 sa[2] = ((port >> 8) & 0xff) as u8; sa[3] = (port & 0xff) as u8
42 sa[4] = ((ip >> 24) & 0xff) as u8; sa[5] = ((ip >> 16) & 0xff) as u8
43 sa[6] = ((ip >> 8) & 0xff) as u8; sa[7] = (ip & 0xff) as u8
44 var p: i64 = 8
45 while p < 16 { sa[p] = 0 as u8; p = p + 1 }
46 return 0
47}
48func u32be(b: *u8, off: i64) -> i64 {
49 return ((b[off] as i64) << 24) | ((b[off+1] as i64) << 16) | ((b[off+2] as i64) << 8) | (b[off+3] as i64)
50}
51func read_full(fd: i64, dst: *u8, n: i64) -> i64 {
52 var got: i64 = 0
53 while got < n {
54 let r: i64 = sys_read(fd, (dst + got) as *u8, n - got)
55 if r <= 0 { return got }
56 got = got + r
57 }
58 return got
59}
60// read one CRLF-terminated line into dst; return length incl trailing chars.
61func read_line(fd: i64, dst: *u8, max: i64) -> i64 {
62 var k: i64 = 0
63 while k < max {
64 let r: i64 = sys_read(fd, (dst + k) as *u8, 1)
65 if r <= 0 { return k }
66 if dst[k] == 10 as u8 { return k + 1 }
67 k = k + 1
68 }
69 return k
70}
71// ---- byte builders (return new index) ----
72func put_u32(b: *u8, idx: i64, v: i64) -> i64 {
73 b[idx] = ((v >> 24) & 0xff) as u8; b[idx+1] = ((v >> 16) & 0xff) as u8
74 b[idx+2] = ((v >> 8) & 0xff) as u8; b[idx+3] = (v & 0xff) as u8
75 return idx + 4
76}
77func put_byte(b: *u8, idx: i64, v: i64) -> i64 { b[idx] = (v & 0xff) as u8; return idx + 1 }
78func put_bytes(b: *u8, idx: i64, s: *u8, n: i64) -> i64 {
79 var i: i64 = 0
80 while i < n { b[idx+i] = s[i]; i = i + 1 }
81 return idx + n
82}
83func put_str(b: *u8, idx: i64, s: *u8, n: i64) -> i64 {
84 let j: i64 = put_u32(b, idx, n)
85 return put_bytes(b, j, s, n)
86}
87// ---- SSH binary-packet framing (pre-encryption: no MAC) ----
88func send_packet(fd: i64, payload: *u8, plen: i64) -> i64 {
89 var padlen: i64 = 8 - ((5 + plen) % 8)
90 if padlen < 4 { padlen = padlen + 8 }
91 let pktlen: i64 = 1 + plen + padlen
92 let out: *u8 = sys_mmap(plen + padlen + 16)
93 put_u32(out, 0, pktlen)
94 out[4] = (padlen & 0xff) as u8
95 put_bytes(out, 5, payload, plen)
96 var j: i64 = 0
97 while j < padlen { out[5 + plen + j] = 0 as u8; j = j + 1 }
98 sys_write(fd, out, 5 + plen + padlen)
99 return 0
100}
101func read_packet(fd: i64, payload_out: *u8) -> i64 {
102 let hdr: *u8 = sys_mmap(8)
103 if read_full(fd, hdr, 4) != 4 { return 0 - 1 }
104 let pktlen: i64 = u32be(hdr, 0)
105 let body: *u8 = sys_mmap(pktlen + 16)
106 if read_full(fd, body, pktlen) != pktlen { return 0 - 1 }
107 let padlen: i64 = body[0] as i64
108 let plen: i64 = pktlen - padlen - 1
109 put_bytes(payload_out, 0, (body + 1) as *u8, plen)
110 return plen
111}
112// ---- exchange-hash helpers ----
113func h_u32(ctx: *Sha256, v: i64) -> i64 {
114 let t: *u8 = sys_mmap(4); put_u32(t, 0, v); sha256_update(ctx, t, 4); return 0
115}
116func h_str(ctx: *Sha256, src: *u8, n: i64) -> i64 { h_u32(ctx, n); sha256_update(ctx, src, n); return 0 }
117// mpint value bytes of a 32-byte big-endian K (strip leading zeros, prepend
118// 0x00 if high bit set). Returns length.
119func build_mpint(K: *u8, n: i64, out: *u8) -> i64 {
120 var i: i64 = 0
121 while i < n { if K[i] != 0 as u8 { break } i = i + 1 }
122 if i == n { return 0 }
123 var o: i64 = 0
124 if (K[i] as i64 & 0x80) != 0 { out[o] = 0 as u8; o = o + 1 }
125 while i < n { out[o] = K[i]; o = o + 1; i = i + 1 }
126 return o
127}
128
129func main() -> i64 {
130 let ip: i64 = (192 << 24) | (168 << 16) | (8 << 8) | 240
131 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0)
132 if fd < 0 { P("socket fail\n" as *u8, 12); return 1 }
133 let sa: *u8 = sys_mmap(16); ssh_sockaddr(sa, ip, 22)
134 if nx_connect_bounded(fd, sa, 16, NX_CONN_DEFAULT_MS) != 0 { P("connect fail\n" as *u8, 13); return 2 }
135
136 // ---- version exchange (V_C/V_S exclude CR LF for the hash) ----
137 let VC: *u8 = "SSH-2.0-NishiSSH_0.1" as *u8
138 let vclen: i64 = 20
139 sys_write(fd, VC, vclen); sys_write(fd, "\r\n" as *u8, 2)
140 let vsline: *u8 = sys_mmap(256)
141 var vsraw: i64 = read_line(fd, vsline, 255)
142 var vslen: i64 = vsraw
143 while vslen > 0 { if vsline[vslen-1] == 13 as u8 { vslen = vslen - 1 } else { if vsline[vslen-1] == 10 as u8 { vslen = vslen - 1 } else { break } } }
144 P("server : " as *u8, 11); sys_write(1, vsline, vslen); P("\n" as *u8, 1)
145
146 // ---- read server KEXINIT (I_S) ----
147 let IS: *u8 = sys_mmap(K_MAGIC_4096)
148 let islen: i64 = read_packet(fd, IS)
149 if islen < 0 { P("no KEXINIT\n" as *u8, 11); return 3 }
150 if IS[0] != 20 as u8 { P("not KEXINIT\n" as *u8, 12); return 4 }
151
152 // ---- build + send our KEXINIT (I_C) ----
153 let IC: *u8 = sys_mmap(K_MAGIC_1024)
154 var ic: i64 = 0
155 ic = put_byte(IC, ic, 20)
156 let cookie: *u8 = sys_mmap(16); rand_bytes(cookie, 16); ic = put_bytes(IC, ic, cookie, 16)
157 ic = put_str(IC, ic, "curve25519-sha256" as *u8, 17)
158 ic = put_str(IC, ic, "ssh-ed25519" as *u8, 11)
159 ic = put_str(IC, ic, "chacha20-poly1305@openssh.com" as *u8, 29)
160 ic = put_str(IC, ic, "chacha20-poly1305@openssh.com" as *u8, 29)
161 ic = put_str(IC, ic, "hmac-sha2-256" as *u8, 13)
162 ic = put_str(IC, ic, "hmac-sha2-256" as *u8, 13)
163 ic = put_str(IC, ic, "none" as *u8, 4)
164 ic = put_str(IC, ic, "none" as *u8, 4)
165 ic = put_u32(IC, ic, 0); ic = put_u32(IC, ic, 0) // languages (empty)
166 ic = put_byte(IC, ic, 0) // first_kex_packet_follows
167 ic = put_u32(IC, ic, 0) // reserved
168 send_packet(fd, IC, ic)
169
170 // ---- X25519 ephemeral + KEX_ECDH_INIT (msg 30) ----
171 let eph_priv: *u8 = sys_mmap(32); let QC: *u8 = sys_mmap(32)
172 x25519_gen_keypair(eph_priv, QC)
173 let KI: *u8 = sys_mmap(64); var ki: i64 = 0
174 ki = put_byte(KI, ki, 30); ki = put_str(KI, ki, QC, 32)
175 send_packet(fd, KI, ki)
176
177 // ---- KEX_ECDH_REPLY (msg 31): K_S, Q_S, signature ----
178 let REP: *u8 = sys_mmap(K_MAGIC_4096)
179 let rlen: i64 = read_packet(fd, REP)
180 if rlen < 0 { P("no ECDH_REPLY\n" as *u8, 14); return 5 }
181 if REP[0] != 31 as u8 { P("not ECDH_REPLY (msg=" as *u8, 21); phex((REP) as *u8, 1); P(")\n" as *u8, 2); return 6 }
182 var off: i64 = 1
183 let kslen: i64 = u32be(REP, off); off = off + 4
184 let KS: *u8 = (REP + off) as *u8; off = off + kslen
185 let qslen: i64 = u32be(REP, off); off = off + 4
186 let QS: *u8 = (REP + off) as *u8; off = off + qslen
187 let siglen: i64 = u32be(REP, off); off = off + 4
188 let SIG: *u8 = (REP + off) as *u8
189
190 // ---- shared secret K = X25519(eph_priv, Q_S) ----
191 let K: *u8 = sys_mmap(32)
192 x25519(eph_priv, QS, K)
193 let mp: *u8 = sys_mmap(40); let mplen: i64 = build_mpint(K, 32, mp)
194
195 // ---- exchange hash H ----
196 let ctx: *Sha256 = sys_mmap(256) as *Sha256
197 sha256_init(ctx)
198 h_str(ctx, VC, vclen)
199 h_str(ctx, vsline, vslen)
200 h_str(ctx, IC, ic)
201 h_str(ctx, IS, islen)
202 h_str(ctx, KS, kslen)
203 h_str(ctx, QC, 32)
204 h_str(ctx, QS, qslen)
205 h_str(ctx, mp, mplen)
206 let H: *u8 = sys_mmap(32); sha256_final(ctx, H)
207
208 // ---- parse host-key + signature blobs (ssh-ed25519) ----
209 var ko: i64 = 0
210 let kt: i64 = u32be(KS, ko); ko = ko + 4 + kt // skip "ssh-ed25519"
211 let publen: i64 = u32be(KS, ko); ko = ko + 4
212 let PUB: *u8 = (KS + ko) as *u8 // 32-byte ed25519 pubkey
213 var so: i64 = 0
214 let st: i64 = u32be(SIG, so); so = so + 4 + st // skip "ssh-ed25519"
215 let sraw_len: i64 = u32be(SIG, so); so = so + 4
216 let SIGRAW: *u8 = (SIG + so) as *u8 // 64-byte signature
217
218 // host-key fingerprint (SHA-256 of the K_S blob) for TOFU
219 let fp: *u8 = sys_mmap(32); sha256_digest(KS, kslen, fp)
220 P("hostkey : ssh-ed25519, SHA256 fp=" as *u8, 34); phex(fp, 16); P("...\n" as *u8, 4)
221
222 // ---- THE PROOF: verify the server's signature over our hash ----
223 let v: i64 = ed25519_verify_full(PUB, H, 32, SIGRAW)
224 if v == NX_ED25519_SIG_OK {
225 P("exch hash: " as *u8, 11); phex(H, 32); P("\n" as *u8, 1)
226 P("RESULT : [OK] server Ed25519 signature VERIFIES over our exchange hash.\n" as *u8, 73)
227 P(" => KEX is byte-correct 1:1 vs the real OpenSSH server. Shared secret established.\n" as *u8, 92)
228 sys_close(fd); return 0
229 }
230 P("RESULT : [XX] signature did NOT verify -- a byte is off (V/I_C/I_S/K/hash). Debug.\n" as *u8, 84)
231 sys_close(fd); return 7
232}