nx_fhe.nx source
↩ module page · 166 lines · 5669 B
1// fhe.nx -- fully homomorphic encryption scaffold.
2//
3// EFFICIENCY_ROADMAP ยง6.1. Compute on ciphertext; the party
4// holding the key never sees plaintext, yet gets the right
5// answer. Targets privacy-preserving analytics, untrusted
6// compute substrates (cloud as "hostile arithmetic factory").
7//
8// Schemes available today:
9// BFV / BGV -- exact integer arithmetic, SIMD-friendly
10// CKKS -- approximate real arithmetic, batching
11// TFHE -- binary / bit-level, fast bootstrapping
12//
13// Each has its own complexity tradeoffs. Our scaffold exposes
14// a uniform API (`fhe_add`, `fhe_mul`, `fhe_rotate`) + a
15// scheme-selector enum; dispatchable backends land later.
16//
17// Full implementations are ENORMOUS: OpenFHE is ~200k LoC C++,
18// SEAL is ~100k. Phase A here is API-locking + noise-budget
19// accounting so library code that wants to "maybe run FHE
20// someday" can be written in terms of the right abstractions
21// now.
22//
23// Invariants:
24// FHE1 All ciphertext operations are CONSTANT time in the
25// plaintext (leaking neither value nor sign).
26// FHE2 Every op consumes "noise budget"; fhe_budget_left
27// reports remaining. Bootstrap refreshes (expensive).
28// FHE3 Decrypt with the wrong key produces random garbage,
29// NOT an error indication -- semantic security.
30
31// nx_safety_envelope:
32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
33// sil_target: SIL1
34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
35// verdict: NOT_YET_EVALUATED
36
37import "nx_syscalls.nx"
38const FHE_MAGIC_65537: i64 = 65537
39const FHE_MAGIC_4096: i64 = 4096
40
41const FHE_SCHEME_BFV: i64 = 1
42const FHE_SCHEME_BGV: i64 = 2
43const FHE_SCHEME_CKKS: i64 = 3
44const FHE_SCHEME_TFHE: i64 = 4
45
46const FHE_ERR_PENDING: i64 = -1
47const FHE_ERR_BUDGET: i64 = -2
48
49// Opaque handles. The actual ciphertext can be MB in size;
50// we keep a descriptor with metadata + a pointer to heap
51// storage.
52struct FHECtx {
53 scheme: i64,
54 plain_modulus: i64,
55 poly_degree: i64, // typically 2^12 .. 2^15
56 noise_budget: i64, // decreasing; refresh via bootstrap
57}
58
59struct FHECipher {
60 ctx: *FHECtx,
61 data: *u8, // scheme-specific blob
62 data_bytes: i64,
63 noise_used: i64, // consumed portion of budget
64}
65
66// Create a fresh context for a given scheme.
67func fhe_ctx_new(scheme: i64, plain_mod: i64, poly_n: i64) -> *FHECtx {
68 let raw: *u8 = sys_mmap(32)
69 let c: *FHECtx = raw as *FHECtx
70 c.scheme = scheme
71 c.plain_modulus = plain_mod
72 c.poly_degree = poly_n
73 c.noise_budget = 120 // typical starting budget (bits)
74 return c
75}
76
77// Encrypt a plaintext integer. Phase A produces a dummy
78// "ciphertext" that's just the plaintext XOR'd with a constant;
79// replace with real BFV encrypt when the full impl lands.
80func fhe_encrypt(c: *FHECtx, pt: i64) -> *FHECipher {
81 let raw: *u8 = sys_mmap(64)
82 let ct: *FHECipher = raw as *FHECipher
83 ct.ctx = c
84 ct.data_bytes = 8
85 let data: *u8 = sys_mmap(16)
86 ct.data = data
87 let data_i: *i64 = data as *i64
88 *data_i = pt ^ 0xA5A5A5A5
89 ct.noise_used = 0
90 return ct
91}
92
93// Decrypt. Phase A undoes the XOR.
94func fhe_decrypt(c: *FHECtx, ct: *FHECipher) -> i64 {
95 let data_i: *i64 = ct.data as *i64
96 return *data_i ^ 0xA5A5A5A5
97}
98
99// Homomorphic addition. Noise grows slowly (linear in # ops).
100func fhe_add(a: *FHECipher, b: *FHECipher) -> *FHECipher {
101 // Phase A: XOR the plaintext-xor'd ciphertexts. This works
102 // by coincidence for XOR-based scheme but misses the rich
103 // structure of real FHE.
104 let raw: *u8 = sys_mmap(64)
105 let r: *FHECipher = raw as *FHECipher
106 r.ctx = a.ctx
107 r.data_bytes = 8
108 let data: *u8 = sys_mmap(16)
109 r.data = data
110 let a_i: *i64 = a.data as *i64
111 let b_i: *i64 = b.data as *i64
112 let pa: i64 = *a_i ^ 0xA5A5A5A5
113 let pb: i64 = *b_i ^ 0xA5A5A5A5
114 let r_i: *i64 = data as *i64
115 *r_i = (pa + pb) ^ 0xA5A5A5A5
116 r.noise_used = a.noise_used + b.noise_used + 1
117 return r
118}
119
120// Homomorphic multiply. Noise grows multiplicatively.
121func fhe_mul(a: *FHECipher, b: *FHECipher) -> *FHECipher {
122 let raw: *u8 = sys_mmap(64)
123 let r: *FHECipher = raw as *FHECipher
124 r.ctx = a.ctx
125 r.data_bytes = 8
126 let data: *u8 = sys_mmap(16)
127 r.data = data
128 let a_i: *i64 = a.data as *i64
129 let b_i: *i64 = b.data as *i64
130 let pa: i64 = *a_i ^ 0xA5A5A5A5
131 let pb: i64 = *b_i ^ 0xA5A5A5A5
132 let r_i: *i64 = data as *i64
133 *r_i = (pa * pb) ^ 0xA5A5A5A5
134 r.noise_used = a.noise_used + b.noise_used + 20
135 return r
136}
137
138// How many bits of noise budget remain before a bootstrap
139// refresh becomes necessary.
140func fhe_budget_left(ct: *FHECipher) -> i64 {
141 return ct.ctx.noise_budget - ct.noise_used
142}
143
144// Bootstrap: reset noise budget via a costly homomorphic
145// re-encryption. Phase A is a no-op.
146func fhe_bootstrap(ct: *FHECipher) -> i64 {
147 ct.noise_used = 0
148 return 0
149}
150
151// Compile-only smoke.
152func main() -> i64 {
153 let ctx: *FHECtx = fhe_ctx_new(FHE_SCHEME_BFV, FHE_MAGIC_65537, FHE_MAGIC_4096)
154 if ctx.noise_budget != 120 { return 1 }
155
156 let e1: *FHECipher = fhe_encrypt(ctx, 5)
157 let e2: *FHECipher = fhe_encrypt(ctx, 7)
158 let sum: *FHECipher = fhe_add(e1, e2)
159 let prod: *FHECipher = fhe_mul(e1, e2)
160 if fhe_decrypt(ctx, sum) != 12 { return 2 }
161 if fhe_decrypt(ctx, prod) != 35 { return 3 }
162 if fhe_budget_left(sum) >= 120 { return 4 } // noise consumed
163 fhe_bootstrap(sum)
164 if sum.noise_used != 0 { return 5 }
165 return 0
166}