Hotdry.
ai-security

WICG 邮箱验证协议:SD-JWT+KB 的隐私保护工程实践

深入解析 WICG 邮箱验证协议的技术架构,重点分析 SD-JWT+KB 的实现机制与隐私保护特性。

引言:邮箱验证的隐私困境与标准化需求

在 Web 应用的日常运营中,邮箱验证是用户注册和身份确认的必备环节。传统的验证方式长期存在用户体验差和隐私泄露的双重问题。经典的 "点击邮件链接验证" 流程要求用户在应用和邮箱之间多次切换,导致显著的用户流失;而社交登录虽然便捷,却需要应用与多家服务商建立复杂关系。

更为关键的是,传统方式向邮件服务商暴露了用户的应用使用行为。邮件验证的发送时机和内容直接泄露了用户的在线活动轨迹,这构成了严重的隐私风险。

WICG(Web Incubator Community Group)提出的 Email Verification Protocol 正是为了解决这一 Web 基础问题。该协议通过 SD-JWT+KB(Selective Disclosure JSON Web Token with Key Binding)技术实现了革命性的无邮件验证流程,同时通过浏览器中介架构保护用户隐私。

技术架构:SD-JWT+KB 与浏览器中介

选择性披露 JWT 的密钥绑定特性

该协议的核心是 SD-JWT+KB 令牌结构,基于 IETF 正在制定的选择性披露 JWT 标准草案。协议巧妙地利用了 SD-JWT 的 Key Binding 特性,而不是选择性披露功能,实现 token 发行和展示的完全分离。

SD-JWT+KB 的结构设计:

SD-JWT~KB-JWT

第一个组件是 SD-JWT(发行 token),由邮箱验证发行者签名,包含:

  • iss:发行者标识符
  • iat:签发时间
  • cnf:确认声明,包含浏览器公钥
  • email:用户邮箱地址
  • email_verified:验证状态声明

第二个组件是 KB-JWT(Key Binding token),由浏览器使用私钥签名,包含:

  • aud:依赖方(RP)的 origin
  • nonce:防重放攻击的随机数
  • iat:创建时间
  • sd_hash:第一个 JWT 的 SHA-256 哈希

浏览器中介的隐私保护架构

协议设计的最大创新在于浏览器完全作为中介,隔离了发行者和依赖方。发行者通过网络流量分析无法推断用户的具体应用使用情况,因为所有请求和响应都通过用户的浏览器进行路由和转发。

这种设计实现了:

  • 发行者隐私:无法获知用户正在验证的具体应用
  • 依赖方最小信任:只需信任发行者签发的邮箱验证状态
  • 用户控制权:验证过程完全在用户浏览器本地执行

DNS 委托机制

协议通过 DNS TXT 记录实现邮箱域名的验证服务授权:

_email-verification.$EMAIL_DOMAIN TXT "iss=issuer.example"

这种设计的优势:

  • 利用现有的 Web PKI 信任体系
  • 确保只有域名控制者可以授权验证服务
  • 防止恶意第三方冒充邮箱验证发行者
  • 兼容传统域名管理系统

协议流程:六步验证的工程化实现

步骤一:Email Request(邮箱请求)

依赖方(RP)服务器首先生成随机 nonce 并绑定到用户会话:

// 服务器端 nonce 生成
const nonce = crypto.randomUUID();
session.set('email_verification_nonce', nonce);

页面返回包含特定属性的 HTML 元素:

<input 
  id="email" 
  type="email" 
  autocomplete="email" 
  nonce="12345677890..random"
>
<script>
const input = document.getElementById('email');
input.addEventListener('emailverified', e => {
  // 接收浏览器转发的 presentation token
  console.log({ presentationToken: e.presentationToken });
});
</script>

步骤二:Email Selection(邮箱选择)

当用户聚焦到邮箱输入字段时,浏览器显示已保存的邮箱地址列表:

  • 浏览器基于用户历史输入提供智能建议
  • 用户选择已保存邮箱或手动输入新地址
  • 未来版本将支持通过 passkey 在公共设备上进行验证

