#1149305 MFA auth receipt and TOTP are not invalidated after use, allowing replay attacks

Package:
src:keystone
Source:
src:keystone
Submitter:
Thomas Goirand
Date:
2026-10-02 09:03:03 UTC
Severity:
normal
Tags:
#1149305#5
Date:
2026-09-29 10:20:13 UTC
From:
To:
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).

#1149305#8
Date:
2026-09-29 10:47:56 UTC
From:
To:
Hello,

Bug #1149305 in keystone reported by you has been fixed in the
Git repository and is awaiting an upload. You can see the commit
message below and you can check the diff of the fix at:

https://salsa.debian.org/openstack-team/services/keystone/-/commit/78608aa89ca09127da29e6a7363928ceebdd266c
------------------------------------------------------------------------
* OSSN-0109: identity, credentials: require MFA re-verification for
    sensitive actions:
    - OSSN-0109_lp-2157347_invalidate_MFA_auth_receipts_and_TOTP_passco....patch
      (Closes: #1149305)
    - OSSN-0109_identity_credentials_require_MFA_re-verification_for_se....patch
      (Closes: #1149304)
------------------------------------------------------------------------

(this message was generated automatically)
-- 
Greetings

https://bugs.debian.org/1149305

#1149305#15
Date:
2026-09-29 11:19:41 UTC
From:
To:
We believe that the bug you reported is fixed in the latest version of
keystone, which is due to be installed in the Debian FTP archive.

A summary of the changes between this version and the previous one is
attached.

Thank you for reporting the bug, which will now be closed.  If you
have further comments please address them to 1149305@bugs.debian.org,
and the maintainer will reopen the bug report if appropriate.

Debian distribution maintenance software
pp.
Thomas Goirand <zigo@debian.org> (supplier of updated keystone package)

(This message was generated automatically at their request; if you
believe that there is a problem with it please contact the archive
administrators by mailing ftpmaster@ftp-master.debian.org)
Format: 1.8
Date: Tue, 29 Sep 2026 12:33:16 +0200
Source: keystone
Architecture: source
Version: 2:30.0.0~rc1-2
Distribution: unstable
Urgency: medium
Maintainer: Debian OpenStack <team+openstack@tracker.debian.org>
Changed-By: Thomas Goirand <zigo@debian.org>
Closes: 1149304 1149305
Changes:
 keystone (2:30.0.0~rc1-2) unstable; urgency=medium
 .
   * Add Type=notify to keystone service unit.
   * OSSN-0109: identity, credentials: require MFA re-verification for
     sensitive actions:
     - OSSN-0109_lp-2157347_invalidate_MFA_auth_receipts_and_TOTP_passco....patch
       (Closes: #1149305)
     - OSSN-0109_identity_credentials_require_MFA_re-verification_for_se....patch
       (Closes: #1149304)
Checksums-Sha1:
 3e098755d3bc1e97b80bf726fea1a6bacc7d1d88 3486 keystone_30.0.0~rc1-2.dsc
 7878d63215f687f9d25a6e1b01f7c19c9f896b3b 63956 keystone_30.0.0~rc1-2.debian.tar.xz
 faa280d0214e2668b67bdd029e5c18fe8206c61e 17057 keystone_30.0.0~rc1-2_amd64.buildinfo
Checksums-Sha256:
 7e399b271ff81f726268248bd5983b138d9b3a4aa50576e2a3983aa02267de8a 3486 keystone_30.0.0~rc1-2.dsc
 e6d16e3ab6355f9d41d203de8dcca64b311745a55f543c80801df89988f2c8cb 63956 keystone_30.0.0~rc1-2.debian.tar.xz
 e339bf4731eca20c9c8ac07651933eefc8f60b03fb606029f40a500d0b16f3fb 17057 keystone_30.0.0~rc1-2_amd64.buildinfo
Files:
 a298cecf9e54263e312dd4080e8fe369 3486 net optional keystone_30.0.0~rc1-2.dsc
 d9ef4444329f4868db641fbc2353d87e 63956 net optional keystone_30.0.0~rc1-2.debian.tar.xz
 6683f4f8f0953dc6bab1dc6730505c3c 17057 net optional keystone_30.0.0~rc1-2_amd64.buildinfo
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCgAdFiEEoLGp81CJVhMOekJc1BatFaxrQ/4FAmq7l1wACgkQ1BatFaxr
Q/424Q//Vc24oqTInOY+tAnrPgEAUdqhfiSZwVDepBrv3DTNiLMoJsRGxEQFFO4L
pxx4mWpdsK6Hdr0ypl8W8MHltMPC8erMJWxis5DMof5pjEV7OCCqpEOCROZSZugG
atnglQnToR0Y06p3iBi3+6pbxKrTJzTnXkpnN+7wG0UnPdv0rUd1pVSWdeZEplJV
fbewmDHYENxMCfMjLfQhvt30FAMSYBcN/gZ2C5QKSTLbU2DUBRtVhxsqJiPHx3qs
y4B9o8IkQXIrKNwm8sTjT6XcOts+Q559eOMy4eOnvX37XHeO3zefEPHxPaLXZcL8
hk3nIwv9uFmn38VTnoTj/dFu7koojR9TSVqNG0tIsY5rG55c+32a8jmBGKPzCSk7
jfTAHIHzJ6drYQF2pvlbvWqftK+PR+RTbjxLcCQR7L7YpxXcDb/eviJocedhEK2Y
ag75mhx2qHwT2d1i7vk203uK3UXJ2T7OcG4HFKXXDQATspP2xlqvbbRzRYN5pWho
+J2vO6VaObhLNNI8QKRwbPZLF+WckL8ytbvXVvYPsALEYAqWWavdzHxavipzafTW
i9PSlZvSUqtkZfvJGVndA7r1shwTGKxd4PKFMFMYBPLFQ/EAif+sL2M6+TpTPy+T
aAxGnuiAQBiwcaCmPeoJ2/GDlWYv+Lg5kZ+pjor23AAvfkKc2fo=
=x4dt
-----END PGP SIGNATURE-----