Экосистема SeedKey. Или как улучшить беспарольную аутентификацию.
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. Вы можете посмотреть, как это выглядит уже сейчас на демо странице проекта.
Как это работает
- Пользователь создает Identity (мастер‑ключ).
- Сайт запрашивает у расширения публичный ключ для текущего домена.
- Расширение из мастер‑ключа детерминированно выводит пару (public/private) для домена.
- Фронтенд отправляет publicKey на бэкенд.
- Бэкенд формирует Challenge (специальный объект для подписи) и отдает его клиенту.
- Клиент передает challenge в расширение.
- Расширение подписывает Challenge и возвращает подпись.
- Бэкенд проверяет подпись тем publicKey, который уже знает, и завершает аутентификацию (выдает токены).
Схематично это выглядит следующим образом:

Детали реализации
Давайте разберем компоненты, из которых состоит наша система:
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)
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 пользователя и, соответственно, все пары ключей на домены всегда будут согласованными.

Клиентский 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;
- или занять собственную нишу — покажет время.
Если у вас возникнут какие-либо вопросы по реализации или внедрению протокола в вашу бизнес‑логику, напишите мне по любому из контактов, и мы вместе разберем ваш кейс.
Мой Telegram: @maks_bessarab
Email: maks@besssarab.ru


