Реализация поставщика услуг электронной почты (эмитента)

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

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

Как настроить обнаружение эмитента

Чтобы браузеры могли автоматически обнаруживать ваши конечные точки проверки при выборе адреса электронной почты, принадлежащего вашему домену, предоставьте доступ к конфигурации с помощью DNS и конечной точки HTTP .well-known.

Как настроить запись делегирования DNS

Настройте TXT-запись DNS в домене электронной почты, которая делегирует полномочия на проверку идентификатору эмитента. Эти идентификаторы могут использовать один и тот же домен, в зависимости от вашей инфраструктуры.

Формат записи: _email-verification.<email-domain>

Пример файла зоны:

_email-verification.example.com IN TXT "iss=accounts.issuer.example"

Как разместить конечную точку .well-known/email-verification

Разместите файл JSON с метаданными в домене эмитента по пути /.well-known/. В этом файле указаны ваши возможности по выпуску сертификатов и криптографические алгоритмы подписи, которые поддерживает ваша инфраструктура.

Конечная точка: https://<issuer-domain>/.well-known/email-verification

Пример ответа:

{
  "issuance_endpoint": "https://accounts.issuer.example/email-verification/issuance",
  "jwks_uri": "https://accounts.issuer.example/.well-known/vc-public-jwks",
  "signing_alg_values_supported": ["EdDSA", "ES256"]
}

Как разместить конечную точку .well-known/web-identity

Возможно, вы уже реализовали дополнительный ресурс JSON .well-known как часть Federated Credentials (FedCM) API. Этот ресурс содержит ссылки на конечную точку аккаунта и URL для входа.

Конечная точка: https://<domain>/.well-known/web-identity

Пример ответа:

{
  "accounts_endpoint": "https://accounts.issuer.example/accounts",
  "login_url": "https://accounts.issuer.example/login"
}

Используйте конечную точку аккаунтов

Конечная точка accounts из FedCM API предоставляет список аккаунтов, в которые выполнен вход в данный момент. Ниже приведен пример минимального ответа. Подробнее о реализации поставщика идентификационной информации…

Конечная точка: как указано в .well-known/web-identity.

Пример ответа:

{
  "accounts": [
    {
      "id": "demo-example",
      "name": "Demo User",
      "email": "demo@example.com",
      "given_name": "Demo"
    }
  ]
}

Как интегрировать Login Status API

У пользователя должен быть активный сеанс с поставщиком, и вы должны сообщить об этом браузеру с помощью API статуса входа.

Когда пользователь успешно входит в аккаунт или выходит из него, отправьте соответствующий заголовок HTTP-ответа:

Set-Login: logged-in
Set-Login: logged-out

Вы также можете обновить статус с помощью JavaScript в контексте веб-приложения:

navigator.login.setStatus("logged-in");
navigator.login.setStatus("logged-out");

Как обрабатывать запросы на выпуск

Ваш issuance_endpoint получает запрос application/json POST, который содержит ключ email и заголовки HTTP Message Signatures для Signature, Signature-Input и Signature-Key.

Используйте библиотеку, которая поддерживает структурированные заголовки и подписи HTTP-сообщений для вашей среды. В Node.js можно использовать structured-headers и http-message-sig.

Полный формат запроса:

POST /email-verification/issuance HTTP/1.1
Host: provider.example
Accept: application/json
Content-Digest: sha-256=:aBc123aBc123aBc123aBc123aBc123=:
Content-Type: application/json
Sec-Fetch-Dest: email-verification
Signature: sig=:+dEf567dEf567/dEf567dEf567dEf567/dEf567==:
Signature-Input: sig=("@method" "@authority" "@path" "content-digest" "signature-key");created=1786455840
Signature-Key: sig=hwk;crv="Ed25519";kty="OKP";x="gHi890_gHi890_gHi890"

{email: "demo@example.com"}

Проанализируйте и проверьте запрос:

  • Аутентификация сеанса. Проверьте собственные файлы cookie сеанса, отправленные вместе с запросом. Пользователь должен пройти аутентификацию.
  • Заголовок Sec-Fetch-Dest: задайте значение email-verification.
  • Подписи HTTP-сообщений. Проверьте подпись запроса с помощью временного открытого ключа в Signature-Key и подтвердите действительность Content-Digest.
  • Полезная нагрузка. Тело JSON содержит строку email, необходимую для проверки.

Ответ на запрос выпуска

После успешной проверки токена сеанса и запроса создайте подписанный SD-JWT, возвращаемый в формате JSON, с помощью подходящих библиотек для вашей платформы. Например, для Node можно использовать @sd-jwt/core и jose.

Формат необработанной полезной нагрузки должен выглядеть примерно так:

{
  "iss": "https://accounts.issuer.example",
  "iat": 1780272000,
  "exp": 1780272300,
  "cnf": {
    "jwk": {
      "kty": "EC",
      "crv": "P-256",
      "x": "pUbLiCKeY123pUbLiCKeY123pUbLiCKeY123",
      "y": "pUbLiCKeY456pUbLiCKeY456pUbLiCKeY456"
    }
  },
  "email": "demo@example.com",
  "email_verified": true
}

