Testing FIDO/U2F Two Factor Authentication
This month, I was tasked with implementing FIDO/U2F support for two factor authentication for a client. U2F two factor authentication requires a FIDO/U2F hardware key that you insert into your devices USB port and press a button to complete two factor authentication. There are many different vendors that make these devices, such as Yubikey etc. Thanks to the excellent Authen::U2F module by CPAN author ROBN, and Google's u2f-api.js library, implementing support for this proved to be fairly straightforward, but the process for doing this is not the point of this blog post.
The point of this blog post is that, at the time, there was no easy way to write any kind of automated tests for this. As a result, I ended up writing Authen::U2F::Tester. Authen::U2F::Tester acts like a virtual hardware device to complete FIDO/U2F registration and authentication (known as signing in U2F terms) requests.
In order to avoid a lengthy blog post about the details about how U2F works, I'm only going to show the test side of things and I am greatly simplifying the explanation about how FIDO/U2F works here. But the basics are that, when registering a new device, the site generates a string known as a "challenge". The U2F key takes this challenge, and returns a binary string that contains a "key handle" that identifies the keypair it generated for this site, as well as the public key part of the keypair, plus a signature. The server verify's this data and stores the key handle and public key.
Here is an example of how to test the registration process, using WWW::Mechanize and Authen::U2F::Tester:
my $tester = Authen::U2F::Tester->new( key_file => '/path/to/key.pem', cert_file => '/path/to/cert.pem');
...
# somehow extract the challenge and registered key handles from the server
# either by extracting from page content, or, via an ajax request. Assume we
# did an ajax request here and the content is JSON containing:
# appId, challenge, registeredKeys
my $json = decode_json($mech->content);my $res = $tester->register(
$json->{appId},
$json->{challenge},
$json->{registeredKeys}->@*);ok $res->is_success, 'U2F registration request was successful';
...
$mech->set_fields(
name => 'Test U2F key',
registration_data => $res->registration_data,
client_data => $res->client_data);$mech->click_ok;
If this form submit succeeds, then the tester has been registered as a FIDO/U2F key and can be used to complete a U2F authentication request. Here is a code snippet showing how to handle the authentication (or signing) request:
$json = decode_json($mech->content);
# JSON content here contains
# appId
# challenge
# registeredKeys
my $res = $tester->sign(
$json->{appId},
$json->{challenge},
$json->{registeredKeys}->@*);
ok $res->is_success, 'U2F signing request was successful';
$mech->set_fields(
key_handle => $res->key_handle,
signature_data => $res->signature_data,
client_data => $res->client_data);
$mech->click_ok;
The way the Authen::U2F::Tester handles keypairs is that the key handle is actually the private key of the keypair that was generated during register(), but the private key is encrypted using the Tester's own private key. From what I have seen, most physical U2F keys use some variation of this. The reasoning is that the U2F device does not need to store or remember the private keys. The device knows that it owns the key handle if it can be decrypted using the U2F devices' own private key. The FIDO/U2F docs call this a "wrapped" key handle. For the purposes of Authen::U2F::Tester, this means that all test registrations will remain valid unless you use a different private key and X.509 certificate to initialize the tester. For those that require the ability to invalidate registrations in the tester itself, I have written Authen::U2F::Tester in such a way that additional key storage schemes can be created in the future (e.g.: in-memory hash, or database backed).
Hopefully this helps others that need to create automated tests for FIDO/U2F authentication.
Ugh, I typo'd the signing/authentication request line. It should be:
$tester->sign(
$json->{appId},
$json->{challenge},
$json->{registereKeys}->@*);
I guess posts can't be edited here?
You can edit posts. Click "Post" on the top bar to go to your dashboard, hope that your session works correctly, probably log in again, and there you can see your entries for editing.