clientassertion

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

federated.go — федеративная полоса приёма: утверждение подписал ВНЕШНИЙ издатель, а перечень доверенных издателей ведём мы (задача kacho#1124, RFC 7523 §2.1 — вид выдачи, не аутентификация клиента).

Чем эта полоса отличается от полосы аутентификации клиента

Там издатель и субъект обязаны совпадать и оба назвать НАШУ строку, а ключ берётся из строки клиента. Здесь издатель — посторонний, субъект — его собственный, ключ принадлежит ему, и единственное, что есть у нас, — ЗАПИСЬ О ДОВЕРИИ. Промах поэтому даёт не «приняли своё за чужое», а токен платформы предъявителю, которого мы не заводили вовсе.

Три признака, отделяющих утверждение от нашего же токена доступа

На полосе клиента первый признак — объявленный нами тип в заголовке. Здесь его нет и быть не может: тип ставит производитель, а производитель тут посторонний, и требование типа отвергало бы каждого обычного издателя. Признаки, которые остаются и которых достаточно:

  1. **ключ.** Подпись проверяется ключом ИЗДАТЕЛЯ из записи доверия. Наш токен доступа подписан нашим ключом, которого в этой таблице нет;
  2. **наш идентификатор издателя в этой таблице ЗАПРЕЩЁН** — иначе первый признак снимался бы одной административной записью, и наш собственный токен доступа стал бы приниматься как чужое утверждение;
  3. **адресат.** Утверждение адресовано нашему издателю; токен доступа адресован ресурсу.

Package clientassertion — принимающая сторона аутентификации клиента подписанным утверждением (RFC 7523 §2.2, задача kacho#898).

Чем эта сторона отличается от предъявляющей

Утверждение этого вида дерево уже подписывает и предъявляет; вся та работа стоит на стороне КЛИЕНТА. Здесь заводится сторона СЕРВЕРА, и цена ошибки у неё противоположна: у предъявителя неверное утверждение даёт отказ, видимый сразу, — у принимающего неверная проверка даёт ПРИНЯТОЕ ЧУЖОЕ утверждение, не видимое никогда. Успешная аутентификация выглядит одинаково независимо от того, что именно она проверила.

Отсюда правило работ, которому подчинён весь порядок ниже: на каждой развилке, где неясно, отвергать или пропускать, выбирается ОТВЕРГАТЬ.

Почему разбор идёт над той же библиотекой, а не своей

Стеков разбора JOSE в дереве и без нас несколько, и решения, принятые здесь (закрытый словарь алгоритмов, отказ на подмену алгоритма, дублирующееся имя члена заголовка, член, помеченный обязательным к пониманию), доехали бы только до одного из них. Второй библиотеки «ради удобства утверждения» не заводится: проверки, которых библиотека не делает, добавляются шагом НАД ней.

Что здесь делается ДО библиотеки и почему именно это

Три вещи библиотека не делает by construction, и каждая из них — отказ:

  1. **имя члена заголовка дважды.** Разбор объектной записи, встретив имя дважды, берёт одно из значений и не возражает. Два значения одного члена означают, что проверяющий и подписант МОГЛИ ПРОЧИТАТЬ РАЗНОЕ, а значит проверенной оказалась не та половина (RFC 7515 §5.2);
  2. **член, помеченный обязательным к пониманию.** Пометка означает «без понимания этого члена подпись нельзя считать проверенной». Принять — значит объявить проверенным то, чего не поняли;
  3. **встроенный ключевой материал.** Ключ выбирается ТОЛЬКО из реестра; перечень членов, из которых он выбираться не может, объявлен в pkg/tokenpolicy одним местом.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func OurSide added in v0.5.0

func OurSide(o Outcome) bool

OurSide — решил ли исход отказа НЕ предъявитель, а наша сторона: реестр или хранилище однократности не ответили, вопрос об отсечке не задался, выпуск не состоялся. Чинит такой отказ оператор, а не вызывающий.

Одно место классификации на всех читателей: счёт отказов против источника (П3) не засчитывает эти исходы, журнал пишет их уровнем ошибки. Две копии перечня разошлись бы молча — и в сторону «наш сбой засчитан клиенту». Перечень закрыт: исход, заведённый в словаре позже, остаётся исходом предъявителя, пока его не назовут здесь.

func PresenterResponseFor

func PresenterResponseFor(Outcome) string

PresenterResponseFor — опознавательное слово стандартной формы.

Одно и то же для ВСЕХ исходов отказа аутентификации. Функция, а не константа: вызывающему нужен именно ответ, а не строка, и подмена одного другим на каком-то из путей была бы ровно тем расхождением, которого мы избегаем.

Types

type ClientResolver

type ClientResolver interface {
	ResolveAssertionClient(ctx context.Context, clientID string) (domain.AssertionClient, error)
}

ClientResolver — порт реестра, СПОСОБНОГО к утверждению.

«Способного» здесь несущее слово: разрешение идёт только по таблицам, несущим ключевой материал. Вид клиента, у которого нет способа доказать владение ключом by construction, не резолвится ни во что, и отказ наступает на том же шаге, что для несуществующего.

type ExchangePace added in v0.5.0

type ExchangePace interface {
	Reserve(key string) (refund func(), retryAfter time.Duration, ok bool)
}

ExchangePace — порт темпа «обменов в секунду на идентификатор клиента» (kaname#315).

Бронь, а не вопрос: Reserve САМ списывает обмен и сам отвечает, остался ли он, — иначе параллельные предъявления проходили бы сверх темпа. Проверяющий возвращает бронь (refund), когда предъявление отвергнуто: темп клиента тратят только принятые предъявления, и назвавший чужой идентификатор без его ключа чужой темп не расходует.

type Outcome

type Outcome string

Outcome — исход проверки. Закрытый словарь: у КАЖДОГО значения свой счётчик, иначе мёртвый контроль невидим — проверка, не отказавшая ни разу за всё время жизни, неотличима от проверки, которая работает и просто не встречала нарушителя.

const (
	OutcomeAccepted               Outcome = "accepted"
	OutcomeAssertionTypeMismatch  Outcome = "assertion-type-mismatch"
	OutcomeMalformedSerialization Outcome = "malformed-serialization"
	OutcomeDuplicateHeaderMember  Outcome = "duplicate-header-member"
	OutcomeUnsupportedCritical    Outcome = "unsupported-critical-member"
	// #nosec G101 -- машинный ПРИЗНАК ПРИЧИНЫ ОТКАЗА, уезжающий в журнал и в
	// счётчик, а не секрет: значения этого словаря наружу не выходят вовсе,
	// потому что ответ предъявителю у всех исходов один.
	OutcomeTokenTypeMismatch   Outcome = "token-type-mismatch"
	OutcomeAlgorithmNotAllowed Outcome = "algorithm-not-allowed"
	OutcomeAlgorithmMismatch   Outcome = "algorithm-mismatch"
	OutcomeIdentityMismatch    Outcome = "identity-mismatch"
	OutcomeClientUnknown       Outcome = "client-unknown"
	OutcomeClientCannotAssert  Outcome = "client-cannot-assert"
	// OutcomeIssuerUntrusted — пара (издатель, субъект) не резолвится в нашем
	// перечне доверенных издателей. Один исход на все состояния «мы за это не
	// ручаемся»: различимые дали бы предъявителю оракул состава перечня.
	OutcomeIssuerUntrusted Outcome = "issuer-untrusted"
	// OutcomeTrustExpired — запись доверия найдена, её срок истёк. Отдельный
	// счётчик от предыдущего: «доверия не было» и «доверие кончилось» суть
	// разные события эксплуатации, и слитые в один счётчик они делают
	// истечение невидимым.
	OutcomeTrustExpired           Outcome = "issuer-trust-expired"
	OutcomeSignatureMismatch      Outcome = "signature-mismatch"
	OutcomeAudienceMismatch       Outcome = "audience-mismatch"
	OutcomeExpiryMissing          Outcome = "expiry-missing"
	OutcomeIssuedAtMissing        Outcome = "issued-at-missing"
	OutcomeIssuedAtInFuture       Outcome = "issued-at-in-future"
	OutcomeLifetimeAboveCeiling   Outcome = "lifetime-above-ceiling"
	OutcomeExpired                Outcome = "expired"
	OutcomeNotYetValid            Outcome = "not-yet-valid"
	OutcomeAssertionIDMissing     Outcome = "assertion-id-missing"
	OutcomeReplayed               Outcome = "replayed"
	OutcomeRegistryUnavailable    Outcome = "registry-unavailable"
	OutcomeReplayStoreUnavailable Outcome = "replay-store-unavailable"
	// OutcomeClientPaceExceeded — темп обменов ЗАЯВЛЕННОГО идентификатора
	// исчерпан (kaname#315). Решается до реестра и до перечня доверенных
	// издателей, по идентификатору из предъявления, и потому о записи реестра
	// не говорит ничего: темп тратят только принятые предъявления. Наружу —
	// свой код «повторите позже», а не «неверный клиент»: единый тон отказов
	// аутентификации держится для исходов, наступающих ПОСЛЕ обращения к
	// реестру, а этот наступает до него.
	OutcomeClientPaceExceeded Outcome = "client-pace-exceeded"

	// OutcomeMethodNotAllowed — обращение методом, отличным от объявленного.
	OutcomeMethodNotAllowed Outcome = "method-not-allowed"
	// OutcomeBodyAboveCeiling — объявленная длина либо само тело сверх потолка.
	OutcomeBodyAboveCeiling Outcome = "body-above-ceiling"
	// OutcomeMalformedRequest — тело не разбирается как форма запроса.
	OutcomeMalformedRequest Outcome = "malformed-request"
	// OutcomeMultipleAssertions — параметр с утверждением встретился дважды.
	// «Ровно одно утверждение» есть требование к НАШЕМУ разбору, а не описание
	// намерения клиента.
	OutcomeMultipleAssertions Outcome = "multiple-assertions"
	// OutcomeUnsupportedGrantType — вид выдачи вне закрытого перечня.
	OutcomeUnsupportedGrantType Outcome = "unsupported-grant-type"
	// OutcomeInFlightCeilingReached — потолок одновременных обменов эндпоинта
	// занят (kaname#315). Решается до проверяющего: потолок бережёт именно
	// проверку — обращение к реестру и сверку подписи.
	OutcomeInFlightCeilingReached Outcome = "in-flight-ceiling-reached"
	// OutcomeSourceFailuresExceeded — источник исчерпал окно отказов
	// доказательства клиента (ось П3, kaname#315). Решается эндпоинтом до
	// потолка и до проверяющего, по источнику (правило адреса Р7), и о клиенте
	// не говорит ничего.
	OutcomeSourceFailuresExceeded Outcome = "source-failures-exceeded"
	// OutcomeAudienceNotAllowed — запрошенный адресат вне объявленного
	// конфигурацией перечня адресатов платформы.
	OutcomeAudienceNotAllowed Outcome = "requested-audience-not-allowed"
	// OutcomeClientExpired — срок клиента истёк.
	OutcomeClientExpired Outcome = "client-expired"
	// OutcomeOwnerNotActive — владелец клиента не в состоянии ACTIVE.
	OutcomeOwnerNotActive Outcome = "owner-not-active"
	// OutcomeOwnerRevoked — ключ человека выдан не позже записанной отсечки
	// отзыва-всех его владельца — любой, каким бы действием, выводящим
	// человека отовсюду, она ни была записана. Отдельный счётчик от «владелец
	// не активен»:
	// человек, вышедший отовсюду, остаётся активным, и слитые в один счётчик
	// эти события неотличимы для того, кто разбирает отказы.
	OutcomeOwnerRevoked Outcome = "owner-revoked"
	// OutcomeOwnerUnverified — владелец ключа — человек с неподтверждённым
	// адресом (kaname#456, Р5). Своя клетка рядом с «вышел отовсюду»: отсечку
	// и неподтверждённость снимают разные действия (войти заново против
	// подтвердить адрес), и слитый счётчик не сказал бы, что чинить.
	OutcomeOwnerUnverified Outcome = "owner-unverified"
	// OutcomeRevocationCheckFailed — отсечку отзыва-всех владельца спросить не
	// удалось. Отказ НАШЕЙ стороны, а не предъявителя: слитый с «выпуск не
	// состоялся» он прятал бы недоступность хранилища отсечек среди сбоев
	// подписанта.
	OutcomeRevocationCheckFailed Outcome = "revocation-check-failed"
	// OutcomeIssuanceFailed — аутентификация прошла, выпуск не состоялся.
	OutcomeIssuanceFailed Outcome = "issuance-failed"
)

func Outcomes

func Outcomes() []Outcome

Outcomes возвращает закрытый словарь исходов целиком.

ВЫВОДИТСЯ отсюда всяким, кому нужен перечень: набор счётчиков, проба переписи, текст отказа. Вторая копия словаря разошлась бы молча, и разошлась бы она в сторону «исход без счётчика» — то есть в сторону невидимости.

type Policy

type Policy struct {
	// ExpectedAudience — идентификатор НАШЕГО издателя. Единственная
	// принимаемая форма адресата утверждения.
	ExpectedAudience string
	// MaxLifetime — потолок разницы «срок − момент выпуска» на полосе клиента.
	MaxLifetime time.Duration
	// MaxFederatedLifetime — тот же потолок на федеративной полосе.
	//
	// Отдельное поле, а не то же число: утверждение полосы клиента выписывает
	// НАШ клиент специально для нас и вправе дать ему минуты, а внешний
	// издатель выпускает своей нагрузке токен со своим сроком и о нашем
	// потолке не знает. Одно число на две полосы означало бы либо отказ
	// каждому внешнему издателю, либо месячное окно у собственного клиента.
	MaxFederatedLifetime time.Duration
	// ClockSkew — допуск расхождения часов. Действует на ОБЕ стороны: и на
	// истечение, и на момент выпуска в будущем.
	ClockSkew time.Duration
	// Clock — источник времени. Вход, а не окружение: граничные сценарии
	// (ровно в момент истечения, ровно в момент начала действия) на системных
	// часах поставить нельзя вовсе, и их отсутствие неотличимо от полноты.
	Clock func() time.Time
}

Policy — объявленная настройка проверяющего.

Каждое поле ОБЯЗАТЕЛЬНО, и это не педантизм: незаданный ожидаемый адресат означает «принимаем любой», незаданный потолок длительности — «любую», неподанные часы — «читаем окружение». Пустое значение здесь означает «не сужаем», а не «по умолчанию».

type ReplayGuard

type ReplayGuard interface {
	Redeem(ctx context.Context, clientID, assertionID string, expiresAt time.Time) error
}

ReplayGuard — порт однократности.

type Result

type Result struct {
	// Outcome — что именно решило исход. Для ЖУРНАЛА и СЧЁТЧИКА, никогда для
	// ответа предъявителю.
	Outcome Outcome
	// Client — разрешённая строка реестра. Заполняется только при приёме.
	Client domain.AssertionClient
	// AssertionID — погашенный идентификатор однократности.
	AssertionID string
	// ExpiresAt — момент истечения предъявленного утверждения.
	ExpiresAt time.Time
	// RetryAfter — через сколько повтор пройдёт по темпу. Заполняется только
	// исходом [OutcomeClientPaceExceeded].
	RetryAfter time.Duration
}

Result — исход проверки вместе с тем, что вправе узнать вызывающий.

func Refuse

func Refuse(o Outcome, format string, args ...any) (Result, error)

Refuse собирает отказ названного исхода за пределами проверяющего — на эндпоинте и на выдаче.

Экспортирована, чтобы отказ СОБИРАЛСЯ ОДНИМ СПОСОБОМ везде: вторая сборка разошлась бы с этой в том, что видит предъявитель, и разошлась бы молча.

func (Result) PresenterResponse

func (r Result) PresenterResponse() string

PresenterResponse — опознавательное слово, которое уходит предъявителю.

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

type TrustedIssuerResolver

type TrustedIssuerResolver interface {
	ResolveTrustedIssuer(ctx context.Context, issuer, subject string) (
		domain.TrustedIssuer, domain.AssertionClient, error)
}

TrustedIssuerResolver — порт НАШЕГО перечня доверенных издателей.

Возвращает и запись доверия, и строку, которую она уполномочивает, ОДНИМ обращением. Два обращения дали бы разную длительность ответа у пары, дошедшей до второго чтения, и у пары, отвергнутой на первом, — то есть оракул, сообщающий, доверен ли издатель, ничего не отвечая по существу.

type Verifier

type Verifier struct {
	// contains filtered or unexported fields
}

Verifier — проверяющий утверждение клиента.

func New

func New(p Policy, clients ClientResolver, issuers TrustedIssuerResolver, replay ReplayGuard, pace ExchangePace) (*Verifier, error)

New строит проверяющего. Неполная настройка — ОТКАЗ ПОСТРОЕНИЯ.

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

func (*Verifier) DeclaredChecks

func (v *Verifier) DeclaredChecks() []tokenpolicy.Check

DeclaredChecks возвращает состав проверок ЭТОГО проверяющего.

Объявление существует затем, чтобы его можно было СВЕРИТЬ с единым перечнем (`tokenpolicy.MandatoryChecks`), а не читать четыре реализации глазами. Этот проверяющий — четвёртый ответ на тот же вопрос «годен ли предъявленный подписанный материал», и он появился ровно так, как предсказывала задача, заводившая единый перечень: вместе с новой поверхностью приёма.

Запись, которой проверяющий не исполняет, отсюда нельзя: тогда объявление станет вторым местом об одном предмете и разойдётся молча.

func (*Verifier) DeclaredDeviations

func (v *Verifier) DeclaredDeviations() []tokenpolicy.Deviation

DeclaredDeviations — обязательные проверки, которых этот проверяющий НЕ исполняет, вместе с причиной.

Причина про РАЗНИЦУ ПРЕДМЕТА, а не про очередь работ: утверждение клиента — не токен доступа. Оно предъявляется РОВНО ОДИН раз, на обмен, и его однократность держит страж повтора (`ReplayGuard`) — механизм строже отзыва: отзыв запрещает предъявлять снова начиная с момента, страж — вообще. Спрашивать авторитет отзыва о материале, который живёт минуты и принимается единожды, нечего.

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, assertionType, raw string) (Result, error)

Verify проверяет предъявленное утверждение.

Порядок проверок — часть решения, а не деталь

Он выбран так, чтобы дорогое стояло после дешёвого, а решения о ключе — после того, как ключ разрешён по РЕЕСТРУ:

вид предъявления → форма → заголовок (дубли · пометки · ТИП) → алгоритм
словаря → личность → ТЕМП → реестр → алгоритм КЛИЕНТА → подпись →
адресат → время → однократность

Темп стоит между личностью и реестром: раньше ему нечем ключеваться, позже он уже не бережёт обращения к хранилищам и сверки подписи (kaname#315).

Две границы в нём несущие. Сверка алгоритма с ЗАРЕГИСТРИРОВАННЫМ у клиента стоит ДО проверки подписи: перечень допустимых алгоритмов строится из строки реестра, никогда из заголовка предъявленного утверждения. И погашение однократности стоит ПОСЛЕДНИМ: гасить идентификатор утверждения, которое всё равно будет отвергнуто, значило бы дать предъявителю способ занимать чужие ключи ненадёжными утверждениями.

func (*Verifier) VerifyFederated

func (v *Verifier) VerifyFederated(ctx context.Context, raw string) (Result, error)

VerifyFederated проверяет утверждение, подписанное внешним издателем, и разрешает его в НАШУ строку через перечень доверенных издателей.

Порядок — тот же довод, что и на полосе клиента

Дорогое после дешёвого, решения о ключе — после того, как ключ разрешён по ЗАПИСИ ДОВЕРИЯ, погашение однократности — последним:

форма → заголовок (дубли · пометки) → алгоритм словаря → личность →
наш издатель запрещён → ТЕМП → перечень доверия → срок доверия →
алгоритм ЗАПИСИ → подпись → адресат → время → однократность

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL