QR‑коды вместо стандартной формы входа, отправка Magic Link на почту, OAuth — беспарольные способы стали привычными, и это настолько удобно, что не вызывает у обывателя никаких противоречий.
В этой статье я буду отталкиваться от концепции стандартов WebAuthn/CTAP и покажу экспериментальную альтернативу.

Но для понимания контекста давайте условимся, что:

  • WebAuthn (Web Authentication API) — это браузерный API, через который веб‑приложение запускает регистрацию/аутентификацию (например, через passkeys).

  • CTAP (Client‑to‑Authenticator Protocol) — протокол, описывающий, как браузер/ОС общается с аутентификатором (ключ безопасности, смартфон, Windows Hello и т. п.).

Далее для упрощения я буду называть эту связку единым набором стандартов FIDO2, так как он их объединяет.

Преимущества FIDO2 понятны, но есть ряд кейсов, которые требуют отдельного внимания:

  • Зависимость от устройства. Выход из строя аппаратного ключа или внезапные ограничения вендора, которые сегодня становятся уже не новостью, усложняют процесс восстановления доступа.

  • Навыки менеджмента ключей. Да, существует облачное резервное копирование, экспорт в KeePassXC и ряд других способов в зависимости от технологии, но со стороны обычного пользователя это не совсем упрощает процесс, а местами усложняет его. Сюда относится также перенос ключа на другое устройство.

  • Концепция «личность - устройство - аккаунт». Если у пользователя несколько аккаунтов (личный/рабочий), то на каждый аккаунт нужен свой ключ.

  • И, наконец, философская потребность пользователя самостоятельно менеджерить свои данные.

Я решил поэкспериментировать над этим и сфокусировался на следующих вопросах, а что, если:

  • сделать меньшую связность сайта с аппаратными ключами?

  • перенести менеджмент аутентификации ближе к web‑сервису, сохраняя допустимый уровень безопасности?

  • оставить за пользователем и web‑сервисом право гибко распоряжаться этим процессом и при этом не сильно усложнять UX?

Но тогда концепция «личность - устройство - аккаунт» утрачивается? Да, но пользователь и так может создать несколько фейковых аккаунтов, накупив аппаратных ключей или при должных навыках сымитировать ключ. Где-то процесс проще, где-то сложнее, и он все же возможен. И это уже не вопросы аутентификации, а вопросы авторизации — с разграничением прав и подтверждением личности.

Результатом эксперимента стала экосистема беспарольной аутентификации на основе браузерного расширения — SeedKey. Вы можете посмотреть, как это выглядит уже сейчас на демо странице проекта.

Как это работает

  1. Пользователь создает Identity (мастер‑ключ).

  2. Сайт запрашивает у расширения публичный ключ для текущего домена.

  3. Расширение из мастер‑ключа детерминированно выводит пару (public/private) для домена.

  4. Фронтенд отправляет publicKey на бэкенд.

  5. Бэкенд формирует Challenge (специальный объект для подписи) и отдает его клиенту.

  6. Клиент передает challenge в расширение.

  7. Расширение подписывает Challenge и возвращает подпись.

  8. Бэкенд проверяет подпись тем publicKey, который уже знает, и завершает аутентификацию (выдает токены).

Схематично это выглядит следующим образом:

Архитектура протокола SeedKey
Архитектура протокола SeedKey

Детали реализации

Давайте разберем компоненты, из которых состоит наша система:

  • seedkey-browser-extension — само расширение. На него возложена основная часть криптографических операций и хранение Identity.

  • seedkey-client-sdk — клиентский SDK с хелперами и API для общения с расширением и бэкендом.

  • seedkey-server-sdk — серверный SDK (Node.js) с хелперами для формирования/проверки Challenge и контрактами API.

  • seedkey-auth-service — self‑hosted сервис с готовыми REST API эндпоинтами и логикой аутентификации на базе seedkey-server-sdk.

  • seedkey-db-migrations — Liquibase миграции для PostgreSQL, поддерживающие структуру сущностей, необходимых системе.

  • seedkey-auth-service-helm-chart — Helm‑чарт для деплоя миграций и auth-service.

Для комплексного понимания всей системы мы остановимся на каждом из них.

Примечание. Во всех репозиториях помимо базового README есть подробная документация на русском языке в папке doc/ru.

Браузерное расширение (seedkey-browser-extension)

"SeedKey. Ввод seed-фразы"
Создание Identity

GitHub: https://github.com/mbessarab/seedkey-browser-extension

Первое, что нужно сделать пользователю, — создать его Identity. Мне нравится интуитивно понятная концепция seed‑фразы (BIP39): её легко записать/запомнить, и именно она ляжет в основу формирования приватного мастер‑ключа.
С помощью алгоритма PBKDF2‑SHA512 выводим приватный ключ и храним в localStorage.

