google / google-authenticator-libpam

Apache License 2.0
1.76k stars 281 forks source link

yubikey compatibility #186

Closed anarcat closed 3 years ago

anarcat commented 3 years ago

In theory, this plugin should work with hardware tokens like the Yubikey, which supports OATH+HOTP. In practice, it doesn't work out of the box: the secret generated by the script is base32-encoded, while ykpersonalize expects a hex string. The secret is also 16 bytes instead of the expected 20.

Has anyone tried to make this plugin work with yubikeys? So far my attempts are failing and i'm just wondering if i'm missing anything obvious. So far the YK folks have been quite helpful and simply said to "pad the value with zeros", but so far it doesn't work for me. This is the (python) code I have so far to convert between the two:

        remainder = len(secret_b32) % 8
        if remainder > 0:
            # XXX: assume 6 chars are missing, the actual padding may vary:
            # https://tools.ietf.org/html/rfc3548#section-5
            secret_b32 += "======"
        secret = base64.b32decode(secret_b32)
        if len(secret) < 20:
            # pad to 20 bytes
            secret += b'0' * (20 - len(secret))
        print(binascii.hexlify(secret).decode('ascii'))

I am setting the google authenticator plugin side with:

google-authenticator -c -Q NONE -r 1 -R 30 -e 1 -w 3

And writing it to the yubikey with:

ykpersonalize -1 -o oath-hotp -o oath-hotp8 -o append-cr -a

The error I'm getting from the pam module is:

Invalid verification code for [USERNAME]

The module is hooked into sshd with:

auth required pam_google_authenticator.so no_increment_hotp nullok debug

I'm successfully using liboath-pam right now with the yubikey, FWIW.

ThomasHabets commented 3 years ago

I've not used yubikeys in hotp mode, but does your command line giving both -o oath-hotp and -o oath-hotp8 mean that you're trying to use 8 digit codes? This PAM module doesn't support that.

anarcat commented 3 years ago

is that something which could be fixed or maybe configurable?

and yes, codes do seem to be 8 digits...

ThomasHabets commented 3 years ago

Feature request https://github.com/google/google-authenticator-libpam/issues/20

If you remove -o oath-hotp8, does it just work?

anarcat commented 3 years ago

If you remove -o oath-hotp8, does it just work?

nope. it sends 6 digits, but somehow i still get "Invalid verification code for"... maybe i'm doing something wrong when converting the secret?

ThomasHabets commented 3 years ago

Dunno. It should work.

I think the secret conversion must be the problem, yes. It really should work. I say that because GA obviously works with Google accounts that use TOTPs, and presumably HOTP should work just as well. And yubikey oath has been used to log in to Google with TOTP. So it almost logically follows that yubikey oath should work with HOTP.

To test this you should scan the QR code you get, and convert to yk, and play around until you get the same code. So that you don't have to involve PAM and logs and stuff.

Generate a few codes, though, to make sure it's not just a counter off-by-one or something.

anarcat commented 3 years ago

To test this you should scan the QR code you get, and convert to yk, and play around until you get the same code.

Note sure how to do this... what does the qrcode have to do with anything?

Did you see the python code I pasted? does it seem reasonable to you?

ThomasHabets commented 3 years ago

The QR code lets you load it in the app. The app is compatible with the PAM module. If you get the same code in the app and with the yubikey, then you have succeeded.

I just ran the code, and no that doesn't look right. I think you want b'\x00', not b'0'.

#!/usr/bin/python3                                                                                                                          
import base64
import binascii
def foo(s):
  remainder = len(s) % 8
  if remainder > 0:
    s += "======"
  secret = base64.b32decode(s)
  if len(secret) < 20:
    secret += b'\x00' * (20 - len(secret))
  return binascii.hexlify(secret).decode('ascii')
print(foo('N' * 26))

This gives 6b5ad6b5ad6b5ad6b5ad6b5ad6b5ad6b00000000 instead of 6b5ad6b5ad6b5ad6b5ad6b5ad6b5ad6b30303030.

anarcat commented 3 years ago

duuuh of course! and it works now, thanks!! that's awesome! closing this. :)

(although maybe docs could be updated to show how it works... you'll forgive me but i'll try to update my howto first ;)

note: magic script lives here now https://gitlab.com/anarcat/scripts/blob/master/oath-convert

ThomasHabets commented 3 years ago

Looking at the howto. I prefer PIV mode instead of going via gpg, myself. I have some articles about that on https://blog.habets.se/

anarcat commented 3 years ago

i think I tried that but gave up. I don't remember exactly why, but I seem to remember it was conflicting with the 2FA HOTP application or something. Also, it's kind of convenient to have the authentication certificate embedded in my OpenPGP key, as people can just fetch and verify the key the normal way and use:

gpg --export-ssh-keys anarcat@debian.org

... to get my SSH key. I guess it would be possible to encapsulate in OpenPGP in the PIV method as well, but that seems rather... complicated.