依赖方实现

如需在您的网站上实现电子邮件验证,请更新表单标记以请求令牌,并为传入的令牌添加服务器端验证。

注册参加源试用

验证网站必须在其网站上配置源试用。

自 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 密钥集。

使用 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>) 属于同一网站。您可以在开发者工具中检查网站上的源试用配置,具体路径为应用 > 框架 > (选择相关框架)> 源试用。
  • 表单标记:<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 查找失败时失败。
  • 算法标识符(EdDSA 和 Ed25519):签发者和库可以指定 EdDSA 或 Ed25519(以及 ES256)。请确保您的 JWK 导入和验证逻辑接受这两个标识符。
  • 签发者 (iss) 源格式:DNS TXT 记录 (_email-verification.<domain>) 包含裸主机名 (iss=accounts.issuer.example),而 EVT iss 声明是完整的 HTTPS 源(https://accounts.issuer.example,不带尾部斜杠)。在比较之前,为 DNS 记录值添加前缀 https://。

密钥绑定 (KB-JWT) 验证失败

  • 随机数不匹配或已过期:确保 <input> 中呈现的 nonce 与服务器上的有效会话随机数相匹配,并且未被其他标签页覆盖或被之前的请求使用。
  • 受众群体 (aud) 不匹配:aud 声明是验证者的 HTTPS 源 (https://verifier.example,不含路径或尾部斜杠)。

电子邮件声明 (email) 比较失败

  • 大小写和规范化:Chrome 156 及更高版本会按原样返回表单中输入的 email 声明(逐字节),但更早的浏览器版本或提供方可能会返回规范化的地址(例如,first.last@example.com 的规范化地址为 First.Last@example.com)。在将令牌的 email 声明与提交的表单值进行匹配时,请使用不区分大小写的比较。