Source: keystone Version: 2:27.0.0-3+deb13u5 Severity: important Tags: patch security X-Debbugs-Cc: Debian Security Team <[email protected]>
As per launchpad bug at: https://bugs.launchpad.net/keystone/+bug/2157347 Two related single-use enforcement gaps exist in Keystone's MFA authentication flow. They are independent bugs that compound each other. Bug 1 — MFA auth receipt not consumed after use ------------------------------------------------ MFA auth receipts, introduced in the Stein release (bp mfa-auth-receipt), are never invalidated after being used to complete authentication. In keystone/receipt/handlers.py, extract_receipt() validates the receipt but never revokes it: def extract_receipt(auth_context): receipt_id = flask.request.headers.get( authorization.AUTH_RECEIPT_HEADER, None) if receipt_id: receipt = PROVIDERS.receipt_provider_api.validate_receipt( receipt_id) if auth_context['user_id'] != receipt.user_id: raise exception.ReceiptNotFound(...) return receipt After authentication.py issues a full token using the receipt, there is no call to consume it. The receipt (a Fernet token) is stateless, so without explicit tracking it can be presented any number of times until it expires (default 300 seconds / 5 minutes). The original spec (mfa-auth-receipt.rst, Security Impact section) acknowledged this: "an eventual action we should take is to do some form of receipt revocations when turned into a token." That action was never taken. Bug 2 — TOTP passcode not invalidated after use ------------------------------------------------ The TOTP authentication plugin (keystone/auth/plugins/totp.py) never records which passcodes have been accepted. The same 6-digit code can be submitted multiple times within its validity window and each submission authenticates successfully (line 109): if auth_passcode in generated_passcodes: valid_passcode = True By default Keystone accepts the current 30-second window plus one previous window ([totp] included_previous_windows = 1), giving an effective replay window of up to 60 seconds. RFC 6238 Section 5.2 recommends that servers mark a passcode as used and reject reuse within the same time step. This bug affects both the challenge-response flow (password → receipt → TOTP) and the all-upfront flow (password + TOTP in one request). Impact ------ Bug 2 (TOTP replay): an attacker who intercepts a TOTP code can resubmit it within the ~60-second validity window to obtain additional tokens. Bug 1 (receipt replay): one completed victim authentication can be reused multiple times within the 5-minute receipt TTL. The receipt appears in the 401 response header; each time the attacker can observe a fresh TOTP code within that window, they can pair it with the captured receipt to obtain another full token — without the victim authenticating again. The receipt is notable because it appears in response headers, which are more commonly logged than request bodies. An attacker with access to server-side logs or response traffic can capture a receipt without capturing the password. The spec compared receipt replay to "users can already have multiple tokens." That is wrong: every legitimate token required the victim to complete MFA independently. Receipt replay allows the attacker to complete additional MFA authentications on the victim's behalf from a single captured receipt. Verified on devstack (Keystone 29.x, 2026.1 cycle): Bug 1 (receipt replay across TOTP windows): - Step 1: password → receipt. Complete MFA with receipt + TOTP code T1 → token. - Waited for TOTP window to rotate (31s). New code T2 generated (T1 ≠ T2). - Same receipt + T2 → HTTP 201, distinct valid token. - Receipt was not consumed after first use. Bug 2 (TOTP replay within window): - Same TOTP code submitted 3 times in succession. - All 3 returned HTTP 201 with distinct, independently valid tokens. Severity: high. MFA is deployed when passwords are considered insufficient. These bugs together reduce hardware MFA from a persistent barrier requiring physical presence on every auth, to a one-time obstacle: the attacker needs only one observation of decrypted traffic. Bug 1 alone reduces it to one observation per 5-minute receipt window. Bug 2 alone reduces it to multiple tokens from one observation within 60 seconds. Suggested fix ------------- Both bugs can be fixed using oslo.cache with appropriate TTLs: Bug 1: after a receipt is used to issue a full token, store the receipt ID in cache with TTL = receipt expiry (300 seconds). Reject any subsequent request presenting a cached receipt ID. Bug 2: after a TOTP passcode is accepted, store "totp_used:{user_id}:{passcode}" in cache with TTL = (included_previous_windows + 1) * 30 seconds (default 60 seconds). Reject any subsequent request for the same user+passcode combination. If the cache backend is the null backend (caching disabled), log a warning that replay protection is degraded. Both fixes require no database migration and are straightforward to backport. The same oslo.cache pattern is proposed for the related OAuth1 nonce replay fix (LP#2157345). References: - Original spec (Stein): https://specs.openstack.org/openstack/keystone-specs/specs/keystone/stein/mfa-auth-receipt.html - Implementing commit: d9e6c1d4d ("Implement auth receipts spec") - RFC 6238 Section 5.2 (TOTP single-use recommendation) - keystone/receipt/handlers.py, keystone/auth/plugins/totp.py Affected versions: All Keystone releases since Stein (19.0.0) for Bug 1; Rocky (17.0.0+) for Bug 2 (when TOTP is enabled).

