Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: Noir tests

on:
push:
branches:
- main
branches:
- main
pull_request:

jobs:
Expand All @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
toolchain: [1.0.0-beta.4]
toolchain: [1.0.0-beta.11]
steps:
- name: Checkout sources
uses: actions/checkout@v4
Expand All @@ -36,7 +36,7 @@ jobs:
- name: Install Nargo
uses: noir-lang/[email protected]
with:
toolchain: 1.0.0-beta.4
toolchain: 1.0.0-beta.11

- name: Run formatter
working-directory: ./lib
Expand All @@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-latest
# We want this job to always run (even if the dependant jobs fail) as we want this job to fail rather than skipping.
if: ${{ always() }}
needs:
needs:
- test
- format

Expand Down
14 changes: 7 additions & 7 deletions lib/Nargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[package]
name = "zkemail.nr"
name = "zkemail_nr"
type = "lib"
authors = ["Mach 34"]
compiler_version = ">=1.0.0"

[dependencies]
bignum = { tag = "v0.6.0", git = "https://github.com/noir-lang/noir-bignum" }
rsa = { tag = "v0.7.0", git = "https://github.com/noir-lang/noir_rsa" }
base64 = { tag = "v0.4.0", git = "https://github.com/noir-lang/noir_base64" }
nodash = { tag = "v0.40.2", git = "https://github.com/olehmisar/nodash" }
sha256 = { git = "https://github.com/noir-lang/sha256", tag = "v0.1.2" }
poseidon = { tag = "v0.1.0", git = "https://github.com/noir-lang/poseidon" }
bignum = { tag = "v0.8.0", git = "https://github.com/noir-lang/noir-bignum" }
rsa = { tag = "v0.9.1", git = "https://github.com/zkpassport/noir_rsa" }
base64 = { tag = "v0.4.2", git = "https://github.com/noir-lang/noir_base64" }
nodash = { tag = "v0.42.0", git = "https://github.com/olehmisar/nodash" }
sha256 = { tag = "v0.2.1", git = "https://github.com/noir-lang/sha256" }
poseidon = { tag = "v0.1.1", git = "https://github.com/noir-lang/poseidon" }
22 changes: 10 additions & 12 deletions lib/src/headers/email_address.nr
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,31 @@ pub fn parse_email_address<let MAX_HEADER_LENGTH: u32>(
// check the sequence is proceeded by an acceptable character
if email_address_sequence.index != 0 {
assert(
EMAIL_ADDRESS_CHAR_TABLE[header.get_unchecked(email_address_sequence.index - 1)] == 2,
EMAIL_ADDRESS_CHAR_TABLE[header.get_unchecked(email_address_sequence.index - 1) as u32]
== 2,
"Email address must start with an acceptable character",
);
}
if email_address_sequence.end_index() < header.len() {
assert(
EMAIL_ADDRESS_CHAR_TABLE[header.get_unchecked(
email_address_sequence.index + email_address_sequence.length,
)]
) as u32]
== 3,
"Email address must end with an acceptable character",
);
}
// check the email address and assign
let mut email_address: BoundedVec<u8, MAX_EMAIL_ADDRESS_LENGTH> = BoundedVec::new();
for i in 0..MAX_EMAIL_ADDRESS_LENGTH {
for i in 0..email_address_sequence.length {
let index = email_address_sequence.index + i;
if index < email_address_sequence.end_index() {
let letter = header.get_unchecked(index);
email_address.set_unchecked(i, letter);
assert(
EMAIL_ADDRESS_CHAR_TABLE[letter] == 1,
"Email address must only contain acceptable characters",
);
}
let letter = header.get_unchecked(index);
assert(
EMAIL_ADDRESS_CHAR_TABLE[letter as u32] == 1,
"Email address must only contain acceptable characters",
);
email_address.push(letter);
}
email_address.len = email_address_sequence.length;
// todo: should probably introduce a check for @
email_address
}
18 changes: 9 additions & 9 deletions lib/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ pub mod remove_soft_line_breaks;
pub mod utils;
mod tests;

global RSA_EXPONENT: u32 = 65537;
global KEY_LIMBS_1024: u32 = 9;
global KEY_LIMBS_2048: u32 = 18;
global BODY_HASH_BASE64_LENGTH: u32 = 44;
global CR: u8 = 0x0D;
global LF: u8 = 0x0A;
global MAX_DKIM_HEADER_FIELD_LENGTH: u32 = 300; // kinda arbitrary but gives > 100 chars for selector and domain
global MAX_EMAIL_ADDRESS_LENGTH: u32 = 320; // derived via (https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.1)
pub global RSA_EXPONENT: u32 = 65537;
pub global KEY_LIMBS_1024: u32 = 9;
pub global KEY_LIMBS_2048: u32 = 18;
pub global BODY_HASH_BASE64_LENGTH: u32 = 44;
pub global CR: u8 = 0x0D;
pub global LF: u8 = 0x0A;
pub global MAX_DKIM_HEADER_FIELD_LENGTH: u32 = 300; // kinda arbitrary but gives > 100 chars for selector and domain
pub global MAX_EMAIL_ADDRESS_LENGTH: u32 = 320; // derived via (https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.1)

