Перейти к содержанию
Me2eM

Аттестационные цепочки: делегирование доверия без сервера

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

M

Me2eM Organization

6 мин. чтения

Из seed-фразы детерминированно выводится дерево ключей: Identity → Handle → SubHandle — полный разбор в статье «Me2eM: идентичность из seed-фразы». Дерево даёт каждому — пользователю, устройству, агенту — собственную пару ключей. Но пара ключей сама по себе отвечает только на вопрос «кто подписал». Она не отвечает на вопросы, которые превращают подпись в авторизацию:

  • Кто разрешил этому ключу существовать?
  • Что именно ему разрешено — и для кого?
  • Когда его полномочия заканчиваются?
  • Как это отозвать, если что-то пошло не так?

На все четыре вопроса в Me2eM отвечает один механизм — аттестация. Это родительская подпись, превращающая голый ключ в удостоверение с границами. И вся система делегирования доверия строится без единой записи в серверной базе.

Аттестация — это предложение, а не запись

Классическая система авторизации хранит разрешения в базе: user X has role Y on resource Z. Права существуют, пока жива запись, и зависят от доступности базы при каждой проверке.

Me2eM переворачивает модель. Аттестация — это подписанное родителем утверждение:

«публичный ключ K принадлежит имени N и действует в границах гранта G до момента T».

Четыре факта, одна подпись, ноль записей на сервере. Кто угодно может проверить утверждение — имея только публичный ключ родителя. Никто не может его подделать, расширить или продлить.

flowchart LR
    P["Родитель"] -->|"подписывает"| A["Аттестация:<br/>ключ K = имя N<br/>в границах гранта G"]
    A -->|"предъявляет"| V["Верификатор"]
    PK["Публичный ключ родителя"] -->|"проверка подписи"| V

Аттестация — это публичный артефакт: подписанное свидетельство, которое носитель предъявляет верификатору. Она не секрет — в ней нет ничего секретного: публичный ключ, имя, границы. Секрет (приватный ключ субъекта) остаётся у носителя и никогда не покидает его.

Из чего состоит грант

Грант — это границы полномочий. Четыре измерения:

interface AttestationGrant {
  audiences?: string[];        // кому субъект может предъявлять сессии
  scopes: string[];            // что именно он может делать
  maxSessionTtl: number;       // максимальный срок одной сессии
  subNamePatterns?: string[];  // имена субхэндлов, которые он может создавать
}

Каждое измерение работает по принципу вычитания: то, что не в гранте, — запрещено. Грант ['charge:start'] означает «может только начать зарядку» — а не «может всё, кроме остальных действий». Пустой массив ([]) означает «запрещено всё» — fail-closed по умолчанию.

Дополнительная сила — вложенность. Субхэндл, выпущенный хэндлом, может иметь только подмножество родительских прав:

Родитель: scopes = [charge:start, charge:stop, charge:status], maxTtl = 7200
    ↓
Ребёнок:  scopes = [charge:start], maxTtl = 1800
          → валидно: подмножество, меньше TTL

Ребёнок:  scopes = [charge:start, billing:admin], maxTtl = 7200
          → ОТКЛОНЕНО верификатором: billing:admin вне родительского гранта

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

Два режима верификации

Один и тот же токен сессии можно проверить двумя способами — они отвечают на один вопрос, но для разной аудитории.

flowchart LR
    T["Session Token"] -->|"Mode 1<br/>verifyStateless"| R1["Верификатор владеет<br/>приватным корнем"]
    T -->|"Mode 2<br/>verifyAttested"| R2["Верификатор имеет<br/>только публичный корень"]
    R1 --> O1["Ограничения субхэндла:<br/>только на создании"]
    R2 --> O2["Ограничения субхэндла:<br/>принуждаются на верификации"]

Mode 1 — прямая верификация. Верификатор пересчитывает публичный ключ подписанта из пути в токене, используя экземпляр Identity. Для этого нужен приватный корень — поэтому Mode 1 доступен только владельцу или его доверенной инфраструктуре.

