zkEmail 解析與整合紀錄

zkEmail 是一套利用零知識證明技術驗證電子郵件的工具,透過校驗 DKIM 簽章確保郵件真實性,並能選擇性揭露內容。本文深入探討其核心原理、電路實作(如 Noir 與 Circom)、資訊擷取技術(Sequence 與 zk-regex)以及與 ERC-4337 的整合,並分享了多個實際應用案例與技術挑戰。

zkEmail 提供一系列開發工具,讓開發者能為電子郵件產生零知識證明。透過 ZK 電路,證明者可以在不揭露整封郵件內容的前提下,證明郵件中是否包含特定資訊。舉例來說,對於一封網購收據郵件,證明者可以向合約證明自己購買了某項商品,或實際花費了多少金額,而無需公開完整郵件內容。

零知識證明讓你在不揭露程式參數(或僅揭露部分參數)的情況下,證明某段程式已被正確執行。程式會以電路的形式撰寫,並可生成對應的驗證器部署到鏈上;使用者在鏈下執行程式並提交證明後,鏈上即可驗證該程式邏輯是否確實於鏈下被正確執行。

zkEmail 電路要驗證的邏輯主要分成兩部分:

  • 驗證郵件的電子簽章,確認其完整性(integrity)與來源真實性(authenticity)
  • 選擇性揭露郵件上的資訊以符合應用所需

zkEmail 支援 Circom ( zk-email-verify) 和 Noir ( zkemail.nr) 兩種電路語言 (ZK DSL) 的實作,針對郵件資訊的截取,可以選擇性搭配使用 zk-regex

Photo by VectorElements on Unsplash

本專案由 TEM Grant 贊助,相關連結:

整合到 TEM 領稿費網站

原本的領稿費網站是由管理者中心化登記投稿,我們要改成 TEM 寄信給投稿人,投稿人憑電子郵件自行登記上鏈。詳見 [TEM] 去中心化領稿費機制實驗 2

除了驗證電子郵件的數位簽章,還要截取郵件上的資訊包含:

  • 寄件人(eth.taipei@gmail.com)
  • 郵件的意圖(登記稿件或更新某稿件的收款人地址)
  • 所要登記的文章標題
  • 收款人的錢包地址
  • 郵件的編號(用於撤銷誤寄的郵件)

專案將使用 Noir 語言撰寫零知識電路,並透過 Barretenberg 證明系統後端產生證明與 Verifier 合約,整合到原本的 RoyaltyAutoClaim 合約中,同時支援透過 ERC-4337 協議來操作。

當用戶要登記稿件,首先是到信箱下載 email,到領稿費網站上傳 email,產生證明並送出交易,無需連線錢包,無需支付手續費(手續費由合約來支付)。

How it works

Email Authentication

當我們寄出一封 email,收件者的郵件伺服器(Mail Server)會依循三個協議來判斷郵件是否為垃圾郵件,分別是:

  • SPF (Sender Policy Framework):允許域名持有者透過 DNS 紀錄來宣告合法的郵件伺服器 IP,可供收件人驗證寄信的伺服器是否被允許
  • DKIM (DomainKeys Identified Mail):透過私鑰簽署郵件內容,收件者用 DNS 上的公鑰驗證,確保郵件傳輸過程不被竄改(integrity)及確保郵件來自某域名(authenticity)
  • DMARC (Domain-based Message Authentication, Reporting, and Conformance):建立在 SPF 和 DKIM 之上的框架,進行域名對齊檢查、允許域名持有者發布政策和產生報告。

更多電子郵件的驗證細節可參考以下資源:

對於 zkEmail 的應用需特別關注 DKIM,DKIM 不保證寄件人身份,只確保這封信是由該網域授權的系統簽署。DKIM 驗證的是標頭欄位 DKIM-Signature:d= 的域名,而不是標頭(header)欄位 From: 寄件人地址的域名,這兩者可能不同。

