clientassertion

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

Documentation

Overview

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

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

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

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

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

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

Package clientassertion — принимающая сторона аутентификации клиента подписанным утверждением (RFC 7523 §2.2, задача #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 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 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"

	// 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"
	// OutcomeAudienceNotAllowed — запрошенный адресат вне объявленного
	// конфигурацией перечня адресатов платформы.
	OutcomeAudienceNotAllowed Outcome = "requested-audience-not-allowed"
	// OutcomeClientExpired — срок клиента истёк.
	OutcomeClientExpired Outcome = "client-expired"
	// OutcomeOwnerNotActive — владелец клиента не в состоянии ACTIVE.
	OutcomeOwnerNotActive Outcome = "owner-not-active"
	// 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
}

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) (*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 проверяет предъявленное утверждение.

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

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

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

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

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