Visualização de leitura

We beat Google’s zero-knowledge proof of quantum cryptanalysis

Two weeks ago, Google’s Quantum AI group published a zero-knowledge proof of a quantum circuit so optimized, they concluded that first-generation quantum computers will break elliptic curve cryptography keys in as little as 9 minutes. Today, Trail of Bits is publishing our own zero-knowledge proof that significantly improves Google’s on all metrics. Our result is not due to some quantum breakthrough, but rather the exploitation of multiple subtle memory safety and logic vulnerabilities in Google’s Rust prover code. Google has patched their proof, and their scientific claims are unaffected, but this story reflects the unique attack surface that systems introduce when they use zero-knowledge proofs.

Google’s proof uses a zero-knowledge virtual machine (zkVM) to calculate the cost of a quantum circuit on three key metrics. The total number of operations and Toffoli gate count represent the running time of the circuit, and the number of qubits represents the memory requirements. Google, along with their coauthors from UC Berkeley, the Ethereum Foundation, and Stanford, published proofs for two circuits; one minimizes the number of gates, and the other minimizes qubits. Our proof improves on both.

Resource Type Google’s Low-Gate Google’s Low-Qubit Our Proof
Total Operations 17,000,000 17,000,000 8,300,000
Number of Qubits 1,425 1,175 1,164
Toffoli Count 2,100,000 2,700,000 0

Table 1: Resource upper bounds reported in different proofs for circuits computing the correct output across 9,024 randomly sampled inputs

Our proof fully verifies when using Google’s unpatched verification code. It has the same verification key as their original proofs and is cryptographically indistinguishable from a zero-knowledge proof resulting from actual algorithmic improvements to the quantum circuit. We are releasing the code we developed to forge the proof, and a summary of our proof follows.

Circuit SHA-256 hash: 0x7efe1f62bb14a978322ab9ed41d670fc0fe0f211331032615c910df5a540e999

Groth16 proof bytes: 0x0e78f4db0000000000000000000000000000000000000000000000000000000000000000008cd56e10c2fe24795cff1e1d1f40d3a324528d315674da45d26afb376e8670000000000000000000000000000000000000000000000000000000000000000024ac7f8dd6b1de6279bcce54e8840d8eb20d522bf27dedd776046f6590f33add217db465201c63724e6b460641985543d2b79c3c54daeea688581676a786aafc1dba8604a361acdd9809e268b6d8bc73943a713bb0ed0d96221f73d26def6ea4041d05b077523d9351a48b2ecd984c686b6473df69d20a24296d0a1cba3cdbe92eb13a7cc0ecd92f27f7bf23f9ac859d4293e17216dcbd85d1c7f60a52f65a9d02faef077336acd39e845d534200b575b029d6e3f0afb4f90815557233eab70b0fe88919834dd9beb90d47241f1490dc202e0dce44e4894982b07073c8d4426513732d79e9af9913b254aa29471e1a98fa1b43a1886afb5dbd36988153217aa2

Verification key: 0x00ca4af6cb15dbd83ec3eaab3a0664023828d90a98e650d2d340712f5f3eb0d4

Zero-knowledge virtual machines

Google used Succinct Labs’ SP1 zkVM for their proofs. A zkVM is essentially a way to prove that you know which private inputs for an arbitrary guest program on the zkVM generate some public output. For example, consider this basic Rust guest program.

#![no_main]
sp1_zkvm::entrypoint!(main);

pub fn main() {
 // Read in private inputs a and b
 let a = sp1_zkvm::io::read::<u32>();
 let b = sp1_zkvm::io::read::<u32>();
 // Add them together
 let c = a + b;
 // Write the public output a + b
 sp1_zkvm::io::commit(&c);
}

A user can take the private inputs 2 and 3, run this program on the zkVM, and get a proof that the program ran successfully and that the output was 5. Anyone can verify the proof, but they would get zero knowledge about whether the input was (2, 3), (1, 4), or (6, 0xffffffff). Obviously, this toy problem is simple; real programs can be significantly more complicated.

Behind the scenes, the Rust guest program compiles down to a RISC-V ELF binary. This simple architecture allows complex program logic to be encoded into provable mathematical relationships. For example, the state of the RISC-V registers after executing an instruction is a deterministic function of their state before execution. Having to prove every step makes generating zkVM proofs resource-intensive and costly, but significant engineering work has enabled proving statements about complex programs.

Google’s zkVM guest