步骤三:Token Request(令牌请求)

浏览器执行以下关键操作序列:

DNS 查询阶段

// 解析邮箱域名并查询 TXT 记录
const emailDomain = 'example.com';
const dnsQuery = `_email-verification.${emailDomain}`;
const txtRecord = await dns.lookup(dnsQuery, { type: 'TXT' });
// 返回: { iss: 'issuer.example' }

元数据获取阶段: 浏览器访问发行者的 .well-known 端点获取配置:

{
  "issuance_endpoint": "https://accounts.issuer.example/email-verification/issuance",
  "jwks_uri": "https://accounts.issuer.example/email-verification/jwks",
  "signing_alg_values_supported": ["EdDSA", "RS256"]
}

密钥对生成与请求 token 创建

// 浏览器生成 Ed25519 密钥对
const keyPair = await crypto.subtle.generateKey(
  { name: 'Ed25519' },
  true,
  ['sign', 'verify']
);

// 创建请求 JWT
const header = {
  alg: 'EdDSA',
  typ: 'JWT',
  jwk: await crypto.subtle.exportKey('jwk', keyPair.publicKey)
};

const payload = {
  aud: 'issuer.example',
  iat: Math.floor(Date.now() / 1000),
  jti: crypto.randomUUID(),
  email: 'user@example.com'
};

步骤四:Token Issuance(令牌发行)

发行者端执行多层验证:

请求验证

  • 检查 Content-Type: application/x-www-form-urlencoded
  • 验证 Sec-Fetch-Dest: email-verification
  • 确认浏览器发送了有效的第一方 cookie

Token 验证

  • 解析 JWT 并验证签名使用 jwk 声明中的公钥
  • 确认 aud 声明精确匹配发行者标识符
  • 验证 iat 声明在当前时间的 60 秒窗口内
  • 检查 email 声明包含语法有效的邮箱地址

用户认证确认

  • 验证当前 cookie 会话代表已登录用户
  • 确认登录用户对请求中的邮箱地址有控制权

SD-JWT 生成

{
  "iss": "issuer.example",
  "iat": 1724083200,
  "cnf": {
    "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
  },
  "email": "user@example.com",
  "email_verified": true
}

步骤五:Token Presentation(令牌展示)

浏览器验证 SD-JWT 并构建最终 presentation token:

SD-JWT 验证

// 解析邮箱域名并验证发行者一致性
const emailDomain = extractEmailDomain(payload.email);
const dnsRecord = await queryEmailVerificationDNS(emailDomain);
if (payload.iss !== dnsRecord.iss) {
  throw new Error('发行者不匹配');
}

// 验证发行者公钥签名
const issuerKey = await fetchJWKS(payload.kid);
const isValidSignature = await verifyJWTSignature(
  sdJwt, issuerKey, payload.alg
);

Key Binding JWT 创建

const kbPayload = {
  aud: 'https://rp.example',  // 依赖方 origin
  nonce: session.nonce,      // 原始随机数
  iat: Math.floor(Date.now() / 1000),
  sd_hash: sha256(sdJwt)     // SD-JWT 的哈希值
};

完整 SD-JWT+KB 组合

const presentationToken = `${sdJwt}~${kbJwt}`;

步骤六:Token Verification(令牌验证)

依赖方服务器执行双重验证链:

KB-JWT 验证

const [kbJwt, sdJwt] = presentationToken.split('~');

// 验证 audience 匹配
if (kbPayload.aud !== rpOrigin) {
  throw new Error('Audience 不匹配');
}

// 验证 nonce 与会话绑定
if (kbPayload.nonce !== session.nonce) {
  throw new Error('Nonce 验证失败');
}

// 验证时间窗口合理性
const timeDelta = Math.abs(kbPayload.iat * 1000 - Date.now());
if (timeDelta > 5 * 60 * 1000) {  // 5分钟窗口
  throw new Error('Token 过期');
}

