信賴方實作

如要在網站上實作電子郵件驗證,請更新表單標記來要求權杖,並為傳入的權杖新增伺服器端驗證。

註冊來源試用

驗證網站時,必須在網站上設定原始碼試用。

自 Chrome 154 起,系統支援第三方來源試用版,但請注意:試用版註冊的來源必須與發行者位於相同網站。例如:

  • 發卡機構網域:issuer.example
  • OT 註冊者:https://issuer.example
  • JavaScript 來源:https://issuer.example (或 https://app.issuer.example,適用於子網域比對)

設定表單欄位

在電子郵件提交表單中加入隱藏的權杖欄位:

<input
  type="email"
  name="email-address"
  autocomplete="email">
<input
  type="hidden"
  name="token"
  autocomplete="email-verification-token"
  nonce="rAnD0m-VaLuE">

欄位規定:

  • 電子郵件地址欄位:設定 type="email" 和 autocomplete="email",讓 Chrome 可以自動填入並辨識地址。
  • 權杖欄位屬性:
    • 設定 autocomplete="email-verification-token":Chrome 會識別這個欄位,以便在提交時填入權杖。
    • 設定 nonce="<VALUE>":網站必須提供與工作階段綁定的專屬隨機碼,以驗證表單提交。

驗證電子郵件驗證權杖 (EVT)

使用者提交表單時,您的伺服器會收到電子郵件地址和隱藏欄位中的權杖。如果權杖欄位空白,表示瀏覽器或供應商不支援 EVP,或是使用者略過驗證。如果發生這種情況,請改用現有的驗證程序,例如傳送動態密碼或魔術連結。

如有權杖,請按照下列步驟驗證:

  1. 使用 SD-JWT 程式庫剖析權杖。
  2. 驗證預期值和工作階段聲明。
  3. 驗證 DNS 委派。
  4. 探索簽發者中繼資料並擷取 JWKS。
  5. 驗證加密簽章和金鑰繫結。

1. 剖析權杖

權杖採用 RFC 9901:選擇性揭露 JWT (SD-JWT+KB) 格式。請使用適用於您平台的適當程式庫,剖析及驗證權杖。舉例來說,如果是 Node,您可以使用 @sd-jwt/core 和 jose。原始格式如下:簽發者簽署的 JWT,後接零或多個揭露事項,最後是金鑰繫結 JWT,每個元件之間以半形波浪號分隔:

<Issuer-signed EVT>~<Disclosure 1>~...~<Disclosure N>~<Key Binding JWT>

在目前的實作方式中,權杖包含零個揭露事項 (<Issuer-signed EVT>~<Key Binding JWT>)。不過,這項設定日後可能會變更。

使用程式庫解碼權杖:

import { decodeSdJwtSync } from "@sd-jwt/core";
import { createHash } from "node:crypto";
const hasher = (data, alg) =>
  createHash(alg === "sha-256" ? "sha256" : alg)
    .update(data)
    .digest();
const decoded = decodeSdJwtSync(rawToken, hasher);
const evtPayload = decoded.jwt.payload;
const kbPayload = decoded.kbJwt?.payload;

如果 verifier.example 驗證 demo@provider.example,解碼後的權杖會類似下列內容:

{
  "evtJwtDecodedHeader": {
    "typ": "evt+jwt",
    "alg": "EdDSA",
    "kid": "issuer-key-id"
  },
  "evtJwtDecodedPayload": {
    "iss": "https://provider.example",
    "iat": 12345678901,
    "exp": 12345679901,
    "cnf": {
      "jwk": {
        "kty": "OKP",
        "crv": "Ed25519",
        "x": "pUbLiCkEy123pUbLiCkEy123pUbLiCkEy123"
      }
    },
    "email": "demo@provider.example",
    "email_verified": true
  },
  "kbJwtDecodedHeader": {
    "alg": "EdDSA",
    "typ": "kb+jwt"
  },
  "kbJwtDecodedPayload": {
    "aud": "https://verifier.example",
    "iat": 12345678901,
    "nonce": "rAnDoM123rAnDoM123rAnDoM123rAnDoM123",
    "sd_hash": "hAsH456hAsH456hAsH456hAsH456hAsH456"
  },
  "disclosures": []
}

2. 驗證預期值和工作階段聲明

確認酬載中的基本值與您提供和預期的值相符:

  • email_verified:必須是 true。
  • email:必須與表單中提交的電子郵件地址相符。
  • aud (目標對象):必須與網站來源相符。
  • nonce:必須與表單中提供的隨機碼相符。
  • iat (核發時間) 和 exp (到期時間):確認權杖在有效時間範圍內,且尚未過期。

3. 驗證 DNS 委派

驗證電子郵件地址網域的 _email-verification DNS 記錄。舉例來說,如要查詢 demo@gmail.com,請查詢 _email-verification.gmail.com TXT 記錄。對於這個供應商,查詢會傳回帳戶供應商的位置,也就是 accounts.google.com。

$ dig +short TXT _email-verification.gmail.com
"iss=accounts.google.com"

確認簽發者架構為 https://,且 https://<domain> 與 EVT 中的 iss 聲明相符。

4. 驗證 EVT 簽章

從 https://<issuer>/.well-known/email-verification 擷取簽發者的探索中繼資料:

{
  "issuance_endpoint": "https://accounts.google.com/gsi/email-verification/issue",
  "jwks_uri": "https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks",
  "signing_alg_values_supported": ["EdDSA"]
}

從 jwks_uri 擷取 JSON Web Key Set。

