Skip to content
HN On Hacker News ↗

Obfuscation (Part II): Diamond iO

▲ 12 points 1 comments by elffjs 4w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is human-written.

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 1 of 1
SEGMENTS · AI 0 of 1
WORD COUNT 1,707
PEAK AI % 0% · §1
Analyzed
Jul 31
backend: pangram/v3.3
Segments scanned
1 windows
avg 1707 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,707 words · 1 segments analyzed

Human AI-generated
§1 Human · 0%

Dark Mode Toggle 2026 Jul 28 See all posts Obfuscation (Part II): Diamond iO Special thanks to Sora Suegami and Janmajaya Mall for feedback and review. In the last part of this series, we went through the full tech tree of the most mainstream and conservative line of cryptographic obfuscation (iO) protocols. These protocols allow you to "encrypt" programs in such a way that anyone can run the "encrypted" program on plaintext inputs and get plaintext outputs, without being able to see the internal logic of the program. This can be used for all kinds of use cases, particularly situations where the program contains some secret key internally, and obfuscating accomplishes the goal of giving someone a package that lets them do some things with the secret key in some circumstances, but not anything else. The biggest downside of these protocols so far has been their literally galactic runtime - if you actually try to figure out how long it would take to run one of them, you would get an answer longer than the lifetime of the universe. As a result, despite recent breakthroughs in feasibility, obfuscation protocols have so far been theoretical curiosities. This post will describe in detail a different style of obfuscation: diamond iO (paper, presentation). This approach relies on much "braver" and more untested cryptographic assumptions, but it achieves having merely "planetary", rather than galactic runtime - still infeasible today, but perhaps only a few further optimizations away from becoming a reality at least for a few use cases. Different types of obfuscation How does diamond iO work? At a high level, diamond iO is built by modifying the BGG+14 attribute-based encryption (ABE) scheme, explained in the previous post. I highly recommend re-reading that section before continuing. It's also not this Abe. Bonus points if you know which Abe this one is - harder than the four in the last post! Like, the more traditional iO protocols, diamond iO runs the computation in FHE inside of ABE, and then gives the evaluator a way to decrypt the result only if it's actually the outcome of running the computation correctly. But the way that diamond iO uses this machinery is different. First, it uses a completely different mechanism to do conditional FHE decryption. Second, it uses a completely different mechanism to generate the encodings for the input. These two modifications are connected to each other, and are the real reason why diamond iO manages to be much more efficient - it's "just as computationally intensive as" functional encryption, instead of being a much more complicated tower on top of FE. As a reminder, BGG+14 works by maintaining encodings of the form \(s * (B - G * m) + e\) Where: \(s\) is a secret \(B\) is a public matrix, of which there is a different one for each "wire" in the circuit \(m\) is the bit on that wire during the computation \(e\) is an "error" (alternatively, you can add \(G * m\) instead of subtracting; both give equally valid and efficient schemes) Given two \(B\) matrices representing two "input wires" to an operation - either addition, multiplication or negation - you can generate a \(B\) matrix representing the output wire. Given valid encodings for two inputs to an operation, you can get an encoding for the output - \(1 - m\), \(m_a + m_b\) or \(m_a * m_b\) - that is based on the \(B\) matrix representing the output wire. Importantly, this is not fully-homomorphic encryption: to multiply, you need to know either \(m_a\) or \(m_b\) in the clear. In BGG+14, there is a step at the end, which allows decoding a pre-selected output, only if the value in the computation on some "output" wire equals 0. Here, we do not do that. Instead, what we will do is just extract the data that we need from the encoding. But in both cases, the decryption method depends on the encoding being based on a specific matrix \(B_{final}\) representing the output wire - this is how we enforce that decryption can happen if you did the computation correctly, but not in any other context. The core of diamond iO is: Generate BGG+14 \(B\)-matrices and encodings representing the input \(x\), plus a few other things Run a computation, over these BGG+14 encodings, that converts \(x\) into an FHE ciphertext \(FHE.enc(f(x, z))\), where \(f\) is public and \(z\) is an internal hidden input that the obfuscator is trying to hide Run another slightly modified BGG+14 step to FHE-decrypt the output Finally, do a "trapdoor" step, borrowing similar machinery to BGG+14 decryption but using it in very different ways, to get the result - only in the situation where the circuit that you ran is the "correct" one The complexity of diamond iO rests in three places: What you do to the encoding of the output at the end, that allows this FHE decryption only in situations where the circuit that was computed is exactly the same circuit representing \(f\), and without fully leaking the key Some adjustments to \(f(x, z)\) to take into account the fact that this whole scheme is only secure if the outputs of the FHE decryption are fully uniformly distributed random-looking, and then convert it from "obfuscate \(f(x, z)\) hiding \(z\)" to "full iO that hides the program" How to let the evaluator construct the ABE encodings of the inputs, without giving away the secrets. The first two ideas came from prior work, particularly HLL23 and AKY24. The third piece, the mechanism for constructing the ABE encodings of the inputs, originally came from GGH15; the new contribution in diamond iO is to use it not to evaluate the whole program (which turned out insecure) but to generate BGG+ encodings for the inputs. We will tackle these three pieces in turn. The decryption step Assume for now that the evaluator somehow gets as input four types of BGG+ encodings of: \(1\) (this will be helpful later, for constructing these encodings) FHE encryptions of the fixed hidden input to \(f\), which we denote \(z\) (we'll denote the encrypted version \(E[z]\)) The public input bits: \(s * (B_{x_1} - x_1 * G) + e_{x_1} ... s * (B_{x_L} - x_L * G) + e_{x_L}\), where \(x_1 ... x_L\) is the input to \(f\) An FHE decryption key (which must be low-norm, and the last value must be -1), which we will call \(t\) We'll label this ensemble \([1, E[z], x, t]\). The evaluator first runs the BGG+ computation using the encodings of \(E[z]\) and \(x\). Remember, the computation is not \(f(x, z)\) directly, rather, it's \(FHE.eval(f, x, E[z])\). From the BGG+ perspective, \(x\) and \(E[z]\) are both cleartext bits. From the FHE perspective, \(z\) is a hidden input, of which only the encrypted form is known to the evaluator. At the end of doing the BGG+ computation, the evaluator has BGG+ encodings of bits of the FHE ciphertext representing \(f(x, z)\). BGG+ is not fully homomorphic encryption; in general, computing on BGG+ encodings requires having the underlying cleartext. But we can avoid this rule for additions, and for multiplications by known values. This is because a BGG+ encoding of \(m_a * m_b\) is computed via: \(c_{out} = m_b * c_a + c_b * G^{-1}(B_a)\) (If the encodings were adding \(G * m\) instead of subtracting, then it would be \(G^{-1}(-B_a)\) instead) We can allow \(m_a\) to be unknown (ie. given to us via encodings only), as long as \(m_b\) is known. This is an important fact for us. To see why, remember the structure of GSW decryption (also described in the previous post in this series): Because the evaluator knows all the bits of the actual execution trace, including the final FHE ciphertext (which we'll call \(Y\)), we can give the evaluator the FHE decryption key, \(t\), only as BGG+ encodings. Let's see how the decryption works. We want to compute \(t * Y\). We have only BGG+ encodings of \(t\), and we have both the BGG+ encodings and the raw bits of \(Y\). So in principle we can do it. But there's a problem: \(Y\) is given to us as a series of bits. That is, we don't get a vector of the form \([2, 6, 11...]\). We get a vector of the form \([0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1...]\), where each bucket of bits (in this example, 4 bits) is a binary-encoding of a number, by convention least-significant-bits first. So we have to somehow re-scale the bits, so that in every cell where we BGG+ multiply some cell \(t_j * Y_{\{i,j,b\}}\), where \(Y_{\{i,j,b\}}\) is the order \(2^b\) bit of the cell \(Y_{\{i,j\}}\), the encoding that comes out is scaled by \(2^b\). For convenience, let's group together encodings of bits that are of the same order and that will eventually be inside the same value in the answer: that is, the whole column \(t_j * Y_{\{i,j,b\}}\) for some specific \(i\) and \(b\) across all \(j\). \(\sum_j s * (B_{\{out,i,j,b\}} - t_j * Y_{\{i,j,b\}} * G) + e_{\{out,i,j,b\}}\) \(= s * (B_{\{out,i,b\}} - (tY)_{\{i,b\}} * G) + e_{\{out,i,b\}}\) Now we get to the puzzle: how do we combine together bits of different orders? One naive thing we could do is just rescale and add: \(\sum_{b=0}^{log(q)-1} (s * (B_{\{out,i,b\}} - (tY)_{\{i,b\}} * G) + e_{\{out,i,b\}}) * 2^b\) The problem with this is that it multiplies up the errors of the high-order digits too much: the error of the highest-order term would get multiplied by \(2^{log(q)-1} = \frac{q}{2}\) so it would flood the whole range. So here's what we do instead: \(\sum_{b=0}^{log(q)-1} (s * (B_{\{out,i,b\}} - (tY)_{\{i,b\}} * G) + e_{\{out,i,b\}}) * G^{-1}(2^bG)\) \(G^{-1}(2^bG)\) is doing all the work here. Basically, this is a bucket-wise left shift operator - it takes each \(log(q)\)-bit bucket \(tY\), and shifts them all to the left. Remember, BGG+ encodings are of the form \(s * (B - G * m) + e\). And remember that \(G\) has this form: This means that BGG+ encodings encode \(m\) simultaneously at every scale. And what we're doing here is we're moving higher-scale encodings of higher-order bits into the first column of each bucket. And because we're just doing additions and shifts, not multiplications, we avoid the