SD-JWT 验证

// 通过 DNS 和 .well-known 获取发行者公钥
const emailDomain = extractEmailDomain(sdPayload.email);
const dnsRecord = await queryEmailVerificationDNS(emailDomain);
const issuerMetadata = await fetchWellKnown(dnsRecord.iss);
const issuerKeys = await fetchJWKS(issuerMetadata.jwks_uri);

// 验证发行者签名
const isValid = await verifyJWTSignature(sdJwt, issuerKeys[kid], alg);

// 验证关键声明
if (!sdPayload.email_verified) {
  throw new Error('邮箱未验证');
}

隐私保护机制:去中心化身份认证实践

最小信息披露原则

协议严格遵循最小信息披露原则:

  • 依赖方视角:仅获取验证状态的必要信息(email + email_verified)
  • 发行者视角:无法追踪用户的应用使用模式
  • 中间流量:浏览器中介完全隐藏了应用身份信息

DNS 委托的安全信任链

DNS TXT 记录的委托机制提供了:

  • 域名控制证明:只有域名所有者可以配置验证服务
  • Web PKI 继承:利用现有的 DNS 安全机制
  • 去中心化信任:无需依赖集中化的身份提供商
  • 可验证性:任何人都可以验证委托关系的真实性

防跟踪设计

协议通过多种机制防止跟踪:

  • 时间窗口限制:所有令牌都有短时间有效期
  • 一次性 nonce:防止重放攻击和跟踪
  • 浏览器生成密钥:发行者无法通过密钥关联用户行为

部署实践:工程实现的关键考量

发行者端实现

DNS 配置模板

# 邮箱域名运营商需配置
_email-verification.example.com. TXT "iss=accounts.example.com"

密钥轮换管理

// KID 格式:日期或版本号
const kid = '2025-11-10-v1';
const keyMetadata = {
  kid,
  alg: 'EdDSA',
  created_at: '2025-11-10T00:00:00Z',
  expires_at: '2026-11-10T00:00:00Z'
};

发行端点实现

app.post('/email-verification/issuance', async (req, res) => {
  try {
    // 验证请求头
    if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') {
      return res.status(415).json({ error: 'invalid-content-type' });
    }
    
    // 验证用户认证和邮箱控制权
    const user = await authenticateUser(req.cookies);
    if (!user || !user.ownsEmail(req.body.email)) {
      return res.status(401).json({ error: 'authentication_required' });
    }
    
    // 验证请求 token
    const requestToken = await verifyRequestToken(req.body.request_token);
    
    // 生成 SD-JWT
    const issuanceToken = await generateSDJWT({
      iss: 'accounts.example.com',
      email: req.body.email,
      email_verified: true,
      cnf: { jwk: requestToken.jwk }
    });
    
    res.json({ issuance_token: issuanceToken });
  } catch (error) {
    res.status(400).json({ error: 'invalid_request', error_description: error.message });
  }
});

依赖方端集成

前端事件处理

// HTML 集成示例
<input type="email" id="email" autocomplete="email">
<div id="status"></div>

<script>
document.getElementById('email').addEventListener('emailverified', async (e) => {
  const statusDiv = document.getElementById('status');
  statusDiv.textContent = '验证中...';
  
  try {
    const response = await fetch('/api/verify-email', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ 
        presentation_token: e.presentationToken 
      })
    });
    
    const result = await response.json();
    if (result.verified) {
      statusDiv.textContent = '邮箱验证成功';
    } else {
      statusDiv.textContent = '验证失败: ' + result.error;
    }
  } catch (error) {
    statusDiv.textContent = '网络错误';
  }
});
</script>

后端验证逻辑