In the case of Google’s zero-knowledge proofs, the private input is the quantum circuit (in a custom assembly language), and the program is a simulator that checks the circuit. Note that these are “circuits” in the quantum sense, not the typical zero-knowledge definition. The public output includes bounds on the number of qubits and gate operations. In general, simulating quantum circuits is difficult, but the “kickmix” circuits defined in this paper refer to a specific subset that can be tested classically.

The following script, adapted from one of Google’s examples, increments a 3-qubit value. It includes three operations and a total of three qubits. Note that the first instruction CCX has two inputs (q0 and q1) and computes q2 = q2 ^ (q0 & q1). This is called a Toffoli gate. Toffoli gates are quite useful, but they’re much harder to implement on actual quantum hardware, so the complexity of quantum algorithms is sometimes measured in the number of Toffoli gates (or more accurately, non-Clifford gates). Circuits like this are serialized into bytes and sent to the zkVM simulator.

# Increment a value held in 3 qubits (q2, q1, q0). Sends
# (0, 0, 0) -> (0, 0, 1)
# (0, 0, 1) -> (0, 1, 0)
# ...
# (1, 1, 1) -> (0, 0, 0)

# If q0 and q1 are set, flip q2.
CCX q0 q1 q2
# If q0 is set, flip q1.
CX q0 q1
# Flip q0.
X q0

To verify that a circuit computes the correct function, the simulator deserializes the circuit, randomly initializes the qubits (e.g., to (1, 0, 1)), iteratively applies every operation in the circuit, and panics unless the final state is as expected (e.g., (1, 1, 0)). The simulator repeats this for many different inputs (9,024 times, to be precise), so proving that the simulator terminated without error is essentially the same as proving that the circuit is correct with high probability. In Google’s zkVM program, the circuit must compute one elliptic curve point addition, a critical subroutine of Shor’s algorithm for solving the elliptic curve discrete logarithm problem.

In addition to checking that the circuit computes the correct function, it also counts the total number of operations, the number of qubits, and the average number of Toffoli gates (some Toffoli gates are conditioned on classical bits and may be skipped during simulation). These performance metrics are checked to ensure they do not exceed specified upper bounds; if they don’t, the upper bounds are committed as public output.

Plan of attack

Since Google’s zero-knowledge proof comes from the results of running a Rust simulator on a private kickmix assembly script, we can create our own zero-knowledge proof by providing our own private input to the same program. If we find some input that causes the simulator to misreport the quantum costs, we’ll have successfully forged a proof. To beat Google’s results on any metric, we have the following goals:

  • Must compute elliptic curve point addition correctly
  • Preferably reports fewer than 17 million total operations
  • Preferably reports fewer than 2.1 million Toffoli gates
  • Preferably reports fewer than 1,175 qubits

This turns a quantum computing problem into an application security problem. Any deserialization bugs when parsing the kickmix circuit input are fair game, as well as any logic bugs we find in the simulator.

Vulnerability 1: Bypassing the Toffoli counter

One area of concern in the Rust source code was the use of unsafe blocks, disabling important memory safety checks. This was presumably done to reduce the overall cycle count of the zkVM guest program; each additional bounds check inflates the already substantial cost of generating a zero-knowledge proof, particularly checks that run millions of times. The vulnerability starts in the following two lines of code from program/src/main.rs.

let private_circuit_bytes = sp1_zkvm::io::read_vec();
let ops = unsafe {
 rkyv::access_unchecked::<rkyv::Archived<Vec<Op>>>(&private_circuit_bytes)
};

The first line shows that private circuit bytes (private_circuit_bytes) are directly read from outside the zkVM, and the use of the rkyv serialization library’s access_unchecked function instructs the library to assume that private_circuit_bytes corresponds to a valid serialization. But data from outside the zkVM is untrusted, so what happens if the bytes, which are meant to represent a vector of circuit operations, are malformed?

The answer is “not much.” There are relative pointer offsets and length fields in the serialization for the Vec type, but I couldn’t see a viable path from manipulating those to getting the prover to underreport resource counts. The Op type is similarly simple, consisting of seven 32-bit fields: one describes the OperationType, and six describe the identifiers of which qubits and classical bits to use as inputs and outputs for the operation. For a while, I was chasing down a bug in how the magic identifier 0xffffffff could bypass the qubit count and trigger an out-of-bounds write in the array of simulated qubit values. I was deep in the details of understanding the Rust heap allocator used by the SP1 zkVM before a colleague pointed out that Google was using SP1’s 64-bit RISC-V architecture rather than the potentially exploitable 32-bit architecture.

That left the kind field, an enum describing which of the 18 supported kickmix OperationType opcodes to apply. When simulating the quantum circuit, the guest program iterates over the vector of operations and determines whether to conditionally execute each operation; if so, it increments the count of Toffoli or Clifford gates, depending on the operation type, and executes the operation. This code is in Simulator::apply_iter.

