tokenpolicy

package
v1.4.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 1 Imported by: 0

Documentation

Overview

Package tokenpolicy — ОДНО объявленное место для политики токенов платформы: закрытый словарь алгоритмов подписи, перечень обязательных проверок и числа, из которых вычисляется отсрочка снятия подписного ключа.

Почему перечень проверок объявляется, а не описывается словами

Приём нашего токена исполняют РАЗНЫЕ реализации на разных поверхностях. Пока состав обязательных проверок живёт у каждой свой, различие между ними не является ничьей находкой: оно НЕ ВЫРАЖЕНО, и потому не может покраснеть. Перечень здесь — предмет, на который может смотреть гейт по дереву.

Реализация проверок остаётся у поверхности: они разные по существу (одна разбирает токен библиотекой, другая — своим кодом над crypto/*). Общей делается ПОЛИТИКА, а не функция.

Почему числа здесь, а не у того, кто их применяет

Отсрочка снятия ключа ВЫЧИСЛЯЕТСЯ из двух слагаемых: максимального срока выпускаемого токена и потолка кэша ключей у самого «медленного» потребителя. Пока слагаемые объявлены по своим сервисам, арифметика невыразима — её нечем проверить, и она угадывается. Здесь она проверяется гейтом, и смена любого слагаемого без пересмотра отсрочки роняет проверку, называя оба числа.

Index

Constants

View Source
const (
	AlgRS256 = "RS256"
	AlgES256 = "ES256"
	AlgEdDSA = "EdDSA"
)

Алгоритмы подписи, которые платформа выпускает и принимает.

View Source
const (
	// MaxTokenTTL — потолок срока выпускаемого токена. Первое слагаемое
	// арифметики отсрочки: ключ нельзя снять раньше, чем истечёт последний
	// подписанный им токен.
	MaxTokenTTL = 30 * time.Minute

	// ConsumerKeySetCacheCeiling — потолок срока, на который ПОТРЕБИТЕЛЬ
	// вправе удерживать снимок набора ключей. Второе слагаемое: снятый ключ
	// ещё какое-то время отвечает «да» из чужого кэша, и всё выглядит
	// исправным. Потолок обязан быть объявлен числом на КАЖДОЙ поверхности —
	// без него слагаемое неизвестно, и отсрочка не вычисляется, а угадывается.
	ConsumerKeySetCacheCeiling = time.Hour

	// RemovalSlack — запас. Он невелик намеренно: запас, покрывающий
	// произвольную величину, скрыл бы ошибку в слагаемых.
	RemovalSlack = 15 * time.Minute

	// KeyRemovalGrace — отсрочка снятия ключа из набора после вывода его из
	// подписи. ВЫЧИСЛЯЕТСЯ, а не выбирается: смена любого слагаемого без
	// пересмотра этого числа роняет гейт и называет оба.
	KeyRemovalGrace = MaxTokenTTL + ConsumerKeySetCacheCeiling + RemovalSlack

	// ClockSkew — допуск на расхождение часов при приёме токена. Объявлен
	// числом, потому что проба допуска обязана утверждать обе стороны: за
	// пределом — отказ, внутри — проход.
	ClockSkew = 60 * time.Second

	// UnknownKeyIDRefetchInterval — собственный минимальный интервал
	// вынужденного перезапроса набора. Без него неизвестный идентификатор
	// ключа становится усилителем нагрузки: поток запросов с выдуманными
	// идентификаторами превращается в поток обращений к публикатору.
	UnknownKeyIDRefetchInterval = 30 * time.Second

	// KeySetBodyCeiling — потолок тела ответа источника набора. Чтение
	// прекращается на нём, а не «после разбора».
	KeySetBodyCeiling = 1 << 20

	// KeyIDMaxLen — потолок длины идентификатора ключа.
	//
	// Идентификатор приходит ОТ ПРЕДЪЯВИТЕЛЯ и потому ограничивается по форме
	// ДО того, как попадёт в поиск по снимку, в журнал и в повод вынужденного
	// перезапроса. Число объявлено здесь, а не у каждого, кто его применяет: у
	// формы идентификатора три места исполнения — чеканка и две конфигурации
	// приёма, — и разойтись им можно только молча.
	KeyIDMaxLen = 128
)
View Source
const (
	// TokenTypeAccess — объявленный тип токена доступа, выпускаемого
	// платформой (RFC 9068).
	TokenTypeAccess = "at+jwt"

	// TokenTypeClientAssertion — объявленный тип утверждения, которым клиент
	// себя аутентифицирует.
	//
	// Значение ИНОЕ, чем у токена доступа, и это первый из трёх независимых
	// признаков, разделяющих два вида (приёмка F2 §2.6). Единственным его
	// назначать нельзя: клиент, тип не проставивший, снял бы его целиком —
	// поэтому рядом стоят ещё два (адресат и чей ключ подписал), и требование
	// выполняется КАЖДЫМ по отдельности.
	TokenTypeClientAssertion = "client-authentication+jwt"

	// TokenTypeLegacy — тип, которым помечает свои токены ПРЕЖНИЙ издатель.
	//
	// Значение объявлено здесь по той же причине, что и два выше: пока оно жило
	// копиями в конфигурациях двух поверхностей, их различие не было находкой —
	// оно не выражено и потому не может покраснеть. А разойтись им было чем:
	// полоса прежнего издателя ТЕРПИТ отсутствие типа, но несовпадение
	// отвергает, поэтому расхождение значений даёт отказ на КАЖДОМ запросе этой
	// полосы у одной поверхности и ни одного — у другой. Обе при этом зелены на
	// своих пробах: их фикстуры чеканят тип сами.
	//
	// ПРЕДИКАТ СНЯТИЯ: значение уходит вместе с самой полосой прежнего издателя
	// (Ф4 эпика отказа от внешнего сервера выдачи). Пока полоса существует, у
	// значения есть предмет; когда её снимут — эта константа обязана исчезнуть
	// в том же изменении, а не пережить его.
	TokenTypeLegacy = "JWT"

	// ClientAssertionType — объявленный стандартом вид предъявления
	// (RFC 7523 §2.2). Сравнивается ТОЧНО, без нормализации: отличие регистром
	// либо хвостовым пробелом — тоже отказ.
	ClientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"

	// GrantTypeClientCredentials — выдача по учётным данным клиента, чья
	// личность доказана подписанным утверждением (RFC 7523 §2.2).
	GrantTypeClientCredentials = "client_credentials"

	// GrantTypeJWTBearer — ФЕДЕРАТИВНАЯ выдача: основанием служит утверждение
	// ВНЕШНЕГО издателя, которому мы доверяем поимённо (RFC 7523 §2.1,
	// задача #1124).
	//
	// Отличается от вида предъявления выше не только строкой, но и предметом:
	// там утверждение доказывает личность клиента, здесь оно И ЕСТЬ основание
	// выдачи, а клиента, который себя аутентифицирует, нет вовсе. Поэтому
	// параметры у них разные, и смешивать их формы нельзя: иначе предъявитель
	// выбирал бы, какой проверкой его проверят.
	//
	// #nosec G101 -- это ОБЪЯВЛЕННОЕ СТАНДАРТОМ имя вида выдачи, а не учётные
	// данные: строка приезжает в теле запроса от клиента и сверяется с ней же.
	// Её знает всякий, кто читал RFC 7523; секретом она быть не может by
	// construction, потому что клиент обязан её ПРИСЛАТЬ.
	GrantTypeJWTBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer"
)
View Source
const ExpiredCredentialReclaimGrace = 24 * time.Hour

ExpiredCredentialReclaimGrace — верхняя отсрочка снятия истёкшего удостоверения.

Это ПРОДУКТОВОЕ решение, а не арифметика, и сказано это прямо: арифметика даёт нижнюю границу (MinExpiredCredentialReclaimDelay, около двадцати минут), а сутки выбраны НАБЛЮДАЕМОСТЬЮ. Человек, у которого доступ перестал работать ночью, приходит утром и обязан увидеть ПРИЧИНУ — истёкшую строку в перечне, — а не пустоту. Сутки покрывают цикл дежурства; меньшая величина отдаёт разбор в журнал аудита, большая ничего не добавляет, потому что след снятия в журнале остаётся навсегда.

View Source
const MaxAssertionLifetime = 5 * time.Minute

MaxAssertionLifetime — потолок разницы «срок − момент выпуска» у утверждения клиента.

Объявлен числом РОВНО ОДИН РАЗ и участвует в двух расчётах сразу: он ограничивает длительность самого утверждения и он же задаёт верхнюю границу жизни строки погашения. Два объявления об одном предмете разошлись бы молча — и разошлись бы там, где расхождение не видно, потому что обе величины по отдельности выглядят разумными.

Почему потолок вообще нужен

Запись погашения обязана жить до истечения утверждения: снять её раньше значит сделать повтор законным. Значит длительность утверждения И ЕСТЬ срок жизни строки, а выбирает эту длительность ПРЕДЪЯВИТЕЛЬ. Без потолка утверждение со сроком в годы занимает свой ключ на годы, то есть хранилище, ограниченное сроком, перестаёт быть ограниченным, а отказ в обслуживании становится дешёвым.

Почему отсчёт от момента выпуска, а не от «сейчас»

«Сейчас» — у нас, момент выпуска — у клиента. Отсчёт от «сейчас» дал бы разный потолок при одном и том же утверждении в зависимости от задержки доставки, то есть величину, которую нельзя ни объяснить клиенту, ни воспроизвести в пробе. Разница `exp − iat` — свойство самого утверждения.

View Source
const MaxFederatedAssertionLifetime = time.Hour

MaxFederatedAssertionLifetime — потолок той же разницы у утверждения, которое подписал ВНЕШНИЙ издатель (задача #1124).

Почему потолок здесь ДРУГОЙ, а не тот же

Потолок выше выбран из того, что утверждение полосы клиента выписывается СПЕЦИАЛЬНО для нас: подписант — наш же клиент, он знает адресата и вправе дать документу минуты. Внешний издатель выпускает своей нагрузке токен со своим сроком и о нашем потолке не знает вовсе. Приложи мы к нему пятиминутную величину — федеративная полоса отвергала бы КАЖДОЕ утверждение обычного издателя, и выглядело бы это как исправная строгая проверка.

Почему час, а не «сколько дадут»

Час покрывает наблюдаемые сроки: проецируемый токен Kubernetes по умолчанию живёт час, полосы сборочных конвейеров — минуты. Всё, что дольше, есть предъявительский документ с окном в сутки и более: однократность гасит повтор, но окно ДО первого предъявления перехвативший получает целиком.

Величина связана с тем же расчётом, что и потолок выше: длительность утверждения есть срок жизни строки погашения, и час означает час, а не месяц.

View Source
const MinRSAModulusBits = 2048

MinRSAModulusBits — минимальный допустимый размер модуля RSA.

Короткий модуль факторизуется, а значит подпись подделывается: ключ меньшего размера не должен попадать в снимок вовсе. Величина объявлена ЗДЕСЬ, потому что её обязаны знать все, кто принимает ключи, а второй словарь разошёлся бы молча — и разошёлся бы в сторону «принимаем меньше», потому что смягчать проще, чем ужесточать.

View Source
const SecretCredentialTTLCeiling = 90 * 24 * time.Hour

SecretCredentialTTLCeiling — потолок срока удостоверения вида SECRET.

Почему потолок вообще есть

Секрет ПРЕДЪЯВИТЕЛЬСКИЙ: доказать владение при предъявлении он не может ни на одной поверхности, где принимается (докерная полоса шлёт `Basic`, сборочный конвейер кладёт строку в переменную). Значит перехвативший строку получает ровно то же, что владелец, и окно этого равенства закрывает СРОК — больше ничего. Отзыв закрывает окно только там, где владелец заметил утечку.

Почему 90 суток, а не «сколько попросят»

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

Срок СВЕРХ потолка отвергается, а НЕ урезается молча: урезание даёт вызывающему успех при неприменённом параметре — «принято-и-проигнорировано».

View Source
const SecretCredentialTTLDefault = 30 * 24 * time.Hour

SecretCredentialTTLDefault — срок, применяемый когда вызывающий его не назвал.

Почему умолчание, а не отказ

Требование срока было бы ломающим изменением у существующего глагола: клиент, сегодня законно не шлющий срока, начал бы получать отказ. Умолчание сохраняет вход и при этом НЕ заводит бессрочного секрета.

Коллизия нуля разрешена ИЗЪЯТИЕМ значения, а не вторым его написанием

У видов KEYPAIR и FEDERATED `ttl_seconds = 0` означает «бессрочно». У вида SECRET бессрочного НЕ СУЩЕСТВУЕТ НИ В КАКОМ НАПИСАНИИ, поэтому ноль означает «срок не назван» и разрешается этим умолчанием. Из области значений убрано одно из двух значений — не введён второй способ написать его.

Цена названа честно: один и тот же ноль означает у двух видов разное. Смягчение — в том, что неоднозначность живёт ТОЛЬКО ВО ВХОДЕ и снимается на границе: строка удостоверения и ответ выдачи всегда несут ЗАПОЛНЕННЫЙ срок, поэтому ниже по потоку двусмысленности нет ни у одного читателя.

Variables

This section is empty.

Functions

func AlgorithmAllowed

func AlgorithmAllowed(alg string) bool

AlgorithmAllowed отвечает, входит ли алгоритм в словарь.

Пустое значение НЕ входит: «алгоритм не назван» означало бы «любой», а именно этого класса мы и избегаем.

func Algorithms

func Algorithms() []string

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

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

func CriticalHeadersUnderstood

func CriticalHeadersUnderstood(crit []string) (bool, string)

CriticalHeadersUnderstood отвечает, можем ли мы принять токен с этим `crit`.

Второй ответ — имя первого непонятого параметра — нужен тексту отказа: без него оператор видит «токен отвергнут» и не знает, чем именно он негоден.

Пустой и отсутствующий `crit` — законный вход: требование касается только того, что отправитель ЯВНО пометил обязательным.

func ExpiredCredentialGraceFor

func ExpiredCredentialGraceFor(lifetime, ceiling, minDelay time.Duration) time.Duration

ExpiredCredentialGraceFor — отсрочка ОДНОЙ строки: она связана со сроком самой строки, а не одинакова для всех.

Правило одной фразой: окно памяти о вещи не должно быть дольше жизни самой вещи. Никто не ищет часовое удостоверение сутки спустя, и никто не приходит за девяностосуточным чаще раза в сутки.

grace = min( ceiling , max( minDelay , lifetime ) )

Зачем связывать. Минимального срока у секрета нет — законно и часовое удостоверение, — поэтому плоская отсрочка держала бы его труп в двадцать четыре раза дольше, чем жила сама вещь. Тогда ОТСРОЧКА САМА становилась бы тем, что упирается в потолок, то есть работа частично воспроизводила бы предмет, который чинит.

Ниже minDelay результат не опускается никогда: там ещё живут отчеканенные токены. Следствие названо прямо — при сроке короче пола строка лежит дольше, чем жила; это свойство пола, а не дефект уборки.

func GraceCoversLiveTokens

func GraceCoversLiveTokens() bool

GraceCoversLiveTokens отвечает, покрывает ли объявленная отсрочка оба слагаемых. Предикат, а не комментарий: его зовёт гейт.

func GrantTypes

func GrantTypes() []string

GrantTypes возвращает ЗАКРЫТЫЙ перечень видов выдачи, принимаемых нашим токен-эндпоинтом.

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

func KeySourceHeaderMember

func KeySourceHeaderMember(name string) bool

KeySourceHeaderMember отвечает, является ли член заголовка источником ключа.

func KeySourceHeaderMembers

func KeySourceHeaderMembers() []string

KeySourceHeaderMembers — члены заголовка, из которых ключ проверки выбираться НЕ МОЖЕТ.

Перечень объявлен в ОДНОМ месте, а не выписан по месту употребления: место, закрывшее один член и забывшее второй, выглядит закрытым, и разница видна только тому, кто держит перечень в голове.

Почему это запрет, а не осторожность

Доверять ключу, приехавшему ВМЕСТЕ С ПОДПИСЬЮ, значит принимать любую подпись: предъявитель приложит тот ключ, которым подписал. Ключ выбирается ТОЛЬКО из нашего реестра.

Где то же доверие ЗАКОННО — названо, чтобы запрет не расползся

В доказательстве владения ключом (RFC 9449) ключ приходит В САМОМ доказательстве, и проверяющий связывает его с токеном по отпечатку: там встроенный ключ не уязвимость, а ПРЕДМЕТ. Такое место обязано включать выбор из предъявленного материала ЯВНО и по своему решению — умолчание «как получится» перенесло бы доверие туда, где оно недопустимо, и перенесло бы ТИХО: положительный путь при этом работает.

func KnownCriticalHeaders

func KnownCriticalHeaders() []string

KnownCriticalHeaders — параметры заголовка, которые мы УМЕЕМ исполнять и потому вправе принять помеченными обязательными.

Перечень ПУСТ, и это факт, а не заготовка: ни одна поверхность приёма не исполняет ни одного расширения заголовка. Пустой перечень означает «любой помеченный обязательным параметр отвергается» — то, что и требуется, пока расширений нет.

func MinExpiredCredentialReclaimDelay

func MinExpiredCredentialReclaimDelay(registryTokenTTL time.Duration) time.Duration

MinExpiredCredentialReclaimDelay — техническая НИЖНЯЯ граница отсрочки.

Раньше неё снимать нельзя ни при какой настройке: до этого момента ещё живут токены, отчеканенные снимаемой строкой.

	ClockSkew + registryTokenTTL + RemovalSlack

  - ClockSkew — часы предъявителя и наши расходятся; строка, истёкшая по нашим
    часам, у него ещё жива;
  - registryTokenTTL — на полосе секрета срок докерного токена НЕ урезается
    остатком срока строки (он чеканится фиксированным), поэтому токен,
    отчеканенный перед самым истечением, живёт ещё столько;
  - RemovalSlack — запас, невелик намеренно.

MaxTokenTTL в сумму НЕ входит, и это решение, а не пропуск. У KeyRemovalGrace это слагаемое есть потому, что там ключ снимают из набора ПРИ ЖИВЫХ токенах. На полосе ключевой пары такого состояния не бывает: срок токена урезается до остатка срока клиента, и запас взят самим урезанием. Внести MaxTokenTTL сюда значило бы заплатить дважды за одно свойство; полоса, где урезания нет, учтена СВОИМ слагаемым — сроком докерного токена.

Срок докерного токена приходит АРГУМЕНТОМ, а не константой: он конфигурируем (`api-server.registry-token.ttl`), и страж старта обязан считать границу по ДЕЙСТВУЮЩЕМУ значению. Иначе поднятый срок молча вывел бы отсрочку из-под её же основания.

func ReclaimGraceCoversLiveTokens

func ReclaimGraceCoversLiveTokens(grace, registryTokenTTL time.Duration) bool

ReclaimGraceCoversLiveTokens — предикат, а не комментарий: его зовёт гейт.

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

func ResolveSecretCredentialTTL

func ResolveSecretCredentialTTL(requested time.Duration) (ttl time.Duration, ok bool)

ResolveSecretCredentialTTL — единственное место, где ноль превращается в срок, а превышение потолка — в отказ. Второй копии этого разрешения не заводится: копия разошлась бы молча и разошлась бы в сторону «принимаем больше».

ok=false означает «сверх потолка»; вызывающий обязан отвергнуть запрос с именем поля, а не урезать величину.

func SignedMaterialTypes

func SignedMaterialTypes() []string

SignedMaterialTypes возвращает объявленные типы обоих видов подписанного.

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

Types

type Check

type Check string

Check — обязательная проверка предъявленного токена.

const (
	// CheckSignature — подпись сходится с ключом, найденным по kid.
	CheckSignature Check = "signature"
	// CheckAlgorithmAllowed — алгоритм заголовка входит в закрытый словарь;
	// «без подписи» отвергается ДО разрешения ключа.
	CheckAlgorithmAllowed Check = "algorithm-allowed"
	// CheckKeyBoundAlgorithm — алгоритм заголовка равен алгоритму,
	// ЗАКРЕПЛЁННОМУ за найденным ключом. Заголовок алгоритм не выбирает.
	CheckKeyBoundAlgorithm Check = "key-bound-algorithm"
	// CheckIssuer — издатель токена имеет объявленную запись источника
	// ключей; издателя без записи не бывает.
	CheckIssuer Check = "issuer"
	// CheckAudience — адресат токена равен адресату поверхности. Незаданный
	// адресат означает «любой», поэтому он обязателен в настройке.
	CheckAudience Check = "audience"
	// CheckTokenType — тип объявлен и равен ожидаемому.
	CheckTokenType Check = "token-type"
	// CheckExpiry — срок ПРИСУТСТВУЕТ и не истёк. Обязательность включается
	// ЯВНЫМ параметром: разбор, не встретив срока, не возразит сам.
	CheckExpiry Check = "expiry"
	// CheckNotBefore — токен не выпущен «из будущего» за пределом допуска.
	CheckNotBefore Check = "not-before"
	// CheckKeyID — идентификатор ключа присутствует, его форма ограничена ДО
	// использования, и он резолвится в ключ объявленной записи.
	CheckKeyID Check = "key-id"
	// CheckCriticalHeaders — параметр, помеченный отправителем обязательным к
	// пониманию (`crit`, RFC 7515 §4.1.11), либо исполняется, либо токен
	// отвергается целиком. Обратная сторона того же требования — НЕ помеченное
	// неизвестное игнорируется (RFC 7519, EID 8060): ужесточение первого без
	// второго ломает совместимость.
	CheckCriticalHeaders Check = "critical-headers"
	// CheckRevocation — отзыв читается НА ПРЕДЪЯВЛЕНИИ. Контроль, действующий
	// только на выдаче, отзывом не является: он лишь не выдаёт нового.
	CheckRevocation Check = "revocation"
)

Обязательные проверки. Перечень ЗАКРЫТ: поверхность, объявляющая свою проверку сверх него, обязана объявить и ПРИЧИНУ — молчаливое расхождение является находкой.

func MandatoryChecks

func MandatoryChecks() []Check

MandatoryChecks возвращает перечень обязательных проверок.

Это и есть тот «один объявленный перечень», которым пользуются все реализации приёма. Реализация объявляет, какие проверки она исполняет, и гейт сверяет её объявление с этим перечнем.

func MissingChecks

func MissingChecks(declared []Check) []Check

MissingChecks возвращает обязательные проверки, которых нет в объявлении реализации. Пустой ответ означает согласие с перечнем.

func MissingChecksExcept

func MissingChecksExcept(declared []Check, deviations []Deviation) []Check

MissingChecksExcept возвращает обязательные проверки, которых нет ни в объявлении реализации, ни в перечне её объявленных отступлений.

Пустой ответ означает: всякая обязательная проверка либо исполняется, либо её неисполнение НАЗВАНО с причиной. Отступление с пустой причиной не засчитывается — иначе поле стало бы способом снять требование молча.

type Deviation

type Deviation struct {
	Check  Check
	Reason string
}

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

Причина обязательна и не бывает «пока»: отступление без причины неотличимо от пропуска, а «сделаем потом» — тот самый отложенный долг, за который никто не отвечает. Причина обязана называть, ЧТО делает эту проверку неприменимой здесь, а не то, когда её собираются добавить.

Jump to

Keyboard shortcuts

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