- Investigate using the krb5 responder for driving the PAM conversation with OTPs
- Interaction with SSSD, GDM, OTP and GNOME Keyring
One-Time-Passwords (OTP) are typically used as one part of a Two-Factor authentication (2FA). In most cases the second factor is a long term password of the user. In general the combined two factors are seen by the client as an opaque blob which is send together with the user name to an authentication service with decides if the authentication is correct or not and returns the result to the client.
In modern environments there are a number of use cases where only the long term password factor is needed:
- offline authentication: 2FA authentication service is not available and long term password should be compared with the hashed copy
- unlocking key-rings, encrypted devices: the long term password is used to protect key-rings, files or file-systems; changes of the long term password should change the encryption key for the other uses as well
The most obvious way to get the long term password is to prompt the user separately for the long term password and the OTP. But for historical reasons most user interfaces and more important most network protocols expect a single string as password. While it would be possible to modify the local user interfaces (graphical and command line) to handle the two factors separately it is next to impossible to cover all network protocols. This means we always have to handle the case where both factors are only available in a single string as a fallback and having both factors already split will just be a special case.
It is common practice that when using 2FA with a long term password and an OTP (mostly generated by a hardware token) the long term password factor is entered first at the password prompt and then the OTP. In enterprise environments typically one brand of hardware tokens is used which means that the OTP factor has a known number of characters. With this kind of information the combined strings can be split in long term and OTP factor heuristically. Additionally if the combined string was split successfully once the size of the OTP factor can be stored in the cache because in general it will not change and long as the same hardware token is used.
If splitting is not possible other consumers of the long term password should be made aware that they have to request the password on their own if needed.
Since OTPs can only be used once SSSD must avoid to use it a second time. This currently is the case when changing the long term password via Kerberos. After the password is changed successfully SSSD tries to get a fresh TGT with the new password. This should not happen in the case an OTP is used instead the user should be asked to enter a fresh password (with the new long term password and a valid OTP).
If the combined password cannot be split into long term and OTP factor and new PAM response type should be send back to pam_sss to indicate that the combined password should be removed so that other pam modules (pam-gnome-keyring, pam_mount) cannot use it anymore and have to request a password on their own. It might be a good idea to allow an optional string in this new PAM response. If the password can be split the string can contain the long term password which should replace the combined password on the PAM stack. As an alternative an unsigned integer which indicated where the long term password ends can be used instead. Then pam_sss will shorten the combined password to the given length.
In sssd-1.12, we will remove the password from the PAM stack when OTP is used to make sure use-cases like gnome-keyring are not broken. We would need more time for implementation of heuristic and proper testing. Currently, the krb5_child returns that an OTP was used during authentication (details in function parse_krb5_child_response). This OTP flag is used just in the function krb5_auth_done. We will pass OTP flag to the pam responder (sssd_pam) and from pam responder to the pam client (pam_sss.so). If the pam client detects that OTP was used it will remove password from auth_token.
In the OTP case asking for a new TGT can easily be skipped in krb5_child but this will leave the user with an invalid TGT. A new PAM response type should indicate that this is the case. It has to be evaluated if it is possible with PAM to get a fresh authentication of the user if only a message indicating that the TGT might be invalid and should be refreshed manually can be send to the user.
There are a number of Heuristics that can be employed depending on the type of tokens used and whether the type is known or not.
If the token type is known and has a fixed number of characters then the client can be simply configured with a hard number and the string provided by the user simply split counting from the end. knowing the minimum password length for the actual user password can also allow to detect errors in entering the credentials (like forgetting to actually type the OTP) so that a partial input can be discarded immediately.
For example if we know the OTP is 6 chars and the password policy says that a password must be at least 8 chars long then an input of “CoolPassword” would be immediately discarded as it is not at least 14 chars long (min 8 + 6 for the OTP), while “CoolPassword123456” would be split in “CoolPassword” and “123456”
If it is know that the token’s OTP is always only digits then this fact can be used to split the last part of the string when the exact length is not known. This heuristic alone is not sufficient as the user password may contain trailing digits, however it may be combined with other heuristics to improve them.
If the length of the OTP is know or is within a small range (for example only 6 or 8 digit tokens are available) then strings like “CoolPassword123456” or “CoolPassword1234567” are easy to split. The first is “CoolPassword”+”123456” the second is “CoolPassword1”+”234567”. A string like “CoolPassword1234T56” would be easy to discard as faulty as there is a non-digit withing the last 6 chars, however “CoolPassword12345678 may be split both as “CoolPassword12” “345678” or “CoolPassword” “12345678” and would need additional heuristics.
If the one shot heuristic fails we can store hints that may allow us to succeed in successive authentication attempts. If we do not know what is the token type, length or constants on character types used we can perform a wild guess as the first authentication attempt by applying a “most common” guess set and then store a number of hashes that will aid us in a follow-up attempt.
For example, we have no knowledge of the token and the user enters “CoolPassword12345678”. We can assume a default heuristic of “6 digits OTP” and this would split the string in “CoolPassword12” + “345678”, however if we got it wrong and the token was 8 digits long (“12345678”) then we would fail auth and be none the wiser.
Therefore before sending out the authentication request we gather and store heuristics of our own in the form of hashes. We will assume that in a 2FA environment there exist reasonable minimum limits to both the Password and the OTP length, for example we assume that passwords are minimum 6 chars long and OTPs are minimum 6 chars long.
with this assumption we store a hints list of salted hashes of the following strings: :
"CoolPassword12" "CoolPassword1" "CoolPassword" "CoolPasswor" "CoolPasswo" "CoolPassw" "CoolPass" "CoolPa"
The order in which the strings are stored on the system may be intentionally scrambled to prevent faster offline attacks on the shorter hash.
If auth succeeds we discard the hints and store only “CoolPassword12” as an offline password hash. If auth fails we keep the hints for the next try and just fail authentication (yes even if the Password+OTP was right).
On the following authentication attempt we can use the hints to aid us in properly splitting the OTP. If the user provides us “CoolPassword19283745” we can try to match it against the hints first splitting and hashing backwards from longest to shortest. We’ll try “CoolPassword19” and it will fail to match then we’ll try “CoolPassword1” and it will match one of the hints, so we will assume that as the password and take the remainder (9283745) as the OTP.
A user mistyping the password on the first attempt may end up causing a mismatch in a later attempt, we can only clear the previous hints and fail the auth until the user gets 2 consecutive attempts with different OTPs right. Once one authentication attempt succeed and we store the offline password hash we’ll have a stronger hint for the future as we’ll have a known good hash. We can also save, as a hint the OTP length, and check it does not vary in following successful authentication attempts, if ti varies then we’ll change the hint to explicitly list the known good length used so far as future hints.
If the user changes its password on a different system or uses multiple OTP tokens of varying type the hints may not work well. So if an offline password hash does not match what the user types we need to start from scratch, and try our best guess as well as save a list of hints.
This process is not fool proof, but given enough hints (either discovered or provided as known facts) we could have a system that works reasonably well.
Sumit Bose <firstname.lastname@example.org>