Documentation
¶
Overview ¶
Package tokenpolicy — ОДНО объявленное место для политики токенов платформы: закрытый словарь алгоритмов подписи, перечень обязательных проверок и числа, из которых вычисляется отсрочка снятия подписного ключа.
Почему перечень проверок объявляется, а не описывается словами ¶
Приём нашего токена исполняют РАЗНЫЕ реализации на разных поверхностях. Пока состав обязательных проверок живёт у каждой свой, различие между ними не является ничьей находкой: оно НЕ ВЫРАЖЕНО, и потому не может покраснеть. Перечень здесь — предмет, на который может смотреть гейт по дереву.
Реализация проверок остаётся у поверхности: они разные по существу (одна разбирает токен библиотекой, другая — своим кодом над crypto/*). Общей делается ПОЛИТИКА, а не функция.
Почему числа здесь, а не у того, кто их применяет ¶
Отсрочка снятия ключа ВЫЧИСЛЯЕТСЯ из двух слагаемых: максимального срока выпускаемого токена и потолка кэша ключей у самого «медленного» потребителя. Пока слагаемые объявлены по своим сервисам, арифметика невыразима — её нечем проверить, и она угадывается. Здесь она проверяется гейтом, и смена любого слагаемого без пересмотра отсрочки роняет проверку, называя оба числа.
Index ¶
- Constants
- func AlgorithmAllowed(alg string) bool
- func Algorithms() []string
- func CriticalHeadersUnderstood(crit []string) (bool, string)
- func ExpiredCredentialGraceFor(lifetime, ceiling, minDelay time.Duration) time.Duration
- func GraceCoversLiveTokens() bool
- func GrantTypes() []string
- func KeySourceHeaderMember(name string) bool
- func KeySourceHeaderMembers() []string
- func KnownCriticalHeaders() []string
- func MinExpiredCredentialReclaimDelay(registryTokenTTL time.Duration) time.Duration
- func ReclaimGraceCoversLiveTokens(grace, registryTokenTTL time.Duration) bool
- func ResolveSecretCredentialTTL(requested time.Duration) (ttl time.Duration, ok bool)
- func SignedMaterialTypes() []string
- type Check
- type Deviation
Constants ¶
const ( AlgRS256 = "RS256" AlgES256 = "ES256" AlgEdDSA = "EdDSA" )
Алгоритмы подписи, которые платформа выпускает и принимает.
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 )
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" )
const ExpiredCredentialReclaimGrace = 24 * time.Hour
ExpiredCredentialReclaimGrace — верхняя отсрочка снятия истёкшего удостоверения.
Это ПРОДУКТОВОЕ решение, а не арифметика, и сказано это прямо: арифметика даёт нижнюю границу (MinExpiredCredentialReclaimDelay, около двадцати минут), а сутки выбраны НАБЛЮДАЕМОСТЬЮ. Человек, у которого доступ перестал работать ночью, приходит утром и обязан увидеть ПРИЧИНУ — истёкшую строку в перечне, — а не пустоту. Сутки покрывают цикл дежурства; меньшая величина отдаёт разбор в журнал аудита, большая ничего не добавляет, потому что след снятия в журнале остаётся навсегда.
const MaxAssertionLifetime = 5 * time.Minute
MaxAssertionLifetime — потолок разницы «срок − момент выпуска» у утверждения клиента.
Объявлен числом РОВНО ОДИН РАЗ и участвует в двух расчётах сразу: он ограничивает длительность самого утверждения и он же задаёт верхнюю границу жизни строки погашения. Два объявления об одном предмете разошлись бы молча — и разошлись бы там, где расхождение не видно, потому что обе величины по отдельности выглядят разумными.
Почему потолок вообще нужен ¶
Запись погашения обязана жить до истечения утверждения: снять её раньше значит сделать повтор законным. Значит длительность утверждения И ЕСТЬ срок жизни строки, а выбирает эту длительность ПРЕДЪЯВИТЕЛЬ. Без потолка утверждение со сроком в годы занимает свой ключ на годы, то есть хранилище, ограниченное сроком, перестаёт быть ограниченным, а отказ в обслуживании становится дешёвым.
Почему отсчёт от момента выпуска, а не от «сейчас» ¶
«Сейчас» — у нас, момент выпуска — у клиента. Отсчёт от «сейчас» дал бы разный потолок при одном и том же утверждении в зависимости от задержки доставки, то есть величину, которую нельзя ни объяснить клиенту, ни воспроизвести в пробе. Разница `exp − iat` — свойство самого утверждения.
const MaxFederatedAssertionLifetime = time.Hour
MaxFederatedAssertionLifetime — потолок той же разницы у утверждения, которое подписал ВНЕШНИЙ издатель (задача #1124).
Почему потолок здесь ДРУГОЙ, а не тот же ¶
Потолок выше выбран из того, что утверждение полосы клиента выписывается СПЕЦИАЛЬНО для нас: подписант — наш же клиент, он знает адресата и вправе дать документу минуты. Внешний издатель выпускает своей нагрузке токен со своим сроком и о нашем потолке не знает вовсе. Приложи мы к нему пятиминутную величину — федеративная полоса отвергала бы КАЖДОЕ утверждение обычного издателя, и выглядело бы это как исправная строгая проверка.
Почему час, а не «сколько дадут» ¶
Час покрывает наблюдаемые сроки: проецируемый токен Kubernetes по умолчанию живёт час, полосы сборочных конвейеров — минуты. Всё, что дольше, есть предъявительский документ с окном в сутки и более: однократность гасит повтор, но окно ДО первого предъявления перехвативший получает целиком.
Величина связана с тем же расчётом, что и потолок выше: длительность утверждения есть срок жизни строки погашения, и час означает час, а не месяц.
const MinRSAModulusBits = 2048
MinRSAModulusBits — минимальный допустимый размер модуля RSA.
Короткий модуль факторизуется, а значит подпись подделывается: ключ меньшего размера не должен попадать в снимок вовсе. Величина объявлена ЗДЕСЬ, потому что её обязаны знать все, кто принимает ключи, а второй словарь разошёлся бы молча — и разошёлся бы в сторону «принимаем меньше», потому что смягчать проще, чем ужесточать.
const SecretCredentialTTLCeiling = 90 * 24 * time.Hour
SecretCredentialTTLCeiling — потолок срока удостоверения вида SECRET.
Почему потолок вообще есть ¶
Секрет ПРЕДЪЯВИТЕЛЬСКИЙ: доказать владение при предъявлении он не может ни на одной поверхности, где принимается (докерная полоса шлёт `Basic`, сборочный конвейер кладёт строку в переменную). Значит перехвативший строку получает ровно то же, что владелец, и окно этого равенства закрывает СРОК — больше ничего. Отзыв закрывает окно только там, где владелец заметил утечку.
Почему 90 суток, а не «сколько попросят» ¶
Девяносто суток — квартал: срок, который переживает отпуск и релизный цикл, но не переживает смену состава команды. Больше означало бы удостоверение, живущее дольше, чем помнят о его существовании.
Срок СВЕРХ потолка отвергается, а НЕ урезается молча: урезание даёт вызывающему успех при неприменённом параметре — «принято-и-проигнорировано».
const SecretCredentialTTLDefault = 30 * 24 * time.Hour
SecretCredentialTTLDefault — срок, применяемый когда вызывающий его не назвал.
Почему умолчание, а не отказ ¶
Требование срока было бы ломающим изменением у существующего глагола: клиент, сегодня законно не шлющий срока, начал бы получать отказ. Умолчание сохраняет вход и при этом НЕ заводит бессрочного секрета.
Коллизия нуля разрешена ИЗЪЯТИЕМ значения, а не вторым его написанием ¶
У видов KEYPAIR и FEDERATED `ttl_seconds = 0` означает «бессрочно». У вида SECRET бессрочного НЕ СУЩЕСТВУЕТ НИ В КАКОМ НАПИСАНИИ, поэтому ноль означает «срок не назван» и разрешается этим умолчанием. Из области значений убрано одно из двух значений — не введён второй способ написать его.
Цена названа честно: один и тот же ноль означает у двух видов разное. Смягчение — в том, что неоднозначность живёт ТОЛЬКО ВО ВХОДЕ и снимается на границе: строка удостоверения и ответ выдачи всегда несут ЗАПОЛНЕННЫЙ срок, поэтому ниже по потоку двусмысленности нет ни у одного читателя.
Variables ¶
This section is empty.
Functions ¶
func AlgorithmAllowed ¶
AlgorithmAllowed отвечает, входит ли алгоритм в словарь.
Пустое значение НЕ входит: «алгоритм не назван» означало бы «любой», а именно этого класса мы и избегаем.
func Algorithms ¶
func Algorithms() []string
Algorithms возвращает закрытый словарь целиком.
Всякий, кому нужен перечень, ВЫВОДИТ его отсюда: страж старта, текст отказа, сверка алгоритма с ключом. Вторая копия словаря разошлась бы молча — и разошлась бы в сторону «принимаем больше», потому что расширять проще.
func CriticalHeadersUnderstood ¶
CriticalHeadersUnderstood отвечает, можем ли мы принять токен с этим `crit`.
Второй ответ — имя первого непонятого параметра — нужен тексту отказа: без него оператор видит «токен отвергнут» и не знает, чем именно он негоден.
Пустой и отсутствующий `crit` — законный вход: требование касается только того, что отправитель ЯВНО пометил обязательным.
func ExpiredCredentialGraceFor ¶
ExpiredCredentialGraceFor — отсрочка ОДНОЙ строки: она связана со сроком самой строки, а не одинакова для всех.
Правило одной фразой: окно памяти о вещи не должно быть дольше жизни самой вещи. Никто не ищет часовое удостоверение сутки спустя, и никто не приходит за девяностосуточным чаще раза в сутки.
grace = min( ceiling , max( minDelay , lifetime ) )
Зачем связывать. Минимального срока у секрета нет — законно и часовое удостоверение, — поэтому плоская отсрочка держала бы его труп в двадцать четыре раза дольше, чем жила сама вещь. Тогда ОТСРОЧКА САМА становилась бы тем, что упирается в потолок, то есть работа частично воспроизводила бы предмет, который чинит.
Ниже minDelay результат не опускается никогда: там ещё живут отчеканенные токены. Следствие названо прямо — при сроке короче пола строка лежит дольше, чем жила; это свойство пола, а не дефект уборки.
func GraceCoversLiveTokens ¶
func GraceCoversLiveTokens() bool
GraceCoversLiveTokens отвечает, покрывает ли объявленная отсрочка оба слагаемых. Предикат, а не комментарий: его зовёт гейт.
func GrantTypes ¶
func GrantTypes() []string
GrantTypes возвращает ЗАКРЫТЫЙ перечень видов выдачи, принимаемых нашим токен-эндпоинтом.
Перечень объявлен один раз и функцией, а не выписан по месту употребления: место, узнавшее про один вид и не узнавшее про второй, выглядит полным, а «прочее» корзиной приёма не является ни в одном из них.
func KeySourceHeaderMember ¶
KeySourceHeaderMember отвечает, является ли член заголовка источником ключа.
func KeySourceHeaderMembers ¶
func KeySourceHeaderMembers() []string
KeySourceHeaderMembers — члены заголовка, из которых ключ проверки выбираться НЕ МОЖЕТ.
Перечень объявлен в ОДНОМ месте, а не выписан по месту употребления: место, закрывшее один член и забывшее второй, выглядит закрытым, и разница видна только тому, кто держит перечень в голове.
Почему это запрет, а не осторожность ¶
Доверять ключу, приехавшему ВМЕСТЕ С ПОДПИСЬЮ, значит принимать любую подпись: предъявитель приложит тот ключ, которым подписал. Ключ выбирается ТОЛЬКО из нашего реестра.
Где то же доверие ЗАКОННО — названо, чтобы запрет не расползся ¶
В доказательстве владения ключом (RFC 9449) ключ приходит В САМОМ доказательстве, и проверяющий связывает его с токеном по отпечатку: там встроенный ключ не уязвимость, а ПРЕДМЕТ. Такое место обязано включать выбор из предъявленного материала ЯВНО и по своему решению — умолчание «как получится» перенесло бы доверие туда, где оно недопустимо, и перенесло бы ТИХО: положительный путь при этом работает.
func KnownCriticalHeaders ¶
func KnownCriticalHeaders() []string
KnownCriticalHeaders — параметры заголовка, которые мы УМЕЕМ исполнять и потому вправе принять помеченными обязательными.
Перечень ПУСТ, и это факт, а не заготовка: ни одна поверхность приёма не исполняет ни одного расширения заголовка. Пустой перечень означает «любой помеченный обязательным параметр отвергается» — то, что и требуется, пока расширений нет.
func MinExpiredCredentialReclaimDelay ¶
MinExpiredCredentialReclaimDelay — техническая НИЖНЯЯ граница отсрочки.
Раньше неё снимать нельзя ни при какой настройке: до этого момента ещё живут токены, отчеканенные снимаемой строкой.
ClockSkew + registryTokenTTL + RemovalSlack
- ClockSkew — часы предъявителя и наши расходятся; строка, истёкшая по нашим
часам, у него ещё жива;
- registryTokenTTL — на полосе секрета срок докерного токена НЕ урезается
остатком срока строки (он чеканится фиксированным), поэтому токен,
отчеканенный перед самым истечением, живёт ещё столько;
- RemovalSlack — запас, невелик намеренно.
MaxTokenTTL в сумму НЕ входит, и это решение, а не пропуск. У KeyRemovalGrace это слагаемое есть потому, что там ключ снимают из набора ПРИ ЖИВЫХ токенах. На полосе ключевой пары такого состояния не бывает: срок токена урезается до остатка срока клиента, и запас взят самим урезанием. Внести MaxTokenTTL сюда значило бы заплатить дважды за одно свойство; полоса, где урезания нет, учтена СВОИМ слагаемым — сроком докерного токена.
Срок докерного токена приходит АРГУМЕНТОМ, а не константой: он конфигурируем (`api-server.registry-token.ttl`), и страж старта обязан считать границу по ДЕЙСТВУЮЩЕМУ значению. Иначе поднятый срок молча вывел бы отсрочку из-под её же основания.
func ReclaimGraceCoversLiveTokens ¶
ReclaimGraceCoversLiveTokens — предикат, а не комментарий: его зовёт гейт.
Он существует затем, чтобы следующий, кто захочет снимать «сразу», упёрся в часы, а не в чужое мнение: смена любого слагаемого без пересмотра отсрочки роняет пробу и называет оба числа.
func ResolveSecretCredentialTTL ¶
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 ¶
MissingChecks возвращает обязательные проверки, которых нет в объявлении реализации. Пустой ответ означает согласие с перечнем.
func MissingChecksExcept ¶
MissingChecksExcept возвращает обязательные проверки, которых нет ни в объявлении реализации, ни в перечне её объявленных отступлений.
Пустой ответ означает: всякая обязательная проверка либо исполняется, либо её неисполнение НАЗВАНО с причиной. Отступление с пустой причиной не засчитывается — иначе поле стало бы способом снять требование молча.
type Deviation ¶
Deviation — обязательная проверка, которую поверхность НЕ исполняет, вместе с причиной.
Причина обязательна и не бывает «пока»: отступление без причины неотличимо от пропуска, а «сделаем потом» — тот самый отложенный долг, за который никто не отвечает. Причина обязана называть, ЧТО делает эту проверку неприменимой здесь, а не то, когда её собираются добавить.