code wiki / (root) / fhe.nx

fhe.nx source

↩ module page · 158 lines · 5485 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 31import "syscalls.nx" 32 33const FHE_SCHEME_BFV: i64 = 1 34const FHE_SCHEME_BGV: i64 = 2 35const FHE_SCHEME_CKKS: i64 = 3 36const FHE_SCHEME_TFHE: i64 = 4 37 38const FHE_ERR_PENDING: i64 = -1 39const FHE_ERR_BUDGET: i64 = -2 40 41// Opaque handles. The actual ciphertext can be MB in size; 42// we keep a descriptor with metadata + a pointer to heap 43// storage. 44struct FHECtx { 45 scheme: i64, 46 plain_modulus: i64, 47 poly_degree: i64, // typically 2^12 .. 2^15 48 noise_budget: i64, // decreasing; refresh via bootstrap 49} 50 51struct FHECipher { 52 ctx: *FHECtx, 53 data: *u8, // scheme-specific blob 54 data_bytes: i64, 55 noise_used: i64, // consumed portion of budget 56} 57 58// Create a fresh context for a given scheme. 59func fhe_ctx_new(scheme: i64, plain_mod: i64, poly_n: i64) -> *FHECtx { 60 let raw: *u8 = sys_mmap(32) 61 let c: *FHECtx = raw as *FHECtx 62 c.scheme = scheme 63 c.plain_modulus = plain_mod 64 c.poly_degree = poly_n 65 c.noise_budget = 120 // typical starting budget (bits) 66 return c 67} 68 69// Encrypt a plaintext integer. Phase A produces a dummy 70// "ciphertext" that's just the plaintext XOR'd with a constant; 71// replace with real BFV encrypt when the full impl lands. 72func fhe_encrypt(c: *FHECtx, pt: i64) -> *FHECipher { 73 let raw: *u8 = sys_mmap(64) 74 let ct: *FHECipher = raw as *FHECipher 75 ct.ctx = c 76 ct.data_bytes = 8 77 let data: *u8 = sys_mmap(16) 78 ct.data = data 79 let data_i: *i64 = data as *i64 80 *data_i = pt ^ 0xA5A5A5A5 81 ct.noise_used = 0 82 return ct 83} 84 85// Decrypt. Phase A undoes the XOR. 86func fhe_decrypt(c: *FHECtx, ct: *FHECipher) -> i64 { 87 let data_i: *i64 = ct.data as *i64 88 return *data_i ^ 0xA5A5A5A5 89} 90 91// Homomorphic addition. Noise grows slowly (linear in # ops). 92func fhe_add(a: *FHECipher, b: *FHECipher) -> *FHECipher { 93 // Phase A: XOR the plaintext-xor'd ciphertexts. This works 94 // by coincidence for XOR-based scheme but misses the rich 95 // structure of real FHE. 96 let raw: *u8 = sys_mmap(64) 97 let r: *FHECipher = raw as *FHECipher 98 r.ctx = a.ctx 99 r.data_bytes = 8 100 let data: *u8 = sys_mmap(16) 101 r.data = data 102 let a_i: *i64 = a.data as *i64 103 let b_i: *i64 = b.data as *i64 104 let pa: i64 = *a_i ^ 0xA5A5A5A5 105 let pb: i64 = *b_i ^ 0xA5A5A5A5 106 let r_i: *i64 = data as *i64 107 *r_i = (pa + pb) ^ 0xA5A5A5A5 108 r.noise_used = a.noise_used + b.noise_used + 1 109 return r 110} 111 112// Homomorphic multiply. Noise grows multiplicatively. 113func fhe_mul(a: *FHECipher, b: *FHECipher) -> *FHECipher { 114 let raw: *u8 = sys_mmap(64) 115 let r: *FHECipher = raw as *FHECipher 116 r.ctx = a.ctx 117 r.data_bytes = 8 118 let data: *u8 = sys_mmap(16) 119 r.data = data 120 let a_i: *i64 = a.data as *i64 121 let b_i: *i64 = b.data as *i64 122 let pa: i64 = *a_i ^ 0xA5A5A5A5 123 let pb: i64 = *b_i ^ 0xA5A5A5A5 124 let r_i: *i64 = data as *i64 125 *r_i = (pa * pb) ^ 0xA5A5A5A5 126 r.noise_used = a.noise_used + b.noise_used + 20 127 return r 128} 129 130// How many bits of noise budget remain before a bootstrap 131// refresh becomes necessary. 132func fhe_budget_left(ct: *FHECipher) -> i64 { 133 return ct.ctx.noise_budget - ct.noise_used 134} 135 136// Bootstrap: reset noise budget via a costly homomorphic 137// re-encryption. Phase A is a no-op. 138func fhe_bootstrap(ct: *FHECipher) -> i64 { 139 ct.noise_used = 0 140 return 0 141} 142 143// Compile-only smoke. 144func main() -> i64 { 145 let ctx: *FHECtx = fhe_ctx_new(FHE_SCHEME_BFV, 65537, 4096) 146 if ctx.noise_budget != 120 { return 1 } 147 148 let e1: *FHECipher = fhe_encrypt(ctx, 5) 149 let e2: *FHECipher = fhe_encrypt(ctx, 7) 150 let sum: *FHECipher = fhe_add(e1, e2) 151 let prod: *FHECipher = fhe_mul(e1, e2) 152 if fhe_decrypt(ctx, sum) != 12 { return 2 } 153 if fhe_decrypt(ctx, prod) != 35 { return 3 } 154 if fhe_budget_left(sum) >= 120 { return 4 } // noise consumed 155 fhe_bootstrap(sum) 156 if sum.noise_used != 0 { return 5 } 157 return 0 158}