ZKEmail written in NoirLang
In your Nargo.toml file, add the version of this library you would like to install under dependency:
[dependencies]
zkemail = { tag = "v0.3.2", git = "https://github.com/zkemail/zkemail.nr", directory = "lib" }
The library exports the following functions:
dkim::RSAPubkey::verify_dkim_signature
- for verifying DKIM signatures over an email header. This is needed for all email verifications.headers::body_hash::get_body_hash
- constrained access and decoding of the body hash from the headerheaders::email_address::get_email_address
- constrained extraction of to or from email addressesheaders::constrain_header_field
- constrain an index/ length in the header to be the correct name, full, and uninterruptedpartial_hash::partial_sha256_var_end
- finish a precomputed sha256 hash over the bodymasking::mask_text
- apply a byte mask to the header or body to selectively reveal parts of the entire emailstandard_outputs
- returns the hash of the DKIM pubkey and a nullifier for the email (hash(signature)
)
Additionally, the @zk-email/zkemail-nr
JS library exports an ergonomic API for easily deriving circuit inputs needed to utilize the Noir library.
For demonstrations of all functionality, see the examples.
A basic email verifier will often look like this:
use dep::zkemail::{
KEY_LIMBS_1024, dkim::RSAPubkey, get_body_hash_by_index,
base64::body_hash_base64_decode, standard_outputs
};
use dep::std::hash::sha256_var;
// Somewhere in your function
...
// verify the dkim signature over the asserted header
pubkey.verify_dkim_signature(header, signature);
// extract the body hash from the header
let signed_body_hash = get_body_hash(header, dkim_header_sequence, body_hash_index);
// compute the sha256 hash of the asserted body
let computed_body_hash: [u8; 32] = sha256_var(body.storage, body.len() as u64);
// constain the computed body hash to match the one found in the header
assert(
signed_body_hash == computed_body_hash,
"SHA256 hash computed over body does not match body hash found in DKIM-signed header"
);
...
From here, you can operate on the header or body with guarantees that the accessed text was signed by the DKIM key.
You may also have an email where you need access to the header, but not the body. You can simply omit everything after verify_dkim_signature
and proceed!
You can use partial hashing technique for email with large body when the part you want to constrain in the body is towards the end.
Since SHA works in chunks of 64 bytes, we can hash the body up to the chunk from where we want to extract outside of the circuit and do the remaining hash in the circuit. This will save a lot of constraints as SHA is very expensive in circuit (~100 constraints/ byte).
use dep::zkemail::{
KEY_LIMBS_2048, dkim::RSAPubkey, headers::body_hash::get_body_hash,
partial_hash::partial_sha256_var_end
};
...
// verify the dkim signature over the asserted header
pubkey.verify_dkim_signature(header, signature);
// extract the body hash from the header
let signed_body_hash = get_body_hash(header, dkim_header_sequence, body_hash_index);
// finish the partial hash
let computed_body_hash = partial_sha256_var_end(partial_body_hash, body.storage(), body.len() as u64, partial_body_real_length);
// constain the computed body hash to match the one found in the header
assert(
signed_body_hash == computed_body_hash,
"SHA256 hash computed over body does not match body hash found in DKIM-signed header"
);
...
To and from email addresses can be extracted from the header with get_email_address
use dep::zkemail::get_email_address;
...
// define the header field to access (set "to" or "from")
let to = comptime { "to".as_bytes() };
// constrained retrieval of the email header
let to_address = get_email_address(header, to_header_sequence, to_address_sequence, to);
...
to_address
is a "BoundedVec", meaning the output of a parsed email address "[email protected]" would export
{
"storage": [122, 107, 101, 109, 97, 105, 108, 64, 112, 114, 111, 118, 101, 46, 101, 109, 97, 105, 108, 0, ..., 0],
"len": 19
}
which is easily interpreted with Buffer.from(output.storage.slice(0, output.storage.len)).toString()
. You can additionally perform your own transformations or commitments in-circuit.
Install the library:
yarn add @zk-email/zkemail-nr
See the witness simulation and proving tests for an in-depth demonstration of each use case.
// example of generating inputs for a partial hash
import { generateEmailVerifierInputs } from "@zk-email/zkemail-nr";
const zkEmailInputs = await generateEmailVerifierInputs(emailContent, {
maxBodyLength: 1280,
maxHeadersLength: 1408,
shaPrecomputeSelector: "some string in body up to which you want to hash outside circuit",
});
TODO
TODO
- Expected InputGen testing
- EVM Contract tests for email integration
- Aztec Contract tests for email integration
- 1024-bit key demo eml (current one is sensitive and cannot be provided in public repo)
- Implementation with Regex
- Add constraint estimations and benchmarking
- Add native proving scripts
- Macro Impl
By Mach-34