OTP Related Improvements
========================

Related Ticket(s):

-  `Investigate using the krb5 responder for driving the PAM
   conversation with OTPs <https://pagure.io/SSSD/sssd/issue/2335>`__
-  `Interaction with SSSD, GDM, OTP and GNOME
   Keyring <https://pagure.io/SSSD/sssd/issue/2278>`__

Problem Statement
-----------------

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).

Overview of the Solution
------------------------

Implementation details
----------------------

Removing password with OTP factor from the PAM stack
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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.

Do not request a new TGT after a successful password change
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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.

Heuristics
----------

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.

Hints to split the combined password
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Fixed number of characters in the OTP
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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"

Fixed set of characters in the OTP
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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.

Previous authentication memory
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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.

How to test
-----------

Author(s)
~~~~~~~~~

Sumit Bose <`sbose@redhat.com <mailto:sbose@redhat.com>`__>