使用 SD-JWT 程式庫驗證權杖套件。程式庫會協調驗證作業:

  1. 根據擷取的 JWKS 驗證 EVT 上的簽名。
  2. 使用 cnf.jwk 中的暫時性公開金鑰,驗證 KB-JWT 上的瀏覽器簽章。
  3. 驗證金鑰繫結 (aud、nonce 和摘要雜湊 sd_hash)。

Node.js 中的驗證邏輯範例:

import { SDJwtInstance } from "@sd-jwt/core";
import { importJWK, compactVerify } from "jose";
import { createHash } from "node:crypto";
const hasher = (data, alg) =>
  createHash(alg === "sha-256" ? "sha256" : alg)
    .update(data)
    .digest();
const sdJwt = new SDJwtInstance({ hasher });
sdJwt.config({
  hasher,
  // Verifier for the Issuer-signed EVT
  verifier: async (data, sig) => {
    const token = `${data}.${sig}`;
    const header = decoded.jwt.header;
    const headerAlg = header.alg || "ES256";
    // Match by kid if present, or iterate across matching algorithm keys
    const keysToTry = header.kid
      ? jwksData.keys.filter(k => k.kid === header.kid)
      : jwksData.keys;
    for (const jwk of keysToTry) {
      try {
        const pubKey = await importJWK(jwk, jwk.alg || headerAlg);
        await compactVerify(token, pubKey);
        return true;
      } catch {
        // Try next candidate key
      }
    }
    return false;
  },
  // Verifier for the Key Binding JWT (KB-JWT)
  kbVerifier: async (data, sig) => {
    try {
      const browserJwkKey = evtPayload.cnf?.jwk;
      if (!browserJwkKey) return false;
      const pubKey = await importJWK(browserJwkKey, decoded.kbJwt.header.alg || "ES256");
      await compactVerify(`${data}.${sig}`, pubKey);
      return true;
    } catch {
      return false;
    }
  },
});
// The library automatically verifies EVT signature, KB-JWT signature, audience, nonce, and sd_hash
const result = await sdJwt.verify(rawToken, {
  kb: {
    expectedNonce: sessionNonce,
    expectedAudience: "https://example.com",
    required: true,
  },
});
const verifiedPayload = result.payload;

如果所有步驟都成功,表示您已向供應商驗證電子郵件地址。 如果驗證失敗,請使用一般流程,改為傳送確認電子郵件給使用者。

疑難排解

如果驗證失敗或瀏覽器未提供權杖,請檢查下列常見問題:

提交時權杖欄位空白

  • 來源試用註冊:確認網頁上是否提供 Origin-Trial 標頭或 <meta> 標記。如果是第三方來源試用 (Chrome 154 以上版本),註冊的試用來源必須與發行者 (https://<issuer-domain>) 位於相同網站。您可以在開發人員工具中,依序選取「Application」 >「Frames」 > (選取相關影格) >「Origin trials」,檢查網站上的來源試用設定。
  • 表單標記:<input type="email" autocomplete="email"> 和 <input type="hidden" autocomplete="email-verification-token" nonce="..."> 必須位於同一個 <form> 元素中 (不得跨 Shadow DOM 邊界隔離),且 nonce 不得為空。
  • 提早提交或重複使用頁面:瀏覽器會在輸入或自動填入電子郵件後,於背景擷取權杖。在要求完成前提交表單會導致權杖空白。如果使用者輸入電子郵件地址後按下 Return 鍵提交表單,就可能發生這種情況。
  • 瀏覽器和供應商必要條件:使用者必須在同一個瀏覽器設定檔中登入參與計畫的供應商,並在 Chrome 設定 (chrome://settings/contactInfo) 中啟用「已驗證的電子郵件」。

發卡機構簽章驗證失敗

  • 缺少 kid 標頭:EVT 標頭和 JWKS 中的 kid (金鑰 ID) 聲明為選用項目 (例如 Gmail 會省略 kid)。如果沒有 kid,請在簽發者的 jwks_uri 中逐一檢查所有候選金鑰,而不是在金鑰 ID 查詢失敗時停止作業。
  • 演算法 ID (EdDSA 和 Ed25519):簽發者和程式庫可以指定 EdDSA 或 Ed25519 (以及 ES256)。請確保 JWK 匯入和驗證邏輯接受這兩種 ID。
  • 簽發者 (iss) 來源格式:DNS TXT 記錄 (_email-verification.<domain>) 包含裸主機名稱 (iss=accounts.issuer.example),而 EVT iss 聲明是完整的 HTTPS 來源 (https://accounts.issuer.example,不含尾端斜線)。比較前,請在 DNS 記錄值加上前置字元 https://。

金鑰繫結 (KB-JWT) 驗證失敗

  • Nonce 不符或已過期:請確認 <input> 中呈現的 nonce 與伺服器上的有效工作階段 Nonce 相符,且未遭其他分頁覆寫,或由先前的要求使用。
  • 目標對象 (aud) 不符:aud 聲明是驗證者的 HTTPS 來源 (https://verifier.example,沒有路徑或尾端斜線)。

電子郵件權杖附加資訊 (email) 比較失敗

  • 大小寫和正規化:Chrome 156 以上版本會傳回表單中輸入的 email 聲明 (逐一比對位元組),但較舊的瀏覽器版本或供應商可能會傳回正規化地址 (例如 first.last@example.com 的 First.Last@example.com)。比對權杖的 email 聲明與提交的表單值時,請使用不區分大小寫的比較方式。