// Функция формирования приватного мастер ключа. 
async function deriveMasterKey(seedPhrase: string): Promise<Uint8Array> {
  const encoder = new TextEncoder();
  const normalizedSeed = seedPhrase.normalize('NFKD');

  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    encoder.encode(normalizedSeed),
    'PBKDF2',
    false,
    ['deriveBits']
  );

  const masterKeyBits = await crypto.subtle.deriveBits(
    {
      name: 'PBKDF2',
      salt: encoder.encode('salt'),
      iterations: 210_000,
      hash: 'SHA-512',
    },
    keyMaterial,
    256
  );

  return new Uint8Array(masterKeyBits);
}

В текущей MVP версии мастер ключ хранится в незашифрованном виде в localStorage и служит для ознакомления с общей концепцией протокола.

Несмотря на то, что сайт не может читать изолированный контекст расширения (Background / Service Worker), на котором происходят все криптооперации, хранить приватный мастер‑ключ «в голом виде» — не очень здорово.
Поэтому в целевой архитектуре шифровать мастер‑ключ нужно некоторым DeviceKey, который можно получить разными способами:

  • Passkeys / любая реализация FIDO2;

  • YubiKey / аппаратный ключ;

  • Windows Hello;

  • или мастер‑пароль.

Веб сервису не нужно заботиться о том, какая реализация шифрования мастер‑ключа выбрана у пользователя: Identity пользователя и, соответственно, все пары ключей на домены всегда будут согласованными.

Целевая архитектура протокола SeedKey
Целевая архитектура протокола SeedKey

Клиентский SDK (seedkey-client-sdk)

GitHub: https://github.com/mbessarab/seedkey-client-sdk

В первую очередь SDK обеспечивает коммуникацию с расширением через ContentScript и отправляет события с различными actions/payload:

type SeedKeyAction =
  | 'check_available'
  | 'is_initialized'
  | 'get_public_key'
  | 'sign_challenge'
  | 'sign_message';

function sendToExtension(
  action: SeedKeyAction,
  payload: SeedKeyRequest
) {
  const request = {
    type: 'SEEDKEY_REQUEST',
    action,
    payload
  };

  const event = new CustomEvent('seedkey:v1:request', {
    detail: request
  });

  document.dispatchEvent(event);
}

API расширения версионируется для обратной совместимости с предыдущими версиями SDK — seedkey:v1:request / seedkey:v1:response.

Для гибкости вы можете самостоятельно реализовать любой этап, используя low-level API, но также можете делегировать это SDK, используя high-level API. При правильной настройке бэкенд‑сервиса ваша реализация может заключаться буквально в несколько строк:

import { getSeedKey, saveTokens, SeedKeyError } from '@seedkey/sdk-client';

// init
const sdk = getSeedKey({
  backendUrl: 'https://api.seedkey-server'
});

// проверка наличия расширения
const available = await sdk.isAvailable();
// Проверка инициализации расширения (создана Identity)
const initialized = await sdk.isInitialized();

if (!initialized && !available) {
  return;
}

// аутентификация, или регистрация, если новый публичный ключ
try {
  const result = await sdk.auth();
  console.log('Access Token:', result.token.accessToken);

  // сохранение токенов в localStorage
  saveTokens(result.token, result.user.id);
} catch (error) {
  if (error instanceof SeedKeyError) {
    console.error('Code:', error.code, 'Message:', error.message);
  }
}

Серверный SDK (seedkey-server-sdk)

GitHub: https://github.com/mbessarab/seedkey-server-sdk

Серверная библиотека — это framework‑agnostic реализация протокола. Она обеспечивает Request/Response‑контракт, формирование Challenge, проверку подписи и дает возможность кастомизировать процесс под свою бизнес‑область.

Внутри библиотеки есть адаптеры для persistence слоя, которые вам необходимо имплементировать:

interface UserStore {
  findById(id: string): Promise<User | null>;
  findByPublicKey(publicKey: string): Promise<User | null>;
  create(publicKey: string, metadata?: UserMetadata): Promise<User>;
  updateLastLogin(userId: string, publicKey: string): Promise<void>;
  publicKeyExists(publicKey: string): Promise<boolean>;
  replacePublicKey?(userId: string, newPublicKey: string, metadata?: KeyMetadata): Promise<PublicKeyInfo | null>;
}

interface ChallengeStore {
  save(challenge: StoredChallenge): Promise<void>;
  findById(id: string): Promise<StoredChallenge | null>;
  markAsUsed(id: string): Promise<boolean>;
  isNonceUsed(nonce: string): Promise<boolean>;
  delete?(id: string): Promise<void>;
}