match op.kind {
 OperationType::CCZ | OperationType::CCX => {
 self.stats.toffoli_gates += executed_shots;
 }
 OperationType::CX
 | OperationType::CZ
 | OperationType::Swap
 | OperationType::R
 | OperationType::Hmr => {
 self.stats.clifford_gates += executed_shots;
 }
 // Note: X and Z are not considered Clifford gates in the
 // stats because they can be tracked in the classical control system.
 // They don't need to cause something to happen on the quantum computer.
 _ => {}
}

match op.kind {
 OperationType::CCX => {
 let v = cond & self.qubit(op.q_control1) & self.qubit(op.q_control2);
 *self.qubit_mut(op.q_target) ^= v;
 }
 OperationType::CX => {
 let v = cond & self.qubit(op.q_control1);
 *self.qubit_mut(op.q_target) ^= v;
 }

What if op.kind falls outside of the expected 0–17 range because rkyv was instructed not to check this value during deserialization? This is undefined behavior, so to investigate, I used Ghidra to reverse-engineer the RISC-V ELF binary Google provided with their proof.

After identifying the location of this function in the binary, I discovered that the Rust compiler emits a pair of jump tables for these two match expressions. The first jump table determines which gate counter to increment, and the second performs the actual operation. But we maliciously control the value of op.kind, so what if instead of the normal behavior, we dereference past the end of the first jump table and directly jump to an address from the second jump table? Then an out-of-range OperationType could still perform the correct operation, but it would completely bypass the Toffoli counter!

“Figure 1: In this simplified execution flow, providing an invalid operation type bypasses the Toffoli counter, giving the same functionality while hiding the true cost.”
Figure 1: In this simplified execution flow, providing an invalid operation type bypasses the Toffoli counter, giving the same functionality while hiding the true cost.

I calculated the necessary offsets, modified Google’s example prover code to inject the invalid operation types, and attempted to simulate a zero-knowledge proof of a simple 64-qubit adder circuit. To my surprise, it worked on the first try.

stdout: circuit.average_cliffords_performed() = 0
stdout: circuit.average_non_cliffords_performed() = 0
stdout: The circuit passed fuzz testing.

I had been concerned that the RISC-V registers would be in an invalid state when jumping into the wrong table, but this ended up not being the case. Now I had the primitive I needed to forge a circuit that misreports the number of Toffoli gates, and I just had to scale up my attack on the 64-qubit adder circuit to full elliptic curve point addition.

Building a quantum circuit

I now had a virtually unlimited budget for Toffoli operations, and the path forward looked simple. I could implement any kickmix circuit that correctly performs elliptic curve point addition without worrying about the Toffoli count, tweak the operation types before feeding the script to the prover, and then forge a proof for whatever Toffoli upper bound I wanted. I might use more total operations or more qubits than Google’s circuits, but it would be an amusing proof of concept. The only concern was that the prover’s running time is proportional to the total number of operations, so my circuit still needed a reasonably low operation count.

It turns out that programming a quantum computer is way more challenging than I anticipated, and this is because of the requirements of reversibility and uncomputation.

Requirement 1: Reversibility. A quantum circuit is made up of a series of reversible (unitary) gates. For kickmix circuits, think of these as reversible bit operations. For example, c’ = c XOR b is allowed because the original value of c can be recovered with c = c’ XOR b. On the other hand, c’ = c AND b is not allowed because if c’ and b are both 0, we cannot know if c was originally 0 or 1. By itself, AND is not reversible, but with an additional input in Toffoli gates, it is. The kickmix Toffoli operation CCX q1 q2 q3 updates q3 to q3’ = q3 XOR (q1 AND q2), and this operation can be reversed with q3 = q3’ XOR (q1 AND q2).

Requirement 2: Uncomputation. To avoid the undesirable effects of entanglement, any auxiliary (or ancilla) qubits used to store intermediate results of computation must be “uncomputed,” or reset to state 0. The reversibility requirement makes this a challenge, since the intermediate result may have been 0 or 1. The intermediate state must be uncomputed from the computation result in order to be reversibly cleared out.

As we try to build our reversible elliptic curve point addition circuit with uncomputation, a couple of tools are available. We could use Bennett’s trick, which involves preserving inputs and outputs in spare qubits, then running the full computation a second time in reverse to clear ancilla qubits. This approach isn’t ideal because it roughly doubles the operation count for each level of the call stack. Another approach is to use the more efficient measurement based uncomputation. Google has revealed that this is the technique their circuits use, but it requires a much finer-grained algorithmic analysis to apply correctly.

Vulnerability 2: Efficient operations with register aliasing

After struggling to implement elliptic curve point addition while keeping the operation count and qubit count low, I discovered another exploitable vulnerability: register aliasing. Recall the Toffoli (CCX) operation defined in Simulator::apply_iter.

OperationType::CCX => {
 let v = cond & self.qubit(op.q_control1) & self.qubit(op.q_control2);
 *self.qubit_mut(op.q_target) ^= v;
}

There’s no check that the qubit inputs (op.q_control1 and op.q_control2) are different from the qubit output (op.q_target), so tying all three together becomes q1 = q1 ^ (q1 & q1) = 0. That is, we can immediately reset a qubit to zero, violating the quantum requirement of reversibility and making uncomputation trivial.1

“Figure 2: By setting the output of a kickmix operation to the input, we can build circuits that violate quantum reversibility and implement arbitrary classical logic gates.”
Figure 2: By setting the output of a kickmix operation to the input, we can build circuits that violate quantum reversibility and implement arbitrary classical logic gates.

In addition, we can use this primitive to create any logical gate we want, like the classical AND gate that violates reversibility or the functionally complete NAND gate. Now that I don’t have to deal with the limitations of quantum circuits, it’s basically Nand2Tetris, except the goal is elliptic curve point addition. I implemented basic logic gates, followed by integer addition and subtraction, modular addition, modular multiplication, modular inversion, and, finally, point addition.

After exploiting a memory corruption issue in unsafe Rust code, implementing elliptic curve operations from the ground up using individual logic gates, and squeezing whatever performance I could out of the non-quantum aspects of the design, I finally had a working kickmix script that passed validation. 0 Toffolis, 8 million operations, and 1288 qubits. This beats one of Google’s two proofs but falls short of beating the other one by just 113 qubits.

If I wanted to truly claim that our zero-knowledge proof beat Google’s, I couldn’t leave it there. I needed to find some way to shave off 113 qubits, but I was all out of vulnerabilities.

The final challenge: Euclidean algorithm optimization

Profiling my circuit made it clear that the most expensive operation was modular inversion, and the same is true for many published quantum elliptic curve addition circuits. My optimized circuit required 4 field elements (1024 qubits) for the inversion, including some tricks to store intermediate field elements, and a handful of qubits for control flags and carry bits. If I were to beat Google’s proof, I needed to lose those tricks and do modular inversion using fewer than 2.59 field elements.

One idea is to use Fermat’s little theorem: $x^{-1} \equiv x^{p-2} \pmod{p}$. We replace inversion with exponentiation, which is just a sequence of modular multiplications. Each multiplication requires three field elements, and this approach requires hundreds of multiplications, well beyond our total qubit and operations budget.

What many quantum circuits use instead is a variant of the extended Euclidean algorithm (EEA). To compute $x^{-1} \pmod{p}$, this algorithm involves four variables $(a, u, b, v)$ initialized to $(x, 1, p, 0)$. The algorithm proceeds through several iterations to cancel out bits of $a$ and $b$, perform the same operations to $u$ and $v$, and (assuming $x$ and $p$ are coprime) the algorithm terminates with $(a, u, b, v) = (0, 0, 1, x^{-1})$.

I based my implementation on the binary EEA, a variant that involves canceling out the least significant bits of a and b rather than the standard most significant bits. Thanks to Thomas Pornin’s clear exposition of this algorithm, it was relatively easy to reimplement a high-performance version in my circuit, but the qubit overhead was still too high.

Next, I found this recent preprint by Han Luo, Ziyi Yang, Ziruo Wang, Yuexin Su, and Tongyang Li, which came out just days after Google’s announcement. It describes a method to compute modular inverses with the space equivalent of 3 field elements. Many of the techniques went above my head, but they open-sourced their code, so I had a much easier time understanding their paper. Their code included a Qiskit circuit, but I was unsuccessful in integrating this into my exploit. Despite these difficulties, the paper gave me the key term I would need to shave off the remaining qubits: Proos-Zalka register sharing.

The 2003 paper by John Proos and Christof Zalka recognizes that over the course of the standard EEA, the bit-lengths of a and b gets smaller, while the bit-lengths of u and v get larger. Their register-sharing algorithm saves space by limiting the number of qubits for each value at each iteration. This can fail with low probability, but rare failures are tolerable when doing Shor’s algorithm. I implemented a classical version of the register-sharing algorithm of Proos and Zalka, and I ended up with 30 million total operations, almost twice Google’s result.

Finally, I had the insight I needed. What if I combined the operation efficiency of the binary EEA with the space efficiency of the Proos-Zalka algorithm? The binary EEA doesn’t have the same bounds on u and v as the standard EEA, but a slight tweak (doubling v instead of halving u) does, and needs only a simple correction factor at the end. This idea is deeply connected to Kaliski’s method, which is considered in papers by Roetteler et al., Gouzien et al., Häner et al., and Litinski. Reversibility constraints require an extra qubit for each of about 512 iterations, but our implementation doesn’t need to be reversible.

“Figure 3: The first 20 and last 5 rounds of the modified binary EEA depict how different variables can share space when performing modular inversion. A final correction factor is not applied here.”
Figure 3: The first 20 and last 5 rounds of the modified binary EEA depict how different variables can share space when performing modular inversion. A final correction factor is not applied here.

Thanks to register sharing, my final modular inversion requires the space of only 2.55 field elements, barely less than the 2.59 required. In total, my elliptic curve point addition circuit uses 8,288,880 operations, 1,164 qubits, 5,980,691 pre-bypass Toffoli gates, and 0 reported Toffoli gates. This is less than half the reported operations in Google’s circuits and just a few qubits fewer than their best variant. The source code for generating this proof of concept is available here.

What Google’s secret circuit (probably) does

The zero-knowledge properties of the proof makes this unanswerable, but framed in a different way, we can answer what problems are documented in prior work that Google would have to overcome to achieve their results.

Google’s circuit does elliptic curve point addition, which requires at least one modular division. In previous circuits, modular inversion is the most expensive step in terms of gate count and qubit count, so that’s where improvements are needed most. Our register-sharing implementation shows that 2.55 field elements of storage is enough for a nonreversible circuit, but prior quantum implementations of Kaliski’s EEA variant require an extra qubit per iteration to preserve reversibility. This adds 512 qubits of overhead to guarantee that modular inversion is invertible, and a circuit based on Kaliski’s method with Google’s qubit counts would need to solve this problem.

Even the most revolutionary scientific breakthroughs are rooted in published literature, and I think a healthy understanding of prior work can help demystify the risk of a shadowy adversary destabilizing cryptocurrencies with a secret algorithm.

The aftermath

Zero-knowledge proofs are a transformational new technology with wide-ranging impacts, and their application to vulnerability disclosure is still new. Without knowing the details of their circuit, it’s impossible for me to conclude whether Google’s decision to announce this discovery using a zero-knowledge proof is justified. However, I do have experience with both vulnerability disclosure and academic publishing, and this points to broader implications in the deployment of zero-knowledge technology.

One potentially overlooked aspect of coordinated disclosure is the importance of an embargo period. Current industry best practices recommend a 30-day buffer between a timely patch becoming available and full disclosure of the technical details. This allows time for patch adoption, benefits defenders who rely on the technical details, and prevents opportunistic exploitation by low-skill attackers. Zero-knowledge proofs can communicate the importance of patching, but they are not a cryptographic replacement for the benefits of eventual disclosure.

In academic publishing, the more details that are available in published work, the easier it is to improve upon that work. Papers that intentionally facilitate replication and have a clear statement of methods and claims are usually the ones that are later cited and have the greatest impact. Using a zero-knowledge proof still establishes improvement over prior work; it also indicates a confidence that no one else will independently develop the same improvement, and that no one but the authors will be able to improve upon the discovery in future work.

As a direct example of the value of open publishing, I want to highlight Google’s decision to release a well-documented kickmix simulator and thorough proof generation instructions. This is the sole reason I was able to find and demonstrate the vulnerabilities, and their patches simultaneously increase confidence in their zero-knowledge claims while preventing attackers from forging proofs of quantum breakthroughs that spread fear, uncertainty, and doubt.

Zero-knowledge systems are an incredible technology with many applications, but their use introduces a different set of risks than traditional approaches. They aren’t a magic wand that eliminates trust; instead, they redistribute trust from an original domain, such as the opinions of scientific experts, to trust in programming languages, compilers, proof systems, and cryptography experts. There are many frontiers that are considering the benefits of zero-knowledge, including electronic voting and age verification, but it’s also critical to consider the risks and make plans for what happens when this technology fails.

Acknowledgments

Thank you to Craig Gidney, Ryan Babbush, Tanuj Khattar, and Adam Zalcman from Google for their quick response and for putting up with my naive questions about quantum algorithms, and to Sophie Schmieg for putting us in touch. Finally, this would not have happened without Joe Doyle and the wider Trail of Bits cryptography team, whose suggestions and enthusiasm pushed this project over the finish line.


  1. There’s a second bug in the HMR and R instructions, which are meant to reset a qubit to 0 while randomizing the phase. An error in conditional logic makes it possible to reset the qubit without trashing the phase, but register aliasing is a strictly better exploit primitive. ↩︎

The post We beat Google’s zero-knowledge proof of quantum cryptanalysis appeared first on Security Boulevard.

What we learned about TEE security from auditing WhatsApp’s Private Inference

WhatsApp’s new “Private Inference” feature represents one of the most ambitious attempts to combine end-to-end encryption with AI-powered capabilities, such as message summarization. To make this possible, Meta built a system that processes encrypted user messages inside trusted execution environments (TEEs), secure hardware enclaves designed so that not even Meta can access the plaintext. Our now-public audit, conducted before launch, identified several vulnerabilities that compromised WhatsApp’s privacy model, all of which Meta has patched. Our findings show that TEEs aren’t a silver bullet: every unmeasured input and missing validation can become a vulnerability, and to securely deploy TEEs, developers need to measure critical data, validate and never trust any unmeasured data, and test thoroughly to detect when components misbehave.

The challenge of using AI with end-to-end encryption

WhatsApp’s Private Processing attempts to resolve a fundamental tension: WhatsApp is end-to-end encrypted, so Meta’s servers cannot read, alter, or analyze user messages. However, if users also want to opt in to AI-powered features like message summarization, this typically requires sending plaintext data to servers for computationally expensive processing. To solve this, Meta uses TEEs based on AMD’s SEV-SNP and Nvidia’s confidential GPU platforms to process messages in a secure enclave where even Meta can’t access them or learn meaningful information about the message contents.

The stakes in WhatsApp are high, as vulnerabilities could expose millions of users’ private messages. Our review identified 28 issues, including eight high-severity findings that could have enabled attackers to bypass the system’s privacy guarantees. The following sections explore noteworthy findings from the audit, how they were fixed, and the lessons they impart.

Key lessons for TEE deployments

Lesson 1: Never trust data outside your measurement

In TEE systems, an “attestation measurement” is a cryptographic checksum of the code running in the secure enclave; it’s what clients check to ensure they’re interacting with legitimate, unmodified software. We discovered that WhatsApp’s system loaded configuration files containing environment variables after this fingerprint was taken (issue TOB-WAPI-13 in the report).

This meant that a malicious insider at Meta could inject an environment variable, such as LD_PRELOAD=/path/to/evil.so, forcing the system to load malicious code when it started up. The attestation would still verify as valid, but the attacker’s malicious code would be running inside, potentially violating the system’s security or privacy guarantees by, for example, logging every message being processed to a secret server.

Meta fixed this by strictly validating environment variables: they can now contain only safe characters (alphanumeric plus a few symbols like dots and dashes), and the system explicitly checks for dangerous variables like LD_PRELOAD. Every piece of data your TEE loads must either be part of the measured boot process or be treated as potentially hostile.

Lesson 2: Do not trust data outside your measurement (have we already mentioned this?)

ACPI tables are configuration data that inform an operating system about the available hardware and how to interact with it. We found these tables weren’t included in the attestation measurement (TOB-WAPI-17), creating a backdoor for attackers.

Here’s why this matters: a malicious hypervisor (the software layer that manages virtual machines) could inject fake ACPI tables defining malicious “devices” that can read and write to arbitrary memory locations. When the secure VM boots up, it processes these tables and grants the fake devices access to memory regions that should be protected. An attacker could use this to extract user messages or encryption keys directly from the VM’s memory, and the attestation report will still verify as valid and untampered.

Meta addressed this by implementing a custom bootloader that verifies ACPI table signatures as part of the secure boot process. Now, any tampering with these tables will change the attestation measurement, alerting clients that something is wrong.

Lesson 3: Correctly verify security patch levels

AMD regularly releases security patches for its SEV-SNP firmware, fixing vulnerabilities that could allow attackers to compromise the secure environment. The WhatsApp system did check these patch levels, but it made an important error: it trusted the patch level that the firmware claimed to be running (in the attestation report), rather than verifying it against AMD’s cryptographic certificate (TOB-WAPI-8).

An attacker who had compromised an older, vulnerable firmware could simply lie about their patch level. Researchers have publicly demonstrated attacks that can extract encryption keys from older SEV-SNP firmware versions. An attacker could use these published techniques against WhatsApp users to exfiltrate secret data while the client incorrectly believes it’s connected to a secure, updated system.

Meta’s solution was to validate patch levels against the VCEK certificate’s X.509 extensions. These extensions are cryptographically signed data from AMD that can’t be forged by compromised firmware.

Lesson 4: Attestations need freshness guarantees

Before our review, when a client connected to the Private Processing system, the server would generate an attestation report proving its identity, but this report didn’t include any timestamp or random value from the client (TOB-WAPI-7). This meant that an attacker who compromised a TEE once could save its attestation report and TLS keys, then replay them indefinitely.

Achieving a one-time compromise of a TEE is typically much more feasible and much less severe than a persistent compromise affecting each individual session. For example, consider an attacker who can extract TLS session keys through a side channel attack or other vulnerability. For a single attack, the impact tends to be short-lived, as the forward security of TLS makes the exploit impactful for only a single TLS session. However, without freshness, that single success becomes a permanent backdoor because the TEE’s attestation report from that compromised session can be replayed indefinitely. In particular, the attacker can now run a fake server anywhere in the world, presenting the stolen attestation to clients who will trust it completely. Every WhatsApp user who connects would send their messages to the attacker’s server, believing it’s a secure Meta TEE.

Meta addressed this issue by including the TLS client_random nonce in every attestation report. Now each attestation is tied to a specific connection and can’t be replayed.

How Meta fixed the remaining issues

Before their launch, Meta resolved 16 issues completely and partially addressed four others. The remaining eight unresolved issues are low- and informational-severity issues that Meta has deliberately not addressed. Meta provided a justification for each of these decisions, which can be reviewed in appendix F of our audit report. In addition, they’ve implemented broader improvements, such as automated build pipelines with provenance verification and published authorized host identities in external logs.

Beyond individual vulnerabilities: Systemic challenges in TEE deployment

While Meta has resolved these specific issues, our audit revealed the need to solve more complex challenges in securing TEE-based systems.

Physical security matters: The AMD SEV-SNP threat model doesn’t fully protect against advanced physical attacks. Meta needed to implement additional controls around which CPUs could be trusted (TOB-WAPI-10). If you are interested in a more detailed discussion on physical attacks targeting these platforms, check out our webinar, which discusses recently published physical attacks targeting both AMD SEV-SNP and Intel’s SGX/TDX platforms.

Transparency requires reproducibility: For external researchers to verify the system’s security, they need to be able to reproduce and examine the CVM images. Meta has made progress in this area, but achieving full reproducibility remains challenging, as issue TOB-WAPI-18 demonstrates.

Complex systems need comprehensive testing: Many of the issues we found could have been caught with negative testing, specifically testing what happens when components misbehave or when malicious inputs are provided.

The path forward for securely deploying TEEs

Can TEEs enable privacy-preserving AI features? Our audit suggests the answer is yes, but only with rigorous attention to implementation details. The issues we found weren’t fundamental flaws in the TEE model but rather implementation and deployment gaps that a determined attacker could exploit. These are subtle flaws that other TEE deployments are likely to replicate.

This audit shows that while TEEs provide strong isolation primitives, the large host-guest attack surface requires careful design and implementation. Every unmeasured input, every missing validation, and every assumption about the execution environment can become a vulnerability. Your system is only as secure as your TEE implementation and deployment.

For teams building on TEEs, our advice is clear: engage security reviewers early, invest in comprehensive testing (especially negative testing), and remember that security in these systems comes from getting hundreds of details right, not just the big architectural decisions.

The promise of confidential computing is compelling. But, as this audit shows, realizing that promise requires rigorous attention to security at every layer of the stack.

For more details on the technical findings and Meta’s fixes, see our full audit report. If you’re building systems with TEEs and want to discuss security considerations, we offer free office hours sessions where we can share insights from our extensive experience with these technologies.

The post What we learned about TEE security from auditing WhatsApp’s Private Inference appeared first on Security Boulevard.

Try our new dimensional analysis Claude plugin

We’re releasing a new Claude plugin for developing and auditing code that implements dimensional analysis, a technique we explored in our most recent blog post. Most LLM-based security skills ask the model to find bugs. Our new dimensional-analysis plugin for Claude Code takes a different approach: it uses the LLM to annotate your codebase with dimensional types, then flags mismatches mechanically. In testing against real audit findings, it achieved 93% recall versus 50% for baseline prompts.

You can download and use our new dimensional-analysis plugin by running these commands:

claude plugin marketplace add trailofbits/skills
claude plugin install dimensional-analysis@trailofbits
claude /dimensional-analysis

How our plugin differs from most skills

This plugin release is quite different from the wave of security analysis skills released over the past few weeks. The skills we’ve seen tend to take a relatively simple approach, where the LLM is primed with a set of vulnerability classes, exploration instructions, and example findings, and is then told to try to identify bugs within the scope of the skill.

Unfortunately, these approaches tend to produce low-quality results, with precision, recall, and determinism that is often much poorer than simply asking an LLM to “find the bugs in this project.”

What makes dimensional-analysis different is that instead of relying on LLM judgement to search for, identify, and rank vulnerabilities, it uses the LLM as a vocabulary-building/categorization machine that directly annotates the codebase. If the annotations are correct and a dimensional bug is present, that bug shows up as a mismatch between annotations instead of having to rely on an LLM’s judgement to determine how viable a finding is. In effect, this changes the calculus of how the LLM’s reasoning capability is being used, and produces much better results than baseline prompts that overly rely on LLM reasoning capabilities.

Benchmarking

We tested dimensional-analysis against a set of dimensional mismatch issues found during several unpublished audits and compared it to a baseline prompt using 10 samples per codebase. For this evaluation, the dimensional-analysis plugin had a recall rate of 93% with a standard deviation of 12%, versus the baseline prompt, which had a recall rate of 50% with a standard deviation of 20%. This means that dimensional-analysis performed both better and more consistently than the baseline prompt.

How it works

If you haven’t already, read our first blog post on the dimensional analysis technique. The plugin works over four main phases: dimension discovery, dimension annotation, dimension propagation, and dimension validation.

In the first phase, a subagent performs dimension discovery, with the goal of identifying a vocabulary of fundamental base units that every numerical term in the system is composed of. During this process, it also identifies a set of common derived units for quick reference by later agents.

Figure 1: A sample of a dimensional vocabulary for a protocol using Uniswap v4 hooks
Figure 1: A sample of a dimensional vocabulary for a protocol using Uniswap v4 hooks

The dimensional vocabulary is persisted to DIMENSIONAL_UNITS.md, where it can be read by other agents or used during development if you choose to make the annotations a permanent part of your software development lifecycle.

In the second phase, a group of subagents is launched to directly annotate the codebase using the dimensional vocabulary. Each subagent is provided with the DIMENSIONAL_UNITS.md file, a batch of files to annotate, and instructions to annotate state variables, function arguments, variable declarations, and any portions of complex arithmetic. These initial annotations are called “anchor” annotations.

} else if (currentPrice < peakPrice) {
 // D18{1} = (D18{price} - D18{price}) * D18{1} / (D18{price} - D18{price})
 imbalance =
 ((peakPrice - currentPrice) * imbalanceSlopeData.imbalanceSlopeBelowPeak) /
 (peakPrice - eclpParams.alpha.toUint256());
} else {
 // D18{1} = (D18{price} - D18{price}) * D18{1} / (D18{price} - D18{price})
 imbalance =
 ((currentPrice - peakPrice) * imbalanceSlopeData.imbalanceSlopeAbovePeak) /
 (eclpParams.beta.toUint256() - peakPrice);
}
Figure 2: A sample of annotated arithmetic from Balancer v3