Email 的組成分為標頭和內文(body)。標頭內會有 DKIM-Signature: 欄位:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=gmail.com; s=20230601; t=1760717543; x=1761322343; dara=google.com;
        h=to:subject:message-id:date:from:mime-version:from:to:cc:subject
         :date:message-id:reply-to;
        bh=HaXkrmKCJh/y1wpzAEYZ9MaHcuFTSbV17TiQz3aEva4=;
        b=jMmQkcXJayrGLHgGHtd4PzFu3eOutOYOuxFk5nOQW6KMK617x7c+85eSOXlyqke+JB
         58YdwOrVErPfItCqlZKuHxoy5WFPmZ0oqOASxsmZJMSSw8qlrVjo5BlG0TeFg5i1Ki1a
         Jjhh0nu/5IDz2jbWxS9sQA7eSOSiJPbVgcnXX9+LytaKRtRdTZ7KtV3rDsdKf/xzJZQ6
         e1FbQCIrAMNjNQcAtZHGlmd86cyYCb/rdyv/wnl9l4PIsgD+zFmSCo3Q8mOZ/W7mdqhV
         70uMZu3tDjaE3EVfUXEnkleufjMQmlrwJKcElLGcKtYrmwUy1D9aU29e7tIsWTc0OQ1Y
         qI1Q==
  • v=1: DKIM 版本
  • a=rsa-sha256: 使用的簽章演算法,header hash 和 body hash 都是使用 rsa-sha256
  • c=relaxed/relaxed: 標準化方法(header/body)
  • d=gmail.com: 簽署域名
  • s=20230601: selector,用於向 DNS server 查詢簽章所使用的公鑰是哪一把,Gmail 使用日期作為 selector,可以在終端機查詢:dig TXT 20230601._domainkey.gmail.com
  • t=: 簽章時間戳
  • x=: 簽章過期時間
  • h=: 列出要簽署的 header 欄位。(reply-to 欄位雖然被列出,但若標頭內沒有此欄位也不會簽入。)
  • bh=: body hash (Base64 編碼的內文雜湊)
  • b=: signature (Base64 編碼的數位簽章)

順帶一提,在 Gmail 中,當寄件者把自己加入 To / Cc / Bcc 時,無論是寄件匣(Sent)或收件匣(Inbox),寄件者看到的郵件標頭都不包含 DKIM-Signature,所以寄件者寄給自己的信或副本信件是無法拿來產生證明的。

在電路中驗證 DKIM 簽章

理解了 DKIM 的結構後,接下來要在 ZK 電路中實現驗證邏輯。整個驗證流程包含三個步驟:

  1. 使用公鑰驗證 DKIM 簽章的有效性
  2. 產生公鑰的雜湊值以便在合約中驗證域名的可信度
  3. 透過簽章的雜湊值產生註銷符(nullifier)來防止證明被重複使用。(由於電子簽章具有延展性(Malleability),同一訊息可產生多個合法簽章,若將簽章作為註銷符將導致其不唯一,存在合約安全疑慮。本設計假設郵件伺服器安全可信,不會對同一封郵件重複簽署。)

Noir 語言的電路程式碼範例如下:

let header_hash = pubkey.verify_dkim_signature(header, signature);
let pubkey_hash: Field = pubkey.hash(); // Poseidon hash of public key
let nullifier: Field = pedersen_hash(signature); // Pedersen hash of signature
  • 驗證簽章後回傳的 header hash 暫時用不到。
  • pubkey_hash 作為公開參數,用於在合約中驗證域名是否可信。
  • 註銷符作為公開參數,用於在合約中註銷已使用的證明。

郵件的公鑰可以透過標頭欄位 DKIM-Signature: 內的 selector 向 DNS 查詢,然而郵件伺服器會定期更換公鑰,郵件若使用舊的公鑰簽署,DNS 是查不到的。因此會需要一個積極維護的 DKIM 公鑰資料庫,將各個郵件伺服器曾使用過得公鑰儲存起來。

於是 zkEmail 做了 DKIM Public Key Archive,試圖保存不同郵件伺服器的舊公鑰,讓使用舊公鑰簽署的郵件能夠產生 ZK 證明,詳見 此文

為了在鏈上驗證公鑰的合法性,將 DKIM Public Key Archive 搬到鏈上變成一個公鑰映射域名的紀錄:DKIM Registry,其保存公鑰雜湊與域名的映射關係( ERC-7969 也提出相關的標準),讓我們得以在合約中進行驗證, 驗證邏輯 如下:

