Module: Legion::Extensions::Llm::Fleet::TokenValidator
- Extended by:
- Logging::Helper
- Includes:
- Logging::Helper
- Defined in:
- lib/legion/extensions/llm/fleet/token_validator.rb
Overview
Verifies responder-side fleet JWTs and prevents replay on provider nodes.
Constant Summary collapse
- SCALAR_CLAIMS =
%i[ request_id correlation_id idempotency_key operation provider provider_instance model reply_to timeout_seconds expires_at ].freeze
- HASHABLE_CLAIMS =
%i[message_context params caller trace_context].freeze
- MAX_REPLAY_ENTRIES =
100_000
Class Method Summary collapse
- .accepted_issuer?(value) ⇒ Boolean
- .accepted_issuers ⇒ Object
- .active_replay?(entry, now) ⇒ Boolean
- .algorithm ⇒ Object
- .audience ⇒ Object
- .canonical_value(value) ⇒ Object
- .clock_skew_seconds ⇒ Object
- .content_hash(value) ⇒ Object
- .ensure_not_replayed!(jti) ⇒ Object
- .evict_oldest_replay_entries! ⇒ Object
- .issuer ⇒ Object
- .jwt_module ⇒ Object
- .mark_replay!(jti) ⇒ Object
- .purge_replay_cache! ⇒ Object
- .purge_replay_cache_locked!(now) ⇒ Object
- .release_replay!(jti) ⇒ Object
- .replay_entry(state, now = Time.now.to_i) ⇒ Object
- .replay_ttl_seconds ⇒ Object
- .reserve_replay!(jti) ⇒ Object
- .reset_replay_cache! ⇒ Object
- .signing_key ⇒ Object
- .symbolize_keys(hash) ⇒ Object
- .validate!(token:, envelope:, record_replay: true) ⇒ Object
- .validate_envelope_claims!(claims, envelope) ⇒ Object
-
.validate_exact_execution_claims!(claims, envelope) ⇒ Object
When the exact-offering marker is present on the envelope, both exact scalar claims must be signed into the verified JWT and match the envelope.
- .validate_registered_claims!(claims) ⇒ Object
- .validate_request_expiry!(claims) ⇒ Object
Class Method Details
.accepted_issuer?(value) ⇒ Boolean
177 178 179 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 177 def accepted_issuer?(value) accepted_issuers.map(&:to_s).include?(value.to_s) end |
.accepted_issuers ⇒ Object
181 182 183 184 185 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 181 def accepted_issuers issuers = Settings.value(:fleet, :auth, :accepted_issuers, default: [issuer]) issuers = [issuer] if Array(issuers).empty? Array(issuers) end |
.active_replay?(entry, now) ⇒ Boolean
164 165 166 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 164 def active_replay?(entry, now) entry && entry[:expires_at] > now end |
.algorithm ⇒ Object
199 200 201 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 199 def algorithm Settings.value(:fleet, :auth, :algorithm, default: 'HS256') end |
.audience ⇒ Object
195 196 197 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 195 def audience Settings.value(:fleet, :auth, :audience, default: 'lex-llm-fleet-worker') end |
.canonical_value(value) ⇒ Object
228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 228 def canonical_value(value) case value when Hash value.each_with_object({}) do |(key, child), result| result[key.to_s] = canonical_value(child) end.sort.to_h when Array value.map { |child| canonical_value(child) } when Symbol value.to_s else value end end |
.clock_skew_seconds ⇒ Object
187 188 189 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 187 def clock_skew_seconds Settings.value(:fleet, :auth, :max_clock_skew_seconds, default: 30).to_i end |
.content_hash(value) ⇒ Object
111 112 113 114 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 111 def content_hash(value) require 'digest' Digest::SHA256.hexdigest(canonical_value(value).to_s) end |
.ensure_not_replayed!(jti) ⇒ Object
140 141 142 143 144 145 146 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 140 def ensure_not_replayed!(jti) @replay_mutex.synchronize do now = Time.now.to_i purge_replay_cache_locked!(now) raise TokenError, 'fleet token replay detected' if active_replay?(@seen_jtis[jti.to_s], now) end end |
.evict_oldest_replay_entries! ⇒ Object
159 160 161 162 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 159 def evict_oldest_replay_entries! sorted = @seen_jtis.each_pair.sort_by { |_jti, entry| entry[:expires_at] } sorted.first(@seen_jtis.size - MAX_REPLAY_ENTRIES).each_key { |jti| @seen_jtis.delete(jti) } end |
.issuer ⇒ Object
191 192 193 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 191 def issuer Settings.value(:fleet, :auth, :issuer, default: 'legion-llm') end |
.jwt_module ⇒ Object
214 215 216 217 218 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 214 def jwt_module return ::Legion::Crypt::JWT if defined?(::Legion::Crypt::JWT) && ::Legion::Crypt::JWT.respond_to?(:verify) raise TokenError, 'Legion::Crypt::JWT.verify unavailable' end |
.mark_replay!(jti) ⇒ Object
127 128 129 130 131 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 127 def mark_replay!(jti) @replay_mutex.synchronize do @seen_jtis[jti.to_s] = replay_entry(:complete) end end |
.purge_replay_cache! ⇒ Object
148 149 150 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 148 def purge_replay_cache! @replay_mutex.synchronize { purge_replay_cache_locked!(Time.now.to_i) } end |
.purge_replay_cache_locked!(now) ⇒ Object
154 155 156 157 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 154 def purge_replay_cache_locked!(now) @seen_jtis.each_pair { |jti, entry| @seen_jtis.delete(jti) unless active_replay?(entry, now) } evict_oldest_replay_entries! if @seen_jtis.size > MAX_REPLAY_ENTRIES end |
.release_replay!(jti) ⇒ Object
133 134 135 136 137 138 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 133 def release_replay!(jti) @replay_mutex.synchronize do entry = @seen_jtis[jti.to_s] @seen_jtis.delete(jti.to_s) if entry.nil? || entry[:state] == :inflight end end |
.replay_entry(state, now = Time.now.to_i) ⇒ Object
168 169 170 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 168 def replay_entry(state, now = Time.now.to_i) { state: state, expires_at: now + replay_ttl_seconds } end |
.replay_ttl_seconds ⇒ Object
172 173 174 175 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 172 def replay_ttl_seconds ttl = Settings.value(:fleet, :auth, :replay_ttl_seconds, default: 600).to_i ttl.positive? ? ttl : 600 end |
.reserve_replay!(jti) ⇒ Object
116 117 118 119 120 121 122 123 124 125 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 116 def reserve_replay!(jti) @replay_mutex.synchronize do now = Time.now.to_i purge_replay_cache_locked!(now) existing = @seen_jtis[jti.to_s] raise TokenError, 'fleet token replay detected' if active_replay?(existing, now) @seen_jtis[jti.to_s] = replay_entry(:inflight, now) end end |
.reset_replay_cache! ⇒ Object
46 47 48 49 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 46 def reset_replay_cache! @seen_jtis = Concurrent::Map.new @replay_mutex = Mutex.new end |
.signing_key ⇒ Object
203 204 205 206 207 208 209 210 211 212 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 203 def signing_key return ::Legion::Crypt.cluster_secret if defined?(::Legion::Crypt) && ::Legion::Crypt.respond_to?(:cluster_secret) raise TokenError, 'no signing key available - Legion::Crypt not initialized' rescue TokenError raise rescue StandardError => e handle_exception(e, level: :warn, handled: false, operation: 'llm.fleet.token_validator.signing_key') raise TokenError, "no signing key available: #{e.}" end |
.symbolize_keys(hash) ⇒ Object
220 221 222 223 224 225 226 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 220 def symbolize_keys(hash) return {} unless hash.respond_to?(:each) hash.each_with_object({}) do |(key, value), result| result[key.respond_to?(:to_sym) ? key.to_sym : key] = value end end |
.validate!(token:, envelope:, record_replay: true) ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 24 def validate!(token:, envelope:, record_replay: true) raise TokenError, 'fleet token is required' if token.to_s.empty? claims = symbolize_keys(jwt_module.verify( token, verification_key: signing_key, issuer: issuer, algorithm: algorithm, verify_issuer: true )) validate_registered_claims!(claims) validate_request_expiry!(claims) validate_envelope_claims!(claims, symbolize_keys(envelope || {})) record_replay ? reserve_replay!(claims[:jti]) : ensure_not_replayed!(claims[:jti]) claims rescue TokenError raise rescue StandardError => e handle_exception(e, level: :warn, handled: false, operation: 'llm.fleet.token_validator.validate') raise TokenError, "fleet token verification failed: #{e.}" end |
.validate_envelope_claims!(claims, envelope) ⇒ Object
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 82 def validate_envelope_claims!(claims, envelope) SCALAR_CLAIMS.each do |key| expected = canonical_value(envelope[key]) actual = canonical_value(claims[key]) raise TokenError, "fleet token #{key} claim mismatch" unless actual == expected end HASHABLE_CLAIMS.each do |key| hash_key = :"#{key}_hash" expected_hash = content_hash(envelope[key]) actual_hash = claims[hash_key] || content_hash(claims[key]) raise TokenError, "fleet token #{key} hash mismatch" unless actual_hash == expected_hash end validate_exact_execution_claims!(claims, envelope) end |
.validate_exact_execution_claims!(claims, envelope) ⇒ Object
When the exact-offering marker is present on the envelope, both exact scalar claims must be signed into the verified JWT and match the envelope. An unsigned exact marker is never authoritative.
102 103 104 105 106 107 108 109 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 102 def validate_exact_execution_claims!(claims, envelope) return unless envelope[:execution_contract] == Protocol::EXACT_EXECUTION_CONTRACT Protocol::EXACT_SIGNED_SCALAR_CLAIMS.each do |key| raise TokenError, "fleet token missing signed #{key}" if claims[key].to_s.empty? raise TokenError, "fleet token #{key} claim mismatch" unless canonical_value(claims[key]) == canonical_value(envelope[key]) end end |
.validate_registered_claims!(claims) ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 51 def validate_registered_claims!(claims) now = Time.now.to_i raise TokenError, 'fleet token issuer mismatch' unless accepted_issuer?(claims[:iss]) raise TokenError, 'fleet token audience mismatch' unless claims[:aud].to_s == audience if claims[:exp].nil? || claims[:exp].to_i + clock_skew_seconds <= now raise TokenError, 'fleet token expired' end if claims[:nbf].nil? || claims[:nbf].to_i - clock_skew_seconds > now raise TokenError, 'fleet token not yet valid' end raise TokenError, 'fleet token missing jti' if claims[:jti].to_s.empty? end |
.validate_request_expiry!(claims) ⇒ Object
66 67 68 69 70 71 72 73 74 |
# File 'lib/legion/extensions/llm/fleet/token_validator.rb', line 66 def validate_request_expiry!(claims) expires_at = claims[:expires_at] raise TokenError, 'fleet request expires_at is required' if expires_at.to_s.empty? expires = Time.iso8601(expires_at.to_s) raise TokenError, 'fleet request expired' if expires + clock_skew_seconds <= Time.now.utc rescue ArgumentError raise TokenError, 'fleet request expires_at is invalid' end |