Documentation
¶
Overview ¶
federated.go — федеративная полоса приёма: утверждение подписал ВНЕШНИЙ издатель, а перечень доверенных издателей ведём мы (задача kacho#1124, RFC 7523 §2.1 — вид выдачи, не аутентификация клиента).
Чем эта полоса отличается от полосы аутентификации клиента ¶
Там издатель и субъект обязаны совпадать и оба назвать НАШУ строку, а ключ берётся из строки клиента. Здесь издатель — посторонний, субъект — его собственный, ключ принадлежит ему, и единственное, что есть у нас, — ЗАПИСЬ О ДОВЕРИИ. Промах поэтому даёт не «приняли своё за чужое», а токен платформы предъявителю, которого мы не заводили вовсе.
Три признака, отделяющих утверждение от нашего же токена доступа ¶
На полосе клиента первый признак — объявленный нами тип в заголовке. Здесь его нет и быть не может: тип ставит производитель, а производитель тут посторонний, и требование типа отвергало бы каждого обычного издателя. Признаки, которые остаются и которых достаточно:
- **ключ.** Подпись проверяется ключом ИЗДАТЕЛЯ из записи доверия. Наш токен доступа подписан нашим ключом, которого в этой таблице нет;
- **наш идентификатор издателя в этой таблице ЗАПРЕЩЁН** — иначе первый признак снимался бы одной административной записью, и наш собственный токен доступа стал бы приниматься как чужое утверждение;
- **адресат.** Утверждение адресовано нашему издателю; токен доступа адресован ресурсу.
Package clientassertion — принимающая сторона аутентификации клиента подписанным утверждением (RFC 7523 §2.2, задача kacho#898).
Чем эта сторона отличается от предъявляющей ¶
Утверждение этого вида дерево уже подписывает и предъявляет; вся та работа стоит на стороне КЛИЕНТА. Здесь заводится сторона СЕРВЕРА, и цена ошибки у неё противоположна: у предъявителя неверное утверждение даёт отказ, видимый сразу, — у принимающего неверная проверка даёт ПРИНЯТОЕ ЧУЖОЕ утверждение, не видимое никогда. Успешная аутентификация выглядит одинаково независимо от того, что именно она проверила.
Отсюда правило работ, которому подчинён весь порядок ниже: на каждой развилке, где неясно, отвергать или пропускать, выбирается ОТВЕРГАТЬ.
Почему разбор идёт над той же библиотекой, а не своей ¶
Стеков разбора JOSE в дереве и без нас несколько, и решения, принятые здесь (закрытый словарь алгоритмов, отказ на подмену алгоритма, дублирующееся имя члена заголовка, член, помеченный обязательным к пониманию), доехали бы только до одного из них. Второй библиотеки «ради удобства утверждения» не заводится: проверки, которых библиотека не делает, добавляются шагом НАД ней.
Что здесь делается ДО библиотеки и почему именно это ¶
Три вещи библиотека не делает by construction, и каждая из них — отказ:
- **имя члена заголовка дважды.** Разбор объектной записи, встретив имя дважды, берёт одно из значений и не возражает. Два значения одного члена означают, что проверяющий и подписант МОГЛИ ПРОЧИТАТЬ РАЗНОЕ, а значит проверенной оказалась не та половина (RFC 7515 §5.2);
- **член, помеченный обязательным к пониманию.** Пометка означает «без понимания этого члена подпись нельзя считать проверенной». Принять — значит объявить проверенным то, чего не поняли;
- **встроенный ключевой материал.** Ключ выбирается ТОЛЬКО из реестра; перечень членов, из которых он выбираться не может, объявлен в pkg/tokenpolicy одним местом.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func OurSide ¶ added in v0.5.0
OurSide — решил ли исход отказа НЕ предъявитель, а наша сторона: реестр или хранилище однократности не ответили, вопрос об отсечке не задался, выпуск не состоялся. Чинит такой отказ оператор, а не вызывающий.
Одно место классификации на всех читателей: счёт отказов против источника (П3) не засчитывает эти исходы, журнал пишет их уровнем ошибки. Две копии перечня разошлись бы молча — и в сторону «наш сбой засчитан клиенту». Перечень закрыт: исход, заведённый в словаре позже, остаётся исходом предъявителя, пока его не назовут здесь.
func PresenterResponseFor ¶
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" // 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 ¶
Refuse собирает отказ названного исхода за пределами проверяющего — на эндпоинте и на выдаче.
Экспортирована, чтобы отказ СОБИРАЛСЯ ОДНИМ СПОСОБОМ везде: вторая сборка разошлась бы с этой в том, что видит предъявитель, и разошлась бы молча.
func (Result) PresenterResponse ¶
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 ¶
Verify проверяет предъявленное утверждение.
Порядок проверок — часть решения, а не деталь ¶
Он выбран так, чтобы дорогое стояло после дешёвого, а решения о ключе — после того, как ключ разрешён по РЕЕСТРУ:
вид предъявления → форма → заголовок (дубли · пометки · ТИП) → алгоритм словаря → личность → ТЕМП → реестр → алгоритм КЛИЕНТА → подпись → адресат → время → однократность
Темп стоит между личностью и реестром: раньше ему нечем ключеваться, позже он уже не бережёт обращения к хранилищам и сверки подписи (kaname#315).
Две границы в нём несущие. Сверка алгоритма с ЗАРЕГИСТРИРОВАННЫМ у клиента стоит ДО проверки подписи: перечень допустимых алгоритмов строится из строки реестра, никогда из заголовка предъявленного утверждения. И погашение однократности стоит ПОСЛЕДНИМ: гасить идентификатор утверждения, которое всё равно будет отвергнуто, значило бы дать предъявителю способ занимать чужие ключи ненадёжными утверждениями.
func (*Verifier) VerifyFederated ¶
VerifyFederated проверяет утверждение, подписанное внешним издателем, и разрешает его в НАШУ строку через перечень доверенных издателей.
Порядок — тот же довод, что и на полосе клиента ¶
Дорогое после дешёвого, решения о ключе — после того, как ключ разрешён по ЗАПИСИ ДОВЕРИЯ, погашение однократности — последним:
форма → заголовок (дубли · пометки) → алгоритм словаря → личность → наш издатель запрещён → ТЕМП → перечень доверия → срок доверия → алгоритм ЗАПИСИ → подпись → адресат → время → однократность