interface SessionStore {
  create(userId: string, publicKeyId: string, expiresInSeconds?: number): Promise<Session>;
  findById(id: string): Promise<Session | null>;
  invalidate(id: string): Promise<boolean>;
  invalidateAllForUser(userId: string): Promise<void>;
  isValid(id: string): Promise<boolean>;
}

type TokenGenerator = (
  userId: string,
  publicKeyId: string,
  sessionId: string
) => Promise<TokenPair>;

Заинжектить в сервис AuthService и пользоваться API для всего флоу аутентификации:

const authService = new AuthService({
  config,
  users: userStore,
  challenges: challengeStore,
  sessions: sessionStore,
  tokenGenerator,
});

authService.createChallenge(request)
authService.register(request)
authService.verify(request)

Расширение подписывает Challenge по алгоритму Ed25519, и backend проверяет подпись тем публичным ключом, который у него уже есть.
Пример подписанного Challenge:

{
  "publicKey": "JGDwSln8/pcQoRFhxVi9VX8bPpjCicoCfzzRyhEoLG8=",
  "challenge": {
    "nonce": "6EzG5ebclmao8IziuboIejy5HP+eFpdDis7BuwoQqRw=",
    "timestamp": 1768037110034,
    "domain": "seedkey.mbessarab.ru",
    "action": "register",
    "expiresAt": 1768037410034
  },
  "signature": "BZ+b4qbPPPMjuqW5IeFVTS4lqJSOPGS/lr3ANQGKZ23OHDoonW74cie+KtJybLzhpUOGl1PaSTvGYGzo0/cFAw==",
  "metadata": {
    "deviceName": "Firefox on Windows",
    "sdkVersion": "0.0.1"
  }
}

Self‑Hosted сервис (seedkey-auth-service)

GitHub: https://github.com/mbessarab/seedkey-auth-service

Это сервис, который упаковывает seedkey-server-sdk и полностью реализует серверную часть протокола, предоставляя готовый REST API‑контракт для клиентского SDK.

Не запрещено, но и не рекомендуется ходить в БД экосистемы напрямую. Оставьте это для auth-service и используйте его эндпоинты для получения информации о пользователе.

Liquibase‑миграции для PostgreSQL (seedkey-db-migrations)

GitHub: https://github.com/mbessarab/seedkey-db-migrations

Миграции создают структуру сущностей в PostgreSQL, необходимую протоколу SeedKey. Вам не нужно вручную выдумывать таблицы/связи — вместо этого просто запустите Docker‑контейнер.

Helm‑чарт (seedkey-auth-service-helm-chart)

GitHub: https://github.com/mbessarab/seedkey-auth-service-helm-chart

И, наконец, Helm‑чарт в вашем кластере сам создаст Namespace, Service и прочие необходимые компоненты, выполнит Job с миграциями и развернет Deployment с auth-service.

Безопасность

Как уже упоминалось ранее, одна из задач SeedKey — сохранить допустимый уровень безопасности, поэтому реализация протокола обеспечивает:

  • защиту от анти-фишинга на основе деривации пары ключей (public/private) для каждого домена, это предотвращает возможность подделывать подпись ключом от другого домена;

  • использование алгоритма Ed25519 для подписи;

  • rate limiting от злоупотребления API подписи в расширении;

  • реализацию классических refresh/access JWT токенов;

  • проверку TTL, домена, использованного nonce (на стороне сервера).

Заключение

SeedKey — это экспериментальная экосистема, которая не ставит целью заменить существующие стандарты FIDO2, а наоборот — задуматься об их расширении. В частности:

  • упростить UX вокруг криптографической аутентификации;

  • дать промежуточный слой управления ключами с возможностью использования FIDO2;

  • возможно, подготовить пользователя к переходу на нативные решения, такие, как passkeys;

  • или занять собственную нишу — покажет время.

И, если у вас возникнут какие-либо вопросы по реализации или внедрению протокола в вашу бизнес‑логику, напишите мне по любому из контактов, и мы вместе разберем ваш кейс.

Чтобы быть на связи, подписывайтесь мой тг канал: https://t.me/MBessarab_dev

Также мне важно собрать ваши предложения, идеи и ваше видение такой концепции, поэтому буду рад любым комментариям :)

Комментарии (3)


  1. saipr
    13.01.2026 07:10

    Также мне важно собрать ваши предложения, идеи

    А что не так с PKCS11 и NSS (Network Security Services)? Отлаженный механизм.


    1. m_bessarab Автор
      13.01.2026 07:10

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


      1. saipr
        13.01.2026 07:10

        "В обывательском смысле" использование механизма PKCS11 проще. Всё что нужно сделать человеку это сгенерировать закрытый ключ и получить сертификат в аккредитованном удостоверяющем центре...