Реализация проверяющей стороны

Чтобы реализовать проверку адреса электронной почты на своем сайте, обновите разметку формы, чтобы запрашивать токен, и добавьте проверку входящих токенов на стороне сервера.

Как зарегистрироваться на пробный период

На сайте, который нужно подтвердить, должен быть настроен эксперимент с источником.

В 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: Selective Disclosure 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 запросите запись TXT _email-verification.gmail.com. Для этого поставщика запрос возвращает местоположение аккаунта поставщика, то есть accounts.google.com.

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

Убедитесь, что схема эмитента – https:// и что значение https://<domain> совпадает с утверждением iss в EVT.

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"]
}

Получите набор веб-ключей JSON из jwks_uri.

Используйте библиотеку SD-JWT, чтобы проверить пакет токенов. Библиотека координирует проверку:

  1. Проверка подписи издателя в EVT на соответствие полученному JWKS.
  2. Проверка подписи браузера в токене JWT KB с помощью временного открытого ключа в cnf.jwk.
  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 не должен быть пустым.
  • Ранняя отправка или повторное использование страницы. Браузер получает токен в фоновом режиме после того, как пользователь вводит или автоматически заполняет адрес электронной почты. Если отправить форму до завершения запроса, токен будет пустым. Это может произойти, если пользователь нажмет клавишу возврата, чтобы отправить форму после ввода адреса электронной почты.
  • Требования к браузеру и поставщику услуг. Пользователь должен войти в аккаунт поставщика услуг в том же профиле браузера и включить подтвержденный адрес электронной почты в настройках Chrome (chrome://settings/contactInfo).

Не удалось проверить подпись эмитента

  • Отсутствие заголовка kid. Заявление kid (идентификатор ключа) в заголовке EVT и JWKS является необязательным (например, Gmail опускает kid). Если kid отсутствует, переберите все ключи-кандидаты в jwks_uri издателя, а не завершайте работу при поиске идентификатора ключа.
  • Идентификаторы алгоритмов (EdDSA и Ed25519). Издатели и библиотеки могут указать EdDSA или Ed25519 (вместе с ES256). Убедитесь, что логика импорта и проверки JWK принимает оба идентификатора.
  • Формат источника издателя (iss). Запись TXT DNS (_email-verification.<domain>) содержит имя хоста (iss=accounts.issuer.example), а утверждение EVT iss – полный источник HTTPS (https://accounts.issuer.example, без косой черты в конце). Добавьте префикс https:// к значению записи DNS перед сравнением.

Не удалось проверить привязку ключа (KB-JWT)

  • Несоответствие или истечение срока действия одноразового кода. Убедитесь, что код nonce, отрисованный в теге <input>, соответствует одноразовому коду активного сеанса на вашем сервере и не был перезаписан другой вкладкой или использован в предыдущем запросе.
  • Несоответствие аудитории (aud). Заявка aud – это HTTPS-источник верификатора (https://verifier.example без пути и косой черты в конце).

Не удалось сравнить утверждения об адресе электронной почты (email)

  • Регистр и канонизация. Chrome 156 и более поздние версии возвращают утверждение email в том виде, в котором оно было введено в форму, но более ранние версии браузера или поставщики могут возвращать канонизированный адрес (например, First.Last@example.com вместо first.last@example.com). При сопоставлении утверждения email токена со значением, указанным в форме, используйте сравнение без учета регистра.