Mode 2 — верификация с аттестационной цепочкой. Верификатор проверяет подпись сессии против ключа, связанного родительской аттестацией, и каждую аттестацию — против подписи её родителя, восходя к публичному корневому ключу. Каждое звено цепи несёт подписанный грант, поэтому верификатор принуждает гранты на верификации: scopes сессии должны укладываться в грант, TTL-потолки и аудитории соблюдаются, дочерние гранты вкладываются в родительские.

Как выбрать. Вопрос один: готов ли верификатор держать приватный корень? Собственная инфраструктура под полным контролем — Mode 1: цепочка не нужна, всё проверяется пересчётом. Любая внешняя сторона — партнёры, клиенты, чужие сервисы — Mode 2: приватный корень не покидает владельца, а гранты принуждаются математически на каждой проверке. Ошибка в сторону Mode 1 дорога: ограничения принуждаются лишь на создании сессии, а отзыва ветки нет вовсе — это не экспромт, который чинится позже.

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

Покажем весь механизм на корпоративном сценарии. Это учебная ресторация из USE_CASES.md — там же полные коды всех сценариев.

Иерархия

Identity: Корпорация (seed директора — в сейфе)
  └─ Handle: department-engineering  ◀── A: аттестация от корня
       │    грант: message:*, channel:engineering, maxTtl: 8ч
       │    может создавать: worker-*, contractor-*
       ├─ SubHandle: worker-alice    ◀── B: аттестация от HR
       └─ SubHandle: contractor-x    ◀── B с TTL две недели

Настройка (корень — один раз)

const grantA = {
  audiences: ['corp-messenger.internal', 'jira.internal'],
  scopes: ['message:send', 'message:receive', 'channel:engineering'],
  maxSessionTtl: 28800,   // сессии до 8 часов — один рабочий день
  subNamePatterns: ['worker-*', 'contractor-*'],
};
const A = await corpIdentity.attestHandle('department-engineering', grantA,
  { ttlSeconds: 365 * 24 * 3600 });   // аттестация живёт год
// HR-система получает deptHandle + A. Корень спит на год:
// любые новые сотрудники, роли и TTL ≤ 8ч покрываются без корня.

HR-система получает deptHandle + аттестацию A. Корень с этого момента спит. Никакой ежедневной операции с корнем нет — аттестация A действует год.

Найм (HR — автономно)

HR не имеет корня. Он работает локально:

const { worker, B } = await hireEmployee('worker-diana', 'Diana Prince', 'Manager');

Внутри hireEmployee — две операции: деривация субхэндла + аттестация субхэндла, подписанная ключом отдела. Никакой сети, никакого корня, никакого ожидания.

И — принципиально — даже скомпрометированная HR-машина не может эскалировать: channel:management вне grantA.scopes, поэтому такая дочерняя аттестация отклоняется каждым верификатором (SCOPE_EXCEEDED на уровне корня). Права выше гранта отдела требуют отдельного хэндла, аттестованного корнем напрямую.

Верификация двумя сервисами

Мессенджер и Jira проверяют независимо, с одним публичным корнем:

// corp-messenger.internal:
const s = await Session.verifyAttested(
  token, CORP_ROOT_PUB, [A.token, B.token],
  'corp-messenger.internal', revocationChecker,
);
// jira.internal — тот же вызов, другая аудитория

Оба сервиса видят: чей это сотрудник, какие у него права, не отозвана ли его аттестация. Им не нужна БД отделов — вся информация в подписанной цепочке.

Увольнение — правильный способ

Одна операция — отозвать аттестацию сотрудника:

await revocationService.revokeAttestation(B.jti);

Все текущие и будущие сессии worker-alice отклоняются — независимо от TTL. В обоих сервисах одновременно. Из одного хранилища.

