lablink_allocator_service.secret_hash¶
lablink_allocator_service.secret_hash
¶
Argon2 hashing for at-rest deployment/client secrets (SR-F14).
Attributes¶
REGISTER_TOKEN_SUBJECT = '__register_token__'
module-attribute
¶
SECRET_HASH_CACHE_MAX_SIZE = 1024
module-attribute
¶
SECRET_HASH_NEGATIVE_TTL_S = 30.0
module-attribute
¶
SECRET_HASH_POSITIVE_TTL_S = 60.0
module-attribute
¶
Classes¶
TtlLruCache(*, ttl, max_size, negative_ttl=None)
¶
Thread-safe TTL + LRU cache with an optional per-key version guard.
Both caches in this module are instances of this: the
client_secret_hash cache (keyed by hostname, distinct positive and
negative TTLs, version guard in use — see
VmDatabase.get_client_secret_hash) and the verify-result cache
(keyed by (subject, token-fingerprint), successes only, no
version guard).
negative_ttl applies to a stored None and defaults to
ttl. The secret-hash cache sets it shorter so a freshly
registered host becomes auth-able within seconds without a
register-time invalidate, and so a repeatedly probed unknown
hostname doesn't linger.
Race-against-rotation: :meth:get returns a per-key version token
that :meth:put re-checks under the lock. If :meth:invalidate ran
in between — a concurrent register_client rotating the secret
while a stale SELECT was in flight — the mismatch rejects the stale
write, so the next call re-queries and picks up the new value. Pass
expected_version=None to skip the check; the verify cache has
nothing to race with, since it stores a result it derived itself
rather than a value it fetched.
Bounded size: an OrderedDict with LRU eviction on insert beyond
max_size. Both get and put move the entry to the
most-recently-used end. The bound is a DoS guard against unique-key
floods, not a working-set sizing knob.
Source code in packages/allocator/src/lablink_allocator_service/secret_hash.py
Methods:¶
clear()
¶
get(key)
¶
Return (hit, value, version).
hit=False means the caller must re-fetch; pass version
back to :meth:put so a concurrent invalidate between get and
put rejects the stale write.
Source code in packages/allocator/src/lablink_allocator_service/secret_hash.py
invalidate(key)
¶
invalidate_where(predicate)
¶
Drop every entry whose key satisfies predicate.
Unlike :meth:invalidate this bumps no version — it exists for
the verify cache, whose compound key means one subject owns an
unbounded set of keys and which runs no version guard anyway.
Source code in packages/allocator/src/lablink_allocator_service/secret_hash.py
put(key, value, expected_version=None)
¶
Store value for key if no invalidate raced.
Returns True if the entry was written, False if a
concurrent :meth:invalidate bumped the version between the
caller's :meth:get and this put.
Source code in packages/allocator/src/lablink_allocator_service/secret_hash.py
Functions:¶
clear_verify_cache()
¶
hash_secret(plaintext)
¶
Return an argon2 hash string for plaintext (salted, unique per call).
invalidate_verify(subject)
¶
Drop every cached verify result for subject.
Call whenever the stored hash for a subject changes — e.g., on
register_client (rotation) or unregister_client (removal),
the same hook the secret-hash cache uses, so a rotated secret does
not keep accepting old tokens up to the TTL.
Source code in packages/allocator/src/lablink_allocator_service/secret_hash.py
verify_secret(plaintext, hashed)
¶
True iff plaintext matches hashed. False on any mismatch or
malformed hash (never raises).
Source code in packages/allocator/src/lablink_allocator_service/secret_hash.py
verify_secret_cached(subject, plaintext, hashed)
¶
Same contract as :func:verify_secret but memoizes successes by
(subject, sha256(plaintext)) for ~60 s.
The first call pays the argon2 verify cost; subsequent calls with
the same (subject, plaintext) within the TTL return True without
running argon2. Failures are never cached, so a wrong token always
pays the verify cost — there's no fast path for attackers, and a
wrong token cannot poison an entry either: it has a different
fingerprint, so it lands on a different key.
Use a subject the caller has already identified out-of-band
(e.g., hostname from the request body, client_id from the URL), so
a cache hit doesn't silently accept a token across identities.