if (!dkimRegistry.isDKIMPublicKeyHashValid("gmail.com", _proof.pubkeyHash())) {
    revert InvalidDKIMPublicKey(_proof.pubkeyHash());
}

此外也要在合約中檢查與註銷已使用的證明,避免同一封郵件證明被重複使用。注意註銷符僅用於防止同一封郵件在同一合約中被重複使用,並未試圖防止跨合約、跨應用或跨鏈重放。

function _verifyRegistration(string memory title, TitleHashVerifierLib.EmailProof memory proof) internal view returns (bool) {
    ...
    require(!isEmailProofUsed(proof.nullifier()), EmailProofUsed());
    ...
function _registerSubmission(string memory title, address recipient, bytes32 nullifier) internal {
    ...
    $.emailNullifierUsed[nullifier] = true;
    ...

Body Hash

除了驗證標頭,如果所需截取的資訊位於內文,電路就必須驗證 body hash。其位於標頭 DKIM-Signature:bh=,驗證邏輯如下:

// extract the body hash from the header
let signed_body_hash = get_body_hash(header, dkim_header_seq, 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);
// constrain 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",
);

至此,一個驗證郵件完整性的電路,其私密參數包含:

  • header: 標頭
  • pubkey: 公鑰
  • signature: 簽章
  • dkim_header_seq: 標頭欄位 DKIM-Signature: 的位置與長度
  • body: 內文
  • body_hash_index: 標頭欄位 DKIM-Signature:bh= 的位置

公開參數包含:

  • pubkey_hash: 公鑰雜湊
  • nullifier: 註銷符

在鏈下(通常於客戶端),使用者會先準備電路所需的參數並產生零知識證明。該證明由證明資料(proof)與公開參數(publicInputs)所組成。鏈上的驗證器接收這些資料後,合約需先將 publicInputs 解析為可供驗證的型別,再於 主要的 verify 函式 中完成驗證。

截取郵件上的資訊

確保郵件標頭和內文的完整性以後,透過截取郵件中的訊息,才能進行符合應用所需的驗證。

email_from_address 為例,以下介紹如何在電路中截取電子郵件標頭 From: 欄位中的寄件人地址,並將其作為公開參數,供合約端驗證寄件人是否合法。電路邏輯需確保該寄件人地址(eth.taipei@gmail.com)確實位於 From: 欄位內,而合約則會進一步檢查該公開參數是否符合預期的寄件人地址。

以下分別介紹 Sequence approach 和 zk-regex 兩種於電路中截取子字串的方法:

Sequence approach

Sequence 是一個包含索引和長度的結構,在 constrain_header_field 函式中透過輸入 header, field_sequence 和 field_name 來檢查標頭內是否含有特定欄位,field_sequence 是用戶要提供的欄位值的索引和長度。該函式主要有兩點檢查:

  1. check_header_field_bounds
  • 檢查 field_sequence.end_index <= header.len
  • 如果 field_sequence 不是第一行,檢查前面一定有 CRLF
  • 如果 field_sequence 不是最後一行,檢查後面一定有 CRLF
  • 檢查 field_name 的每個字吻合
  • 檢查 field_name 的下個字必須是冒號

2. 不間斷檢查:迴圈欄位內的每個字,檢查中間不能出現 CRLF

其中,CRLF 是電子郵件所使用的換行:\r\n,不是 \n ,需特別注意(註:Windows 系統的換行也是 \r\n)。 RFC 5322 - Internet Message Format 規範郵件訊息的傳輸格式,其中訊息的「換行」都必須使用 CRLF (carriage return + line feed),例如 to:eth.taipei@gmail.com\r\nsubject:=?UTF-8?B?56...\r\n,可以推論,除了第一行之外,每一行的前兩個 byte 都是 CRLF,check_header_field_bounds 利用這點在電路中檢查子字串序列是否合理。

透過上述 constrain_header_field 的方法,能夠建構出其他便於擷取欄位的函式如 get_email_address,它讓你取得標頭內 from:to: 的地址,假設要截取 from:TEM &lt;eth.taipei@gmail.com> 內的 eth.taipei@gmail.com,除了使用 constrain_header_field 之外,還檢查了以下幾點:

  • 檢查 address_sequence 前一字是否有 &lt;
  • 檢查 address_sequence 位於 field_sequence 內。
  • 建立一個 字元表,檢查郵件地址上的每個字是否使用合法的 ASCII 字元。

本專案參考 constrain_header_field 的邏輯及其在 get_email_address 中的用例,建構出一系列用於擷取郵件資訊的函式,包括 get_operation_typeextract_subjectget_numberget_recipient_addressget_title_hash 等。這些函式背後的擷取原理統稱為 Sequence approach。以下將介紹另一種擷取方式。

zk-regex

zk-regex 是一個電路編譯器,讓你透過自定義正規表達式來生成電路的函式庫。

正規表達式的引擎有 DFA (Deterministic Finite Automata) 和 NFA (Nondeterministic Finite Automata) 兩種。zk-regex v1 使用 DFA; zk-regex v2 使用 NFA。它們的背後是關於自動機理論(automata theory)。

2025/6 發佈的 zk-regex v2,使用 NFA 在電路外進行複雜的正則表達式匹配,電路僅負責驗證預先計算的遍歷路徑,相較 v1 提升了電路的效能,詳見 此文

zk-regex v2 可以直接用一條正規表達式,而不必像 v1 需使用分解的(decomposed) regex。v2 只要 Rust 的 regex_automata 能匹配到通常能順利生成電路。

在正規表達式中,匹配(match)和捕獲組(capture group)的差別在於,「匹配」是正規表達式匹配到的整個字串;「捕獲組」是在正規表達式中用括號 () 包起來,從匹配的字串中提取的子字串。例如使用 \w+@\w+\.\w+,輸入 My email is john@example.com,會匹配到 john@example.com,沒有捕獲組。若使用 (\w+)@\w+\.\w+,則匹配到一樣的地址,但包含一個捕獲組:john。使用 regex101 右側欄會清楚呈現 regex 的匹配資訊。

zk-regex 只會採用第一個匹配到的字串,一個匹配可包含多個捕獲組,捕獲組會成為電路的公開參數。

以 Noir 為例,若要從標頭中的 from: 截取寄件人地址,regex 如下:

(?:\r\n|^)from:[^&lt;]*&lt;([A-Za-z0-9._%+-]+@gmail.com)>
  • 郵件中的換行使用 \r\n 而不是一般的換行 \n,在 regex101 測試時要注意。
  • 括號可能用來分組 (A|B),若不想被捕獲要使用 non-capturing group (?:...)

zk-regex v2 所生成的電路 from_address_regex,應用於郵件標頭中擷取郵件地址時,程式範例如下:

mod from_address_regex;

global MAX_EMAIL_HEADER_LENGTH: u32 = 960;
global MAX_MATCH_LENGTH: u32 = 36; // from:Johnson &lt;johnson86tw@gmail.com>
global MAX_GROUP_1_LENGTH: u32 = 21; // johnson86tw@gmail.com

// acir: 7102
// bb circuit size: 21939
fn main(
    header: BoundedVec&lt;u8, MAX_EMAIL_HEADER_LENGTH>,
    match_start: u32,
    match_length: u32,
    current_states: [Field; MAX_MATCH_LENGTH],
    next_states: [Field; MAX_MATCH_LENGTH],
    capture_group_1_id: [Field; MAX_MATCH_LENGTH],
    capture_group_1_start: [Field; MAX_MATCH_LENGTH],
    capture_group_start_indices: [Field; 1],
) -> pub (BoundedVec&lt;u8, MAX_GROUP_1_LENGTH>) {
    // check the body and header lengths are within bounds
    assert(header.len() &lt;= MAX_EMAIL_HEADER_LENGTH);

    let capture_1 = from_address_regex::regex_match::&lt;MAX_EMAIL_HEADER_LENGTH, MAX_MATCH_LENGTH>(
        header.storage(),
        match_start,
        match_length,
        current_states,
        next_states,
        capture_group_1_id,
        capture_group_1_start,
        capture_group_start_indices,
    );
    (capture_1)
}

ZK 電路的本質是一組數學限制式(constraints),電路的結構在編譯階段就已固定,無法動態調整大小,因此電路中每個信號(signal,即電路裡的變數)的長度都是固定的。

Remember me for faster sign in

在上述範例中,它定義了匹配和捕獲組的最大長度,尤其匹配的長度(MAX_MATCH_LENGTH)套用在多個參數上,會大幅決定電路的效能。而 regex pattern 的複雜度不影響電路效能,因為實際的匹配計算已在電路外完成,電路只負責驗證預先計算好的遍歷路徑。

限制式的數量直接決定了證明生成的時間,也是電路的效能指標。使用 Barretenberg 的指令 bb gates -b target/from_address_regex.json 可以查看編譯後的電路效能:

Scheme is: ultra_honk
{"functions": [\
  {\
        "acir_opcodes": 6782,\
        "circuit_size": 20899\
  }\
]}

circuit_size 是 constraints/gates 的數量,即為電路的效能指標。而 acir (Abstract Circuit Intermediate Representation) 是 Noir 編譯後的中間表示,此 acir_opcodes 不等比例於實際電路大小,不能直接作為效能判斷依據。

zk-regex 與 Sequence approach 相比,zk-regex 能直接從 regex pattern 生成電路,從這個角度來看是相對直覺的開發體驗,但是 zk-regex 會讓電路的參數變多,前端在準備所需的參數時會較 Sequence approach 複雜,因為 Sequence approach 只需準備索引和長度。

關於 zk-regex 和 Sequence approach 的實驗性程式與效能比較,可以參考 zkregex-v2-test。實測結果針對標頭內的郵件地址擷取,zk-regex 與 Sequence approach 的電路大小是差不多的。而針對本專案郵件內文的多個欄位擷取時,zk-regex 所生成的電路可能較大,因此本專案都先使用 Sequence approach,不排除未來有機會採用 zk-regex。

至此我們介紹了 zkEmail 的核心原理:透過驗證 DKIM 簽章確保郵件完整性,並運用不同的截取技術從郵件中提取所需資訊。在實際開發過程中,會遇到許多細節問題需要處理,以下章節將分享這些開發過程中的技術細節與實務考量。

技術細節與開發上的考量

本節內容較繁雜,提供目錄供參考:

  • 一、zkEmail Registry
  • 二、郵件主旨:解碼與 Title Hash
  • 三、郵件內文:編碼、removeSoftLineBreaks 與 Partial SHA
  • 四、郵件標頭與內文的長度上限
  • 五、Email Replay & Revoke
  • 六、透過 ERC-4337 呼叫接收 ZK Proof 的函式
  • 七、不同郵件伺服器的問題

一、zkEmail Registry

Registry 網站 提供 UI 介面讓你建立藍圖,你可以替任何郵件設計某種驗證規則,網站會根據藍圖編譯成電路原始碼與相關檔案提供下載,還可以整合 zkemail sdk 提供 remote proving 的功能:將郵件傳到 zkemail 的伺服器來產生證明,比起在瀏覽器產證明來得快,但完整郵件會洩露給伺服器。

本專案原本使用 Registry 建立藍圖搭配 sdk 進行開發,彼時網站提供 Circom 原始碼,後來於 2025/12 該網站進行改版,主要是將 regex v1 升級成 v2。

由於同樣的藍圖可以編譯成 Noir,但暫時無法編譯成 Circom ( 藍圖 v49),有鑒於 Noir 語言的可讀性與完善的開發者工具鏈,且無需可信任設置儀式,因此本專案決定改採 Noir 進行開發。

二、郵件主旨:解碼與 Title Hash

RFC 2047 定義了郵件標頭欄位中使用非 ASCII 字元時的編碼方案,以本專案為例,郵件的主旨使用:

確認已收到投稿: zkEmail 解析與整合紀錄 by Johnson

它會被 base64 編碼成:

Subject: =?UTF-8?B?56K66KqN5bey5pS25Yiw5oqV56i/OiBaSyBFbWFpbCDmlbTlkIjntpPpqZfliIbkuqsgYg==?=
 =?UTF-8?B?eSBKb2huc29u?=

本專案需要在郵件中截取文章標題,其中一個方法是在電路中將主旨解碼( 實作範例),然而其電路效能較低,因此本專案採用另一個方法。

替代方案是在郵件內文中加入 ID: 欄位,放上文章標題的雜湊,將其作為公開參數,然後在合約函式 registerSubmission(title, proof) 中,比對參數 title 與 proof 內的 title hash 是一樣的,才能在接下來的邏輯中使用參數 title。

三、郵件內文:編碼、removeSoftLineBreaks 與 Partial SHA

郵件內文以 Gmail 為例,它同時包含兩種格式:text/plain 和 text/html。他們分別各有一編碼方式,可能是無編碼、quoted-printable 或 base64。

編碼方式由郵件伺服器依據內文大小或其他標準自行決定,所以同樣的內容在 Gmail 無編碼,在 Proton 可能以 base64 編碼。以 Gmail 進行測試,內文若有中文字通常會以 base64 編碼,電路需要特別解碼且會降低效能,因此郵件內文須以全英文方式書寫來避開此問題。

特殊功能之一:removeSoftLineBreaks

郵件內文格式 text/html 通常以 quoted-printable 編碼, RFC 2045 Internet Message Bodies 規定每行不應超過 76 個字,當 quoted-printable 編碼的內容太長時,就需要用 soft line breaks 來分割。

例如 text/plain:

ID: 0x123456\r\n

text/html:

&lt;div>ID:=C2=A00x123=\r\n456&lt;/div>
  • =C2=A0: non-breaking space 的 quoted-printable 編碼。
  • =\r\n: soft line break,解碼時會被移除,兩行會重新連接成原本的完整句子。

由於 soft line breaks 會導致使用 zk-regex 在 text/html 中匹配不到的問題(原本不該換行的地方多一個等號和 CLRF),因此 removeSoftLineBreaks 的功能是在電路中對內文進行解碼,把 soft line breaks 從內文拿掉,以利 zk-regex 於 text/html 截取資訊。

removeSoftLineBreaks 功能是可選的,詳見 原始碼

特殊功能之二:Partial Hash

由於要擷取的內文資訊通常只佔整體內文的一小部分,當郵件內文過長時,可以透過 Partial Hash 的方式處理:先將欲擷取位置之前的內容(前段)預先計算好雜湊值並傳入電路,電路再以該中間雜湊值銜接後段內文進行計算,最終得出完整內文的雜湊值,即 body hash。

將算出的 body hash 與郵件標頭中 DKIM-Signature 欄位的 bh= 進行比對驗證,即可確認郵件內文的完整性與來源真實性。

此外,Partial Hash 通常也會搭配上節提到的 removeSoftLineBreaks 一併使用。這是因為郵件內文一般同時包含 text/plain 與 text/html 兩種格式相同內容,透過 Partial Hash 可以略過前段 text/plain 的部分。而在電路中處理後段的 text/html 時,則可以用 removeSoftLineBreaks 先對資料進行正規化,再搭配 zk-regex 擷取所需資訊。

程式範例可參考:

四、郵件標頭與內文的長度上限

ZK 電路的參數大小必須是固定的,因此設計電路時得先定義好「最大標頭長度」和「最大內文長度」,如果輸入的參數超過該長度就無法產生證明。

在 Gmail 寫信的時候如果透過複製貼上的方式,可能會把文字的「字型和字體」等樣式一併複製到郵件內文中,寫信時看起來像純文字,但是在 html 的部分卻多出許多 css 樣式,可能剛好導致內文長度超過上限而無法產生證明。

直覺解法是增加最大內文長度來緩衝可能因 css 樣式而擴張的內文,但這會增加產證明的時間。另一個解法是要求寄信人使用「plain text mode」來寫信(Gmail 提供的功能之一),此模式下郵件的 html 部分會完全消失,只留下純文字,因此能大幅減少內文的長度,就能提升電路的效能。

在電路定義最大標頭/內文長度時,須使用 64 的倍數,因為 SHA-256 是以 64 bytes 為一個 block 進行處理。原始標頭和內文經過 SHA-256 padding 後,長度會被補齊到 64 bytes 的倍數,標題和內文的長度上限需要配合這個 padded 後的長度來設計。

實作可參考:

五、Email Replay & Revoke

  • 一封郵件產生的證明基本上能同時用於相同應用但不同的鏈,除非在證明裡加入鏈的資訊且於合約中加以驗證。
  • 只要持有郵件即可產生證明,因此能產生證明的人未必是收件者本人。
  • 為了避免寄件者誤傳郵件後,用戶拿誤傳的郵件產證明並登記,我們新增「郵件編號」的欄位於內文,讓管理者能將某編號的郵件撤銷使其無法登記。

六、透過 ERC-4337 呼叫接收 ZK Proof 的函式

本專案用戶可透過 ERC-4337 來呼叫合約函式,手續費從合約內的 ETH 扣除。針對提交 ZK 證明的函式,我們將證明放在 userOp.signature 欄位,將零知識證明視為一種簽章的概念,於 validateUserOp 內驗證證明的有效性。

為了防止 userOp 在送出後遭到 front run(惡意人士攔截 userOp 並竄改 gas 相關欄位,導致交易失敗並消耗合約內的 ETH),解決方案是將 userOpHash 綁定至 ZK 證明。

具體做法是在電路中新增 userOpHash 作為公開輸入參數,但不讓它參與任何邏輯運算(即 userOpHash 僅存在於電路的參數列表中)。如此一來,證明便與特定的 userOp 綁定,概念上類似於將 userOpHash 簽入簽章。

ERC-4337 前端流程會先替 userOp 估計 gas limit,此時我們會製造假證明僅用於估計 gas(同一般 userOp 流程於估計時使用 dummy signature),直到要送出真正的 userOp 之前才會開始產生證明。由於假證明一定驗證失敗且不會走完整個運算,導致 verification gas limit 預估失準,因此 verification gas limit 要在開發時先預測好並給定。

整個 ERC-4337 流程大致上是:

  1. 在開發測試時先統計估算出合適的 verification gas limit 值 PREDEFINED_VGL
  2. 產生假證明
  3. 估計 userOp 的 gas 相關欄位
  4. 將 userOp 的 verification gas limit 估計值替換成 PREDEFINED_VGL
  5. 計算 userOpHash
  6. 產生證明
  7. 送出 userOp

七、不同郵件伺服器的問題

本專案預設寄件人與收件人的郵件伺服器都必須使用 Gmail。

收件人信箱若使用 ProtonMail,由於其端對端加密架構,ProtonMail 會在伺服器端驗證 DKIM 後將郵件加密,用戶下載的郵件已無法取得原始的郵件內文(bh= 對應的內文已無法重建),但郵件標頭的驗證依然可行。所以若應用需要截取內文資訊,就無法使用 ProtonMail 的郵件。

而若使用 Yahoo 信箱,用戶難以直接下載 .eml 格式的郵件,且下載的郵件可能缺少 DKIM-Signature 欄位,需透過 Outlook 等郵件客戶端才能正常取得完整郵件。

(以上兩個案例出自下方 Jupiter x zkEmail 案例介紹的文章連結內。)

相關應用案例介紹

Proof of Twitter

Proof of Twitter 旨在透過 Twitter 帳號發起重設密碼,會收到來自 Twitter 的一封含有確認碼的郵件,用戶並不需要真的改密碼,而是用這封郵件產生證明,並截取郵件上顯示的帳號名稱,以此推論用戶擁有某 Twitter 帳戶(前提是這封郵件沒有洩露給其他人)。

  • 它透過郵件上的 email was meant for @... 來截取用戶帳號名稱。
  • 它綁定一個以太坊地址於證明中。
  • 它使用 Partial SHA 來減少限制式,電路中的內文只有 >Not my account&lt; 字段以後的部分,之前的部分採用預先計算好的雜湊。

這是 zkemail 的入門範例,使用 circom 和 zk-regex v1 製作,詳見 部落格、專案的 藍圖Github

Email Wallet & Account Recovery

Email Wallet 是一個能透過發郵件來送交易的合約錢包。為了確保錢包的交易必須來自合法的信箱持有人,其背後使用 email-tx-builder,它引入 Relayer 的角色,用戶首先向 Relayer 發起交易的意圖,Relayer 會寄一封信給用戶,用戶必須回覆這封信,藉此確認交易意圖與信箱持有人的綁定關係,然後透過 Relayer 產生證明、發起交易與回報交易結果給用戶。

Account Recovery 能夠作為一個合約錢包的模組,指定一個擔保人的信箱具有恢復錢包的能力。假設一個合約錢包備有此模組,當錢包鑰匙遺失(或其他驗證方式無以為繼)時,擔保人可以寄信給 Relayer 請求恢復錢包,交易的意圖可能是:對合約錢包重設一把公鑰。Relayer 同樣寄封信給擔保人,擔保人回覆該信,Relayer 以此回信作為郵件證明並送出交易。

zk-jwt & StealthNote

當我們使用 Google 帳戶進行第三方登入時,背後透過 OAuth 2.0 + OpenID Connect,伺服器會回傳一個用於身分驗證的 Token,其格式為 JWT:$headers.$payload.$signature

JWT 通常用於身份識別,不像電子郵件是訊息傳遞,但它們都使用數位簽章來確保訊息的完整性,因此可以採用相似的作法,在電路中透過公鑰來驗證 JWT 的完整性,如同在電路中驗證 DKIM 一樣。

StealthNote 讓人們在平台上發佈匿名訊息,而該訊息來自可驗證的某公司域名,例如你正在某間公司上班,想要上網爆料公司內部的醜聞,在不洩露自己是誰的情況下,證明自己有該公司的郵件帳戶,讓爆料更有說服力。

技術上來說,假設公司都使用 Google Workspace 來建立郵件伺服器並使用公司自訂的域名,透過 Google 第三方登入在平台上取得用戶的 JWT,於電路中驗證其完整性,公開參數其中之一是公司的域名,作為驗證訊息與公司域名的綁定關係。匿名訊息被放在 JWT payload 內可自定義的 nonce 欄位,但為了更好的用戶體驗,它選擇放入一把公鑰,於是由該公鑰簽署的訊息都可以綁定來自某公司帳戶的持有者,詳見更清楚的 文章介紹Github

zkp2p

zkp2p 想要做去中心化的法幣與加密貨幣交換。加密貨幣的賣方將代幣鎖在合約,買方透過電子支付商如 Venmo 支付法幣給賣方,買方再透過 Venmo 的郵件收據產生證明,提交給合約解鎖並領取代幣,可參考他們的 Githubgrant proposal

zkp2p 只在 v1 中使用 ZK Email,v2 和 v3 皆改採 zkTLS 來產生支付證明。

mintmarks.fun

mintmarks.fun 使用 zkEmail 驗證 Luma 的 “Thanks for joining XXX” 郵件,作為參加某項活動的證明(但有可能用戶報名了卻沒去現場),用戶提交郵件證明後可領取一個 NFT。

這是近期(2025/12)分享於 zkemail telegram 的專案,使用 Noir 撰寫電路,同時整合 zkPassport 來防止女巫攻擊。領稿費網站的 ZK 電路有許多部分參考此專案的 實作

Jupiter x zkEmail: Soft KYC at Scale

這個案例 使用 zkEmail 證明帳號是真人持有而不是分身帳號,透過檢查帳戶信箱內有 Amazon 的訂單或 Uber 的搭乘收據。方法是先用程式抓出可能的假帳號,被程式指控為假帳號的人才需要用郵件提出證明。

結語

zkEmail 讓你將電子郵件的資訊送上鏈,透過客製化的 ZK 電路程式,開發者能夠彈性地截取郵件上的資訊來完成所需的應用,同時用戶能提出不揭露整封郵件的證明,增進隱私保護,開啟許多創新應用的可能性。

然而,電子郵件要下載到本地後上傳(或用戶授權 Dapp 讀取 Gmail 內的郵件)才能產生證明,不同郵件服務商可能有不同實作方式,或者郵件要截取的資訊太多,導致電路效能低落,這些都可能導致 zkEmail 應用出現較差的使用者體驗。此外電子郵件暴露在可能轉移、洩露或遺失的風險,安全性較低,也是應用所需考量的議題。

儘管存在上述的挑戰,仍期待未來能出現更多成功整合 zkEmail 的專案。

最後感謝 Nic 在開發過程中的協助,也感謝 Nic、 Kimi Wu Ryan 對本文提出的改進建議!

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
EthTaipei
EthTaipei
Taipei Ethereum Meetup