In the third phase, dimensions are “propagated” across each file to callers and callees. This phase adds extra annotations to low-priority files that didn’t receive annotations on the first pass, and performs the first set of checks to make sure that dimensions agree within the same code context and across files.

It’s important to note that a dimensional mismatch at this stage doesn’t necessarily mean a vulnerability is present; sometimes it’s not possible to infer the precise dimension of a called function argument without reading the implementation of the function itself, and the system will over-generalize or make a poor guess. This third phase attempts to “repair” these over-generalized annotations and, if repair is not possible, flags them for triage in the final step.

In the fourth and final phase, the plugin attempts to discover mismatches and perform triage. Dimensional mismatching is checked for during assignment, during arithmetic, across function boundaries, across return paths, and across external calls. Dimensional mismatches are compared against a severity classification based on the nature of the mismatch, and a final report is returned to the user.

What’s next?

If you’re a developer working on an arithmetic-heavy project like a smart contract or blockchain node, we highly recommend running this plugin, then committing DIMENSIONAL_UNITS.md along with all of the annotations created by the plugin. Besides finding bugs, these annotations can greatly improve how long it takes to build a thorough understanding of a complex codebase and help improve both human and LLM understanding of the semantic meaning of your project’s arithmetic expressions.

While new tools are exciting, at this time we don’t believe that this tool can find every source of dimensional error. LLMs are probabilistic, which means there is always going to be some level of error. We’re interested in improving this plugin wherever possible, so if you run it and it misses a dimensional error, please open an issue on our GitHub.

The post Try our new dimensional analysis Claude plugin appeared first on Security Boulevard.

❌