app.post('/api/verify-email', async (req, res) => {
  try {
    const { presentation_token } = req.body;
    const sessionNonce = req.session.email_verification_nonce;
    
    // 分离和验证 KB-JWT
    const [kbJwt, sdJwt] = presentation_token.split('~');
    const kbPayload = await verifyKBJWT(kbJwt, {
      aud: req.get('origin'),
      nonce: sessionNonce
    });
    
    // 验证 SD-JWT
    const sdPayload = await verifySDJWT(sdJwt);
    
    // 邮箱验证成功
    await req.session.markEmailVerified(sdPayload.email);
    res.json({ verified: true, email: sdPayload.email });
    
  } catch (error) {
    res.status(400).json({ verified: false, error: error.message });
  }
});

浏览器实现要求

浏览器需要提供:

  • 密钥生成 API:支持 Ed25519 等现代加密算法
  • DNS 查询接口:允许 Web 应用查询 TXT 记录
  • 事件分发机制emailverified 事件的标准化实现
  • 邮箱地址管理:自动填充和安全存储功能

安全性分析:多层次防护体系

密码学安全保障

协议强制要求使用现代密码学算法:

  • 签名算法:推荐 EdDSA(Ed25519),兼容 RSA 系列
  • 哈希算法:使用 SHA-256 进行完整性和防篡改验证
  • 密钥强度:要求足够强度的密钥长度和安全参数

时间窗口与防重放

所有令牌都有严格的时间约束:

  • 请求 token:发行者验证在 60 秒窗口内
  • 展示 token:依赖方验证在合理时间窗口内
  • 会话绑定:nonce 确保令牌与会话的唯一对应关系

错误处理与安全响应

协议定义了详细的安全错误响应:

// 认证要求错误
{
  "error": "authentication_required",
  "error_description": "用户必须已登录并对请求的邮箱有控制权"
}

// 请求格式错误
{
  "error": "invalid_request",
  "error_description": "请求 token 格式错误或缺少必要声明"
}

// 服务器错误
{
  "error": "server_error",
  "error_description": "临时服务器错误,请稍后重试"
}

标准化进展与生态展望

当前标准化状态

WICG Email Verification Protocol 仍处于工作组讨论阶段,相关的标准化工作包括:

  • IETF SD-JWT 草案:选择性披露 JWT 的标准化
  • W3C 可验证凭证:数字凭证的互操作标准
  • WebAuthn 集成:无密码认证的标准化框架

生态采用预期

协议的普及将推动:

  • Web 标准演进:改进身份验证的标准化程度
  • 隐私保护增强:减少对传统邮件验证的依赖
  • 用户体验优化:实现真正的零摩擦验证流程
  • 开发者生态:提供标准化工具和库

潜在挑战与解决方案

挑战一:浏览器实现差异

  • 不同浏览器对 crypto API 的实现可能存在差异
  • 解决方案:制定详细的测试套件和兼容性指南

挑战二:域名运营商支持

  • 需要域名运营商配置 DNS TXT 记录
  • 解决方案:与主要域名注册商合作,提供自动化配置工具

挑战三:用户教育

  • 新的验证流程需要用户理解和接受
  • 解决方案:渐进式部署,配合用户体验优化

结论:Web 身份验证的范式转变

WICG Email Verification Protocol 代表了 Web 身份验证领域的重大进步。通过 SD-JWT+KB 技术和浏览器中介架构,该协议成功解决了传统邮箱验证的用户体验问题和隐私泄露风险。

该协议的核心价值在于:

  • 技术优雅性:通过标准化的密码学技术实现安全验证
  • 隐私保护性:最小化信息披露,保护用户隐私
  • 用户友好性:消除验证流程中的用户摩擦
  • 生态兼容性:基于现有 Web 基础设施的增量改进

随着 Web 生态向更加隐私保护和用户控制的方向发展,类似的标准化协议将发挥越来越重要的作用。Email Verification Protocol 不仅是一个技术标准,更是对 Web 身份验证架构的根本性重新思考。

该协议的最终标准化和广泛部署将为下一代 Web 身份管理奠定坚实基础,推动整个行业向更加安全、透明和用户友好的方向发展。这标志着 Web 社区在平衡安全性、隐私性和用户体验方面取得了重要进展。


参考资料

查看归档