ZK-Snark circuit to prove that a given ciphertext is the correct AES-128
encryption using a certain secret key.
This first iteration uses ECB as the mode of operation and Marlin
as the proving system for the circuit. In the future we will support CBC and GCM as alternative modes and Plonk
as an alternative proving system.
message
: The message to encrypt.secret_key
: The secret key used for the AES encryption.
ciphertext
: The encrypted message. This is public as the entire point of the circuit is for a verifier to be assured that the ciphertext they were given is the correct one.
You can find an example usage under the main.rs
module. Below is an explanation of it.
First, the proving and verifying keys must be generated. You can generate ones for testing by calling
let (proving_key, verifying_key) = synthesize_keys(message_length)?;
where message_length
is the length of the message to be encrypted. Underneath, this is generating some universal SRS and then deriving the keys from it. In a real world scenario, the SRS should be generated in a secure manner through some setup using MPC.
With the proving key in hand, a prover calls
let message = [1_u8; 16];
let secret_key = [0_u8; 16];
let proof = encrypt(&message, &secret_key, &primitive_ciphertext, proving_key)?;
where primitive_ciphertext
is a byte slice with the result of the AES
encryption (under the example there's a helper function for it, but you can use any standard AES
implementation).
The prover then hands the resulting proof along with the ciphertext to the verifier, who calls
let result = verify_encryption(
verifying_key,
&proof,
&primitive_ciphertext
)?;
assert!(result);
AES-128
consists of 11 rounds. The secret key is used to derive 11 round keys, one for each round.
Each AES round then takes a message as input and performs the following steps:
Add Round Key
Sub Bytes
Shift Rows
Mix Columns
Given the above, the building blocks required at the circuit level are the following:
Building Blocks | Required Primitives |
---|---|
AddRoundKey | xor |
SubBytes | conditional select |
ShiftRows | Row shifting |
MixColumns | addmany |
KeyDerivation | All of the above |
This is just an xor of the input against the current round key.
This is the so called Rijndael S-Box, a lookup table that has a pretty complicated calculation involving Rijndael's finite field.
Inside the circuit, we implement it by instantiating the precomputed table as 256 constants and then using a conditional select operation to do the lookup.
This step simply writes the input as a byte matrix and then rotates each row.
Mix Columns
is essentially multiplying the input by a matrix, only the multiplication is once again performed in Rijndael's finite field.
The key derivation is the most complex step, but it's ultimately just a combination of all the basic operations used in the four steps for every round.