Создайте, подпишите и верните токен:

import { importJWK, CompactSign } from "jose";
import { SDJwtInstance } from "@sd-jwt/core";
import crypto from "node:crypto";
// Issuer private key from secure storage
const privateKey = await importJWK(PRIVATE_KEY_JWK, "EdDSA");
const origin = url.origin;
const currentTime = Math.floor(Date.now() / 1000);
const evtPayload = {
  iss: origin,
  iat: currentTime,
  exp: currentTime + 300, // 5 minutes
  cnf: {
    jwk: browserJwk,
  },
  email: payload.email, // exactly as received in payload
  email_verified: true,
};
const sdJwt = new SDJwtInstance({
  signer: async (data) => {
    const [headerB64, payloadB64] = data.split(".");
    const header = JSON.parse(Buffer.from(headerB64, "base64url").toString());
    const payload = Buffer.from(payloadB64, "base64url");
    const signed = await new CompactSign(payload).setProtectedHeader(header).sign(privateKey);
    return signed.split(".").pop()!;
  },
  signAlg: "EdDSA",
  hasher: async (data, alg) => {
    const nodeAlg = alg.replace("-", "");
    return new Uint8Array(crypto.createHash(nodeAlg).update(data).digest());
  },
  hashAlg: "sha-256",
  saltGenerator: async () => crypto.randomBytes(16).toString("base64url"),
});
const issuanceToken = await sdJwt.issue(evtPayload, undefined, {
  header: {
    alg: "EdDSA",
    kid: PRIVATE_KEY_JWK.kid,
    typ: "evt+jwt",
  },
});
return sendResponse({ issuance_token: issuanceToken, }, 200);

Текст ответа будет выглядеть примерно так:

{
  "issuance_token": "tOkEn123tOkEn123tOkEn123...~"
}

После того как вы создадите и подпишете токен подтверждения адреса электронной почты, браузер передаст его на сайт верификатора для проверки.

Устранение неполадок

Если браузер не подключается к вашим конечным точкам или отклоняет выданные токены, проверьте, нет ли следующих распространенных проблем:

Браузер никогда не вызывает accounts_endpoint или issuance_endpoint

  • Статус входа не задан. Chrome отправляет запросы к конечным точкам, только если знает, что пользователь вошел в аккаунт. Убедитесь, что в процессе входа задается HTTP-заголовок Set-Login: logged-in или вызывается функция navigator.login.setStatus("logged-in").
  • Сеансовые файлы cookie заблокированы (SameSite=None). Браузер получает ваши файлы accounts_endpoint и issuance_endpoint от проверяющей стороны на другом сайте. Поскольку это межсайтовые запросы, в файле cookie сеанса должен быть указан атрибут SameSite=None; Secure. Файл cookie SameSite=Lax работает при тестировании на верификаторе одного сайта, но не передается в межсайтовых запросах.
  • Несоответствие аккаунта или проблемы с обнаружением. Убедитесь, что команда _email-verification.<email-domain> возвращает одну запись TXT (iss=<issuer-domain> без https://), оба конечных пункта .well-known возвращают Content-Type: application/json, а ответ accounts_endpoint содержит аккаунт, email которого соответствует введенному адресу.

Не удалось подтвердить запрос на выпуск

  • Заголовок Sec-Fetch-Dest. В Chrome 154 и более поздних версиях отправляется заголовок Sec-Fetch-Dest: email-verification (с дефисом), а в Chrome 153 – emailverification (без дефиса). Во время развертывания принимайте оба значения.
  • Несоответствие подписи сообщения HTTP (@authority) за прокси-сервером. При проверке подписи RFC 9421 компонент @authority отражает общедоступный хост. Если ваш сервер находится за обратным прокси-сервером или балансировщиком нагрузки, восстановите URL подтверждения, используя X-Forwarded-Host (или ваш общедоступный источник), а не внутреннее имя хоста, и вычислите Content-Digest для необработанных байтов тела запроса до синтаксического анализа JSON.

Браузер отклоняет возвращенный файл issuance_token

  • Измененное или канонизированное утверждение email. В Chrome 156 и более поздних версиях браузер проверяет, чтобы утверждение email в EVT совпадало с запрошенным утверждением email побайтово. Если сервер приводит адрес к каноническому формату (например, возвращает First.Last@example.com, когда был запрошен адрес first.LAST@example.com), Chrome удаляет токен. Сопоставьте запрос с аккаунтом пользователя, но верните строку email, полученную в теле запроса.
  • Отсутствует завершающая тильда (~). Даже если раскрытие информации не требуется, issuance_token должен быть действительным SD-JWT, заканчивающимся тильдой (<Issuer-signed-JWT>~), чтобы браузер мог добавить <KB-JWT>.
  • Несоответствие iss или cnf.jwk. Убедитесь, что утверждение EVT iss является точным источником HTTPS (https://<issuer-domain>, без косой черты в конце), соответствующим метаданным .well-known/email-verification, а cnf.jwk содержит временный открытый ключ браузера из заголовка Signature-Key.