Mode 1 не имеет протокольного отзыва хэндлов — интерфейс RevocationChecker принимает только session jti. Обходной путь (конвенция именования jti с парсингом) существует, но хрупок. Mode 2 делает отзыв ветки первоклассной операцией.

Подрядчики — самоистекающий доступ

const B = await deptHandle.attestSubHandle('contractor-external-xyz', {
  audiences: ['corp-messenger.internal'],
  scopes: ['message:send', 'message:receive', 'channel:project-alpha'],
  maxSessionTtl: 28800,
}, { ttlSeconds: 14 * 24 * 3600 });  // через две недели доступ исчезнет сам

HR не пишет в календарь «удалить доступ подрядчику 14 октября». Аттестация истекает сама — у всех верификаторов одновременно.

Отзыв: одна операция, вся ветка

Каждый уровень цепочки имеет свой jti — и отзыв любого уровня работает каскадно:

Отзыв корневой аттестации станции (A.jti)
  → станция мертва целиком: все коннекторы, все сессии

Отзыв аттестации коннектора (B.jti)
  → мёртв только коннектор; станция и остальные — живы

Верификатор проверяет jti на каждом уровне цепочки — от корня вниз. Ревокация любой аттестации в цепочке делает всю цепь недействительной. Это позволяет отзывать с нужной гранулярностью: всю станцию при компрометации, конкретный разъём при поломке.

Что аттестации дают — и чего не дают

Честная таблица:

ГарантируетсяНе гарантируется
Сторонний верификатор без приватного корняЗащита от злоупотребления правами внутри гранта
Принуждение грантов офлайн (не нужен сервер)Обнаружимость повторной аттестации (нужен transparency-лог)
Мгновенный отзыв ветки (jti + хранилище)Отзыв «уже знавшего» — знающий материалы сохраняет их до истечения
Вложенность: ребёнок ⊆ родитель (математически)Ревокация корня без инфраструктуры распределения

Аттестации не защищают от компрометации корня — корень и так порождает все ключи. Они защищают всё, что ниже корня: от несанкционированного расширения, от незаметной подмены, от увольнения без возврата доступа.

Связь с деревом ключей

Аттестация не создаёт ключей. Она связывает существующий (детерминированно выведенный) ключ с именем и грантом. Возвращаясь к дереву из статьи о деривации:

Seed → Identity → Handle → SubHandle     ← дерево (детерминизм)
              ↓
        attestHandle / attestSubHandle   ← свидетельство (доверие)
              ↓
        Session (подпись)                ← использование

Дерево отвечает «какой ключ у этого имени». Аттестация отвечает «кто разрешил этот ключ и в каких границах». Сессия отвечает «что этот ключ делает сейчас». Три слоя — три вопроса — одна согласованная система.

Что читать дальше

Поделиться:

Похожие статьи

Продажа доступа: подписанная квитанция как товар в Me2eM

Сценарий Mode 2 в чистом виде: дрон продаёт 30 минут съёмки, спутник — окно tasking'а. Покупатель верифицирует покупку публичным корнем, оператор не ведёт учёт, чек не подделать и не продлить.

M Me2eM Organization
6 мин. чтения
#attestations #iot #security

Пост-мортем: эволюция @me2em/core — от библиотеки идентичности к протоколу с аттестациями

Честный разбор семи дизайн-решений: детерминизм вместо хранения, stateless-сессии, два режима верификации, аттестации вместо ролей, каноничность имён, свежий salt для кэша и запрет zero-fallback — с альтернативами и тем, что мы бы сделали иначе.

M Me2eM Organization
7 мин. чтения
#architecture #cryptography #security

Почему мы не храним ваш seed — и почему это фича, а не недоработка

Seed-фраза — единственный носитель вашей идентичности. Мы не храним её. Почему это осознанное решение, какую цену вы за него платите, и почему эта цена честнее альтернативы.

M Me2eM Organization
7 мин. чтения
#security #identity #architecture

Готовы начать?

Установите пакет, пройдите quick start и за считанные минуты запустите свой первый сценарий.