pub struct Sequence {
index: u32,
Expand All @@ -30,7 +30,7 @@ impl Sequence {
// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-@" = 1
// "<: " = 2
// ">\r\n" = 3
global EMAIL_ADDRESS_CHAR_TABLE: [u8; 123] = [
pub global EMAIL_ADDRESS_CHAR_TABLE: [u8; 123] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 2, 0, 3, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
Expand Down
8 changes: 4 additions & 4 deletions lib/src/remove_soft_line_breaks.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{CR, LF};
use nodash::array::pack_bytes;
use std::hash::poseidon2::Poseidon2;
use nodash::pack_bytes;
use poseidon::poseidon2::Poseidon2;

/**
* Computes R by packing bytes into fields before hashing to reduce the work
Expand Down Expand Up @@ -36,10 +36,10 @@ pub fn find_zeroes<let N: u32>(encoded: [u8; N]) -> [bool; N] {
// find indexes of chars to zero
let mut should_zero: [bool; N] = [false; N];
should_zero[0] = is_break[0];
should_zero[1] = is_break[1] + is_break[0];
should_zero[1] = is_break[1] | is_break[0];
should_zero[N - 1] = is_break[N - 3];
for i in 2..N - 1 {
should_zero[i] = is_break[i] + is_break[i - 1] + is_break[i - 2];
should_zero[i] = is_break[i] | is_break[i - 1] | is_break[i - 2];
}

should_zero
Expand Down
76 changes: 40 additions & 36 deletions lib/src/tests/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mod test_success {

#[test]
fn test_dkim_signature() {
EmailLarge::PUBKEY.verify_dkim_signature(EmailLarge::HEADER, EmailLarge::SIGNATURE);
let _ = EmailLarge::PUBKEY.verify_dkim_signature(EmailLarge::HEADER, EmailLarge::SIGNATURE);
}

#[test]
Expand Down Expand Up @@ -93,9 +93,9 @@ mod test_tampered_hash {
#[test(should_fail)]
fn test_tampered_header() {
// get tampered header
let mut tampered_header = unsafe { EmailLarge::tampered_header() };
let mut tampered_header = EmailLarge::tampered_header();
// attempt to verify the DKIM signature
EmailLarge::PUBKEY.verify_dkim_signature(tampered_header, EmailLarge::SIGNATURE);
let _ = EmailLarge::PUBKEY.verify_dkim_signature(tampered_header, EmailLarge::SIGNATURE);
}

#[test]
Expand All @@ -107,7 +107,7 @@ mod test_tampered_hash {
EmailLarge::BODY_HASH_INDEX,
);
// get tampered body
let mut tampered_body = unsafe { EmailLarge::tampered_body() };
let mut tampered_body = EmailLarge::tampered_body();
// compute the body hash
let tampered_body_hash: [u8; 32] =
sha256_var(tampered_body.storage(), tampered_body.len() as u64);
Expand All @@ -125,13 +125,15 @@ mod test_tampered_hash {
let delta = 1;
sig[0] += delta * 0x1000000000000000000000000000000;
sig[1] -= delta;
pubkey.verify_dkim_signature(EmailLarge::HEADER, sig);
let _ = pubkey.verify_dkim_signature(EmailLarge::HEADER, sig);
}
}

mod header_field_access {

use crate::{headers::body_hash::get_body_hash, Sequence, tests::test_inputs::EmailLarge};
use crate::{
CR, headers::body_hash::get_body_hash, LF, Sequence, tests::test_inputs::EmailLarge,
};

#[test(should_fail_with = "No ':bh=' or '; bh=' prefix found before body hash")]
fn test_bad_body_hash_off_one_minus() {
Expand All @@ -155,23 +157,24 @@ mod header_field_access {

#[test(should_fail_with = "Header field name does not match")]
fn test_bad_body_hash_not_in_dkim_field() {
// create header field for malicious bh
let mut dkim_field: BoundedVec<u8, EmailLarge::EMAIL_LARGE_MAX_HEADER_LENGTH> =
BoundedVec::new();
dkim_field.len = EmailLarge::HEADER.len();
// craft a malicious "to" field where attacker tries to put bh in display name
let mut malicious_to: [u8; 78] = comptime {
"\r\nto:\"bh=2JsdK4BMzzt9w4Zlz2TdyVCFc+l7vNyT5aAgGDYf7fM=;\" <[email protected]>\r\n"
.as_bytes()
};
// create header field for malicious bh - build array with malicious field
let mut dkim_field_array: [u8; EmailLarge::EMAIL_LARGE_MAX_HEADER_LENGTH] =
[0; EmailLarge::EMAIL_LARGE_MAX_HEADER_LENGTH];
let mut malicious_sequence = Sequence {
index: 8, // 8 to make it check for crlf on both sides (could be anything > 2)
length: malicious_to.len() - 4, // 4 is the crlf on each end
};
for i in 0..malicious_to.len() {
let index = malicious_sequence.index + i - 2;
dkim_field.storage[index] = malicious_to[i];
dkim_field_array[index] = malicious_to[i];
}
let dkim_field: BoundedVec<u8, EmailLarge::EMAIL_LARGE_MAX_HEADER_LENGTH> =
BoundedVec::from(dkim_field_array);
let malicious_body_hash_index = 15;
// copy the body hash to the beginning of the
// attempt to get body hash
Expand All @@ -184,7 +187,7 @@ mod header_field_access {
"dkim-signature:bh=2JsdK4BMzzt9w4Zlz2TdyVCFc+l7vNyT5aAgGDYf7fM=; v=1; a=rsa-sha256; c=relaxed/relaxed; d=icloud.com; s=1a1hai; t=1712141644; h=from:Content-Type:Mime-Version:Subject:Message-Id:Date:to; b="
.as_bytes()
};
let header: BoundedVec<u8, 203> = BoundedVec { storage: dkim_field, len: 203 };
let header: BoundedVec<u8, 203> = BoundedVec::from_array(dkim_field);
let mut dkim_field_sequence = Sequence { index: 0, length: 203 };
let body_hash_index = 18;
let _ = get_body_hash(header, dkim_field_sequence, body_hash_index);
Expand All @@ -203,9 +206,10 @@ mod header_field_access {
// make sequence extend beyond the end of the header field
let mut overflowed_sequence = EmailLarge::DKIM_HEADER_SEQUENCE;
overflowed_sequence.length = overflowed_sequence.length + 1;
// set header len to be a bit longer so it doesn't overflow
// set header len to be a bit longer so it doesn't overflow - extend with zeros
let mut longer_header = EmailLarge::HEADER;
longer_header.len = longer_header.len + 2;
longer_header.push(0);
longer_header.push(0);
// attempt to get body hash
let _ = get_body_hash(
longer_header,
Expand Down Expand Up @@ -264,38 +268,38 @@ mod header_field_access {
+ EmailLarge::DKIM_HEADER_SEQUENCE.length
+ 2, // 2 for crlf in middle
};
tampered_header.len = combined_sequence.length + 4;
// copy dkim-signature field
for i in 0..EmailLarge::DKIM_HEADER_SEQUENCE.length + 2 {
tampered_header.set(
i,
EmailLarge::HEADER.get(EmailLarge::DKIM_HEADER_SEQUENCE.index + i - 2),
);
// add initial crlf (before sequence starts at index 2)
tampered_header.push(CR);
tampered_header.push(LF);
// copy dkim-signature field content only (without surrounding CRLFs)
for i in 0..EmailLarge::DKIM_HEADER_SEQUENCE.length {
tampered_header.push(EmailLarge::HEADER.get(EmailLarge::DKIM_HEADER_SEQUENCE.index + i));
}
tampered_header.set(EmailLarge::DKIM_HEADER_SEQUENCE.length + 2, "\r".as_bytes()[0]);
tampered_header.set(EmailLarge::DKIM_HEADER_SEQUENCE.length + 3, "\n".as_bytes()[0]);
// copy to field
for i in 0..EmailLarge::TO_HEADER_SEQUENCE.length + 2 {
let index = EmailLarge::DKIM_HEADER_SEQUENCE.length + 4;
tampered_header.set(
index + i,
EmailLarge::HEADER.get(EmailLarge::TO_HEADER_SEQUENCE.index + i),
);
// add crlf in middle (this is the newline that should trigger the error)
tampered_header.push(CR);
tampered_header.push(LF);
// copy to field content only (without surrounding CRLFs)
for i in 0..EmailLarge::TO_HEADER_SEQUENCE.length {
tampered_header.push(EmailLarge::HEADER.get(EmailLarge::TO_HEADER_SEQUENCE.index + i));
}
// set crlf at end
tampered_header.set(combined_sequence.length + 2, "\r".as_bytes()[0]);
tampered_header.set(combined_sequence.length + 3, "\n".as_bytes()[0]);
// add crlf at end (at position index + length)
tampered_header.push(CR);
tampered_header.push(LF);
let tampered_body_hash_index = 93; // just manually setting this
// attempt to get body hash
let _ = get_body_hash(tampered_header, combined_sequence, tampered_body_hash_index);
}

#[test(should_fail_with = "Header field out of bounds")]
fn test_header_field_outside_header() {
let mut shortened_header = EmailLarge::HEADER;
// shorten header to be just under the end of the dkim field
shortened_header.len =
// create shortened header to be just under the end of the dkim field
let target_len =
EmailLarge::DKIM_HEADER_SEQUENCE.index + EmailLarge::DKIM_HEADER_SEQUENCE.length - 1;
let mut shortened_header: BoundedVec<u8, EmailLarge::EMAIL_LARGE_MAX_HEADER_LENGTH> =
BoundedVec::new();
for i in 0..target_len {
shortened_header.push(EmailLarge::HEADER.get(i));
}
// attempt to get body hash
let _ = get_body_hash(
shortened_header,
Expand Down