code wiki / _hdl_build / nx_tor_ntor.nx
nx_tor_ntor.nx source
↩ module page · 156 lines · 9234 B
1// nx_tor_ntor.nx -- SOVEREIGN Tor ntor / CREATE2 circuit handshake (tor-spec.txt §5.1.4 + KDF §5.2.2).
2// Phase 1 of the anonymizing-transport arc: the foundational Tor circuit key-agreement. It COMPOSES the
3// ecosystem's already-gated primitives -- x25519 (RFC 7748), hmac_sha256 (RFC 2104/4231), hkdf_expand
4// (RFC 5869) -- into Tor's exact ntor construction. NO new crypto is rolled here.
5//
6// tor-spec §5.1.4 (verbatim mapping):
7// PROTOID = "ntor-curve25519-sha256-1"
8// t_mac = PROTOID | ":mac" t_key = PROTOID | ":key_extract"
9// t_verify = PROTOID | ":verify" m_expand = PROTOID | ":key_expand"
10// H(msg, tweak) = HMAC-SHA256(key=tweak, msg=msg)
11// client onion skin = NODEID(ID,20) | KEYID(B,32) | CLIENT_PK(X,32) [CREATE2 HDATA, htype=ntor]
12// server: secret_input = EXP(X,y) | EXP(X,b) | ID | B | X | Y | PROTOID
13// client: secret_input = EXP(Y,x) | EXP(B,x) | ID | B | X | Y | PROTOID (equal by DH commutativity)
14// KEY_SEED = H(secret_input, t_key) verify = H(secret_input, t_verify)
15// auth_input = verify | ID | B | Y | X | PROTOID | "Server"
16// AUTH = H(auth_input, t_mac) server reply = SERVER_PK(Y,32) | AUTH(32) [CREATED2 HDATA]
17// key material = HKDF-Expand(PRK=KEY_SEED, info=m_expand) -> Df|Db|Kf|Kb|KH (20|20|16|16|20 = 92)
18//
19// EXP(pk, sk) in the spec = X25519(sk, pk) here (scalar sk on point pk).
20//
21// HONEST SCOPE: this is the ntor COMPOSITION, spec-faithful. It does NOT by itself prove byte-exact interop
22// with the live Tor network -- that requires (1) Tor's own published ntor test vector (not present in-tree;
23// zero external calls this run) AND (2) the x25519 base-point fix (nx_x25519_test.nx documents a scalar-
24// pattern bug in base-point multiplication that ntor's X=x*G / Y=y*G / B=b*G derivations depend on). Until
25// a full circuit works, anon_is_anonymizing() stays 0. Sovereign: nx_syscalls + x25519 + hmac + hkdf.
26// license_tier: ORIGINAL
27import "nx_syscalls.nx"
28import "nx_x25519_ephemeral.nx" // x25519_keypair_public (pub=priv*G) + x25519_shared_secret (X25519 DH)
29import "nx_hmac.nx" // hmac_sha256(key,klen,msg,mlen,out32) -- RFC 2104/4231
30import "nx_hkdf.nx" // hkdf_expand(prk,info,ilen,L,out) -- RFC 5869
31const NTOR_MAGIC_2104: i64 = 2104
32const NTOR_MAGIC_4231: i64 = 4231
33const NTOR_MAGIC_5869: i64 = 5869
34
35const NTOR_ID_LEN: i64 = 20 // router identity digest length
36const NTOR_PUB: i64 = 32 // curve25519 public/secret length
37const NTOR_H: i64 = 32 // SHA-256 / HMAC output length
38const NTOR_KEYS_LEN: i64 = 92 // Df(20)|Db(20)|Kf(16)|Kb(16)|KH(20)
39const NTOR_SKIN_LEN: i64 = 84 // ID(20)|B(32)|X(32)
40const NTOR_REPLY_LEN:i64 = 64 // Y(32)|AUTH(32)
41
42const NTOR_OK: i64 = 0
43const NTOR_E_AUTH: i64 = 0 - 1 // client: recomputed AUTH != received AUTH (reject the relay)
44const NTOR_E_BADPOINT:i64 = 0 - 2 // degenerate DH result (all-zero shared secret) -> reject
45
46// NUL-terminated string length (protocol tweak strings).
47func nt_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){ n=n+1 } return n }
48// append src[0..n) into dst at off; return new offset.
49func nt_cat(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var i: i64=0; while i<n { dst[off+i]=src[i]; i=i+1 } return off+n }
50// constant-time equality of a[0..n) and b[0..n): 1 if equal, 0 otherwise. No early exit (HM2 hygiene).
51func nt_ct_eq(a: *u8, b: *u8, n: i64) -> i64 { var d: i64=0; var i: i64=0; while i<n { d = d | ((a[i] as i64) ^ (b[i] as i64)); i=i+1 } if d==0 { return 1 } return 0 }
52// 1 if buf[0..n) is all-zero (a degenerate/low-order DH output to reject).
53func nt_is_zero(buf: *u8, n: i64) -> i64 { var i: i64=0; while i<n { if buf[i]!=(0 as u8) { return 0 } i=i+1 } return 1 }
54
55// secret_input = xy | xb | ID | B | X | Y | PROTOID -> buf ; returns length (204).
56func ntor_build_secret_input(xy: *u8, xb: *u8, id: *u8, bpub: *u8, xpub: *u8, ypub: *u8, buf: *u8) -> i64 {
57 let protoid: *u8 = "ntor-curve25519-sha256-1" as *u8
58 var o: i64 = 0
59 o = nt_cat(buf, o, xy, NTOR_PUB)
60 o = nt_cat(buf, o, xb, NTOR_PUB)
61 o = nt_cat(buf, o, id, NTOR_ID_LEN)
62 o = nt_cat(buf, o, bpub, NTOR_PUB)
63 o = nt_cat(buf, o, xpub, NTOR_PUB)
64 o = nt_cat(buf, o, ypub, NTOR_PUB)
65 o = nt_cat(buf, o, protoid, nt_slen(protoid))
66 return o
67}
68
69// auth_input = verify | ID | B | Y | X | PROTOID | "Server" -> buf ; returns length (178).
70func ntor_build_auth_input(verify: *u8, id: *u8, bpub: *u8, ypub: *u8, xpub: *u8, buf: *u8) -> i64 {
71 let protoid: *u8 = "ntor-curve25519-sha256-1" as *u8
72 let server: *u8 = "Server" as *u8
73 var o: i64 = 0
74 o = nt_cat(buf, o, verify, NTOR_H)
75 o = nt_cat(buf, o, id, NTOR_ID_LEN)
76 o = nt_cat(buf, o, bpub, NTOR_PUB)
77 o = nt_cat(buf, o, ypub, NTOR_PUB)
78 o = nt_cat(buf, o, xpub, NTOR_PUB)
79 o = nt_cat(buf, o, protoid, nt_slen(protoid))
80 o = nt_cat(buf, o, server, nt_slen(server))
81 return o
82}
83
84// From secret_input -> KEY_SEED = H(si, t_key) and verify = H(si, t_verify). Shared by both parties.
85func ntor_kdf_seed_verify(si: *u8, si_len: i64, key_seed_out: *u8, verify_out: *u8) -> i64 {
86 let t_key: *u8 = "ntor-curve25519-sha256-1:key_extract" as *u8
87 let t_verify: *u8 = "ntor-curve25519-sha256-1:verify" as *u8
88 hmac_sha256(t_key, nt_slen(t_key), si, si_len, key_seed_out)
89 hmac_sha256(t_verify, nt_slen(t_verify), si, si_len, verify_out)
90 return 0
91}
92
93// AUTH = H(auth_input, t_mac).
94func ntor_compute_auth(verify: *u8, id: *u8, bpub: *u8, ypub: *u8, xpub: *u8, auth_out: *u8) -> i64 {
95 let t_mac: *u8 = "ntor-curve25519-sha256-1:mac" as *u8
96 let abuf: *u8 = sys_mmap(256)
97 let an: i64 = ntor_build_auth_input(verify, id, bpub, ypub, xpub, abuf)
98 hmac_sha256(t_mac, nt_slen(t_mac), abuf, an, auth_out)
99 return 0
100}
101
102// Expand KEY_SEED -> NTOR_KEYS_LEN bytes of circuit key material (HKDF-Expand, info=m_expand).
103func ntor_expand_keys(key_seed: *u8, keys_out: *u8) -> i64 {
104 let m_expand: *u8 = "ntor-curve25519-sha256-1:key_expand" as *u8
105 return hkdf_expand(key_seed, m_expand, nt_slen(m_expand), NTOR_KEYS_LEN, keys_out)
106}
107
108// ---- CLIENT: build the onion skin (CREATE2 HDATA). x = client ephemeral secret (caller-supplied for
109// determinism, else from x25519_keypair_private). Writes X (client eph pub) and onion_skin = ID|B|X. ----
110func ntor_client_init(id: *u8, bpub: *u8, x_priv: *u8, x_pub_out: *u8, onion_skin_out: *u8) -> i64 {
111 x25519_keypair_public(x_priv, x_pub_out) // X = x * G
112 var o: i64 = 0
113 o = nt_cat(onion_skin_out, o, id, NTOR_ID_LEN)
114 o = nt_cat(onion_skin_out, o, bpub, NTOR_PUB)
115 o = nt_cat(onion_skin_out, o, x_pub_out, NTOR_PUB)
116 return o // 84
117}
118
119// ---- SERVER (relay): given the client's ID/B/X and its own onion key (b,B) + ephemeral secret y,
120// produce Y|AUTH (CREATED2 HDATA) and the derived key material. ----
121func ntor_server_respond(id: *u8, bpub: *u8, b_priv: *u8, x_pub: *u8, y_priv: *u8,
122 y_pub_out: *u8, auth_out: *u8, keys_out: *u8) -> i64 {
123 x25519_keypair_public(y_priv, y_pub_out) // Y = y * G
124 let xy: *u8 = sys_mmap(32); x25519_shared_secret(y_priv, x_pub, xy) // EXP(X,y)=X25519(y,X)
125 let xb: *u8 = sys_mmap(32); x25519_shared_secret(b_priv, x_pub, xb) // EXP(X,b)=X25519(b,X)
126 if nt_is_zero(xy, 32)==1 { return NTOR_E_BADPOINT }
127 if nt_is_zero(xb, 32)==1 { return NTOR_E_BADPOINT }
128 let si: *u8 = sys_mmap(256)
129 let si_len: i64 = ntor_build_secret_input(xy, xb, id, bpub, x_pub, y_pub_out, si)
130 let key_seed: *u8 = sys_mmap(32); let verify: *u8 = sys_mmap(32)
131 ntor_kdf_seed_verify(si, si_len, key_seed, verify)
132 ntor_compute_auth(verify, id, bpub, y_pub_out, x_pub, auth_out)
133 ntor_expand_keys(key_seed, keys_out)
134 return NTOR_OK
135}
136
137// ---- CLIENT finish: on receiving Y|AUTH, recompute + verify AUTH (const-time), derive the SAME keys.
138// x_priv/x_pub are the client's own ephemeral pair from ntor_client_init. Returns NTOR_OK iff AUTH verifies
139// (fail-closed on a bad point or an AUTH mismatch -> the relay is rejected). Also writes KEY_SEED for the CLI. ----
140func ntor_client_finish(id: *u8, bpub: *u8, x_priv: *u8, x_pub: *u8, y_pub: *u8, auth_recv: *u8,
141 key_seed_out: *u8, keys_out: *u8) -> i64 {
142 if nt_is_zero(y_pub, 32)==1 { return NTOR_E_BADPOINT } // reject identity/degenerate Y
143 let xy: *u8 = sys_mmap(32); x25519_shared_secret(x_priv, y_pub, xy) // EXP(Y,x)=X25519(x,Y)
144 let xb: *u8 = sys_mmap(32); x25519_shared_secret(x_priv, bpub, xb) // EXP(B,x)=X25519(x,B)
145 if nt_is_zero(xy, 32)==1 { return NTOR_E_BADPOINT }
146 if nt_is_zero(xb, 32)==1 { return NTOR_E_BADPOINT }
147 let si: *u8 = sys_mmap(256)
148 let si_len: i64 = ntor_build_secret_input(xy, xb, id, bpub, x_pub, y_pub, si)
149 let verify: *u8 = sys_mmap(32)
150 ntor_kdf_seed_verify(si, si_len, key_seed_out, verify)
151 let auth_calc: *u8 = sys_mmap(32)
152 ntor_compute_auth(verify, id, bpub, y_pub, x_pub, auth_calc)
153 if nt_ct_eq(auth_calc, auth_recv, NTOR_H) != 1 { return NTOR_E_AUTH } // AUTH mismatch -> reject relay
154 ntor_expand_keys(key_seed_out, keys_out)
155 return NTOR_OK
156}