code wiki / (root) / nx_ssh_kex.nx

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}