code wiki / (root) / nx_fhe.nx

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}