Yubikey U2F on a Tornado Web Framework

Jared Messenger
Ten Dozen
Published in
8 min readMay 12, 2016

A few years ago my gmail account was hacked and the adversary sent a phishing email to every single one of my contacts. Old high school coaches, college professors, family members and even my boss received a well crafted phishing email. If it wasn’t for a bunch of message delivery failed attempts from dead accounts, I might have never learned that my account was compromised. The steps I took to remedy the situation were typical, changed my password and added two-factor authentication. This was well before gmail supported apps and OAuth2, but if your account has been compromised, I would also revoke all apps that have access to your account.

When I was setting up our administrative console (enables administrators to edit rosters, stats and schedules) I wanted to make sure we were proactive in preventing unauthorized access. While there is no financial gain in accessing our console, a disgruntled fan could potentially change scores and stats which would lead to queries and articles containing incorrect information. The first proactive step was requiring all accounts to use two-factor authentication. Which meant implementing more than the typical username & password.

Intro to two-factor

If you’re unfamiliar with two-factor authentication, also known as 2-step verification or 2FA. It’s often something that is known, such as a password, and something that you posses, such as a mobile phone or usb stick. Example; if you enable two-factor on Twitter, Twitter will send you an SMS text message with a unique token every time you try to login. This unique token is referred to as a one-time password. Another well known implementation of the one-time password is the time based one-time password. Typically you have a very short amount of time, often only 30 seconds, to enter six numbers before that one-time password expires and a new one is created.

While these standard two-factor solutions add an extra level of security, they can fail a well designed phishing scheme. Let’s assume you received an email from Paypal saying there appears to be abnormal activity on your account, click here to view it. You click the link and Paypal’s login screen appears. You proceed to type your username and password, then you type in your two-factor token. Once you do this, you realize the site looks like Paypal, but it’s not actually Paypal. Now an adversary, that designed a website to look just like Paypal, can simply take all the credentials you just provided and sign into the real Paypal site. An adversary can now withdraw money and do all sorts of malicious activity with your account.

Yubikey

At first, I wasn’t too worried about an elaborate phishing attempt, it’s just our credibility at stake. Then one day while I was sitting in a meeting I saw a distinguished engineer tap a usb stick to login to Github. I asked if it was a new type of RSA SecurID. He told me it was a Yubikey. I google’d it and bought one immediately. I’m not going to go into the details of everything it can do, I’m going to focus on the FIDO U2F feature. The FIDO U2F paradigm (without getting into the weeds of cryptography, symmetric, and asymmetric keys) is a challenge created by the server, sent to the browser, then it’s read and signed by the Yubikey. In other words, a user types in their typical username & password combination, the server first validates the password and then generates a random string of characters, called a challenge, that the user’s Yubikey signs. This digital signature is sent back to the server where it can be verified against the challenge the server originally sent. This paradigm makes it much more difficult for an adversary to compromise.

Implementing FIDO U2F

Seeing several libraries available and being pretty knowledgeable in the esoteric field of cryptography, I estimated half a day to implement the FIDO U2F paradigm. Five hours into day two, pulling my hair out, I realized I grossly overestimated the state of the open source libraries. I will try to find time to contribute to the libraries, until then, hopefully this will help guide everyone else through the code landmines that cost me hours of time.

First off, you need to use SSL, even for development on your localhost. To do this, we’re just going to create a self signed certificate and add it to our OSX keychain (if you don’t add it to the keychain, you will get a bunch of warnings in chrome).

openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout localhost.key -out localhost.crt -subj ‘/CN=localhost’

In our Tornado application, we need to load our certificate and key we just created to enable ssl.

ssl_options = {“certfile”: os.path.join(os.path.dirname(__file__), “localhost.crt”),“keyfile”: os.path.join(os.path.dirname(__file__), “localhost.key”)}http_server = tornado.httpserver.HTTPServer(app,
ssl_options=ssl_options)

Now the fun part. The biggest annoyance was the number of inconsistencies between the code and documentation. Specifically the JavaScript API library. The u2f.sign and u2f.register (the only two functions you will use) take a different number of arguments than the examples and documentation says. If you’re receiving an error like requestId not found, it means you’re following the documentation, which says nothing thing about passing the appId as an argument to the function. The error should really be “missing or malformed appId”, which according to the documentation, isn’t even an optional parameter to pass in. Enough ranting, we’re going to ignore the documentation and implement our code based on what we can infer from debugging the library code. We’re also going to interface directly with the u2_v2 python module and not the u2f wrapper methods (the u2f wrapper has a few annoying bugs with it).

pip install python-u2flib-server

In python, import the u2f_v2.

from u2flib_server import u2f_v2

Registering

To login using your second factor Yubikey, you first need to register your device (usb stick) with the reliant party (your website). Since we require two-factor for all accounts, we’re going to require device registration on account creation. On the server, we need to create a register request with the appId, which we’ll use our domain name for. The result will be passed to the HTML template and handled on the client side in JavaScript.

domain = “{protocol}://{host}”.format(
protocol=self.request.protocol, host=self.request.host
)
register_request = u2f_v2.start_register(domain)

In JavaScript, the u2f.register takes the appId (domain name) the register request (created by the server) and a list of already known devices. Since the user is creating their account, there aren’t any known devices by the reliant party. If you want users to add multiple devices, you should pass in an array of their known devices to prevent registering the same device twice.

<script src=”/static/js/u2f-api.js”></script>
<script>
var requests = {% raw register_request %};
var appId;
var registerRequests = new Array();
var i;
for (i in requests) {
appId = requests[i].appId;
registerRequests.push({version: requests[i].version,
challenge: requests[i].challenge});
}
u2f.register(appId, [requests], [], function(resp) {
var form = document.getElementById(‘create-form’);
form.token_response.value = JSON.stringify(resp);
if(resp.errorCode) {
alert(“U2F failed with error: “ + resp.errorCode);
return;
}
})
</script>

Handling the POST method is straightforward. You pass in the original register_request and the u2f_response. The returned device object should be saved to your database.

u2f_response = self.get_argument(“token_response”) 
device, attestation_cert = u2f_v2.complete_register(register_request,
u2f_response)

A minor annoyance using Tornado (compared to Django) is that Tornado doesn’t have a concept of user sessions. For us to lookup the register_request we sent to the user, we need some way to store it so we can compare the signature to the request sent. You might be tempted to just pass the register_request back in the form, but THIS WOULD BE A SERIOUS VULNERABILITY. An adversary would just need to inject their own challenge into the form and sign that with their key. Your server wouldn’t know that the challenge was swapped out. What we’re going to do is have simple model that stores the user’s id (for when they’re authenticated) the U2F challenge information and an expiration. This session object’s id will be stored as a secure cookie for the user.

class HTTPSession(Base): 
__tablename__ = “http_session”
id = Column(GUID, primary_key=True, default=uuid.uuid4)
expires = Column(DateTime(timezone=True),
default=generate_expiration())
admin_id = Column(GUID, ForeignKey(“administrator.id”),
nullable=True)
two_factor_authenticated = Column(Boolean, nullable=False,
default=False)
# store u2f challenges sent to the user
u2f_tmp = Column(Text, nullable=True)
admin = relationship(‘Administrator’, foreign_keys=admin_id)
def is_expired(self):
“””
Check to see if the current session has expired or not
:returns: Bool
“””
now = datetime.datetime.now()
return self.expires > now

Login

We need to know the user’s id to get their registered devices before we can create a challenge for the Yubikey to sign. The login screen will be a typical username and password verification. If the username and password checks out, we create a sign request for all the devices registered by the user. Another gotcha was the challenge. I couldn’t get it to verify the challenges created by the library, in fact it raised an exception. So I created a shared challenge and passed it to all known devices.

devices = http_session.admin.u2f_keys 
devices_dict = [d.to_dict() for d in devices]
shared_challenge = os.urandom(32)
sign_requests = [u2f_v2.start_authenticate(d, shared_challenge) for
d in devices_dict]

Now the JavaScript u2f.sign function will receive a single challenge whether there is one device or nine associated with the user. I also stripped out all the other repetitive properties in the sign request (appId and version).

<script> 
var requests = {% raw requests %};
var appId;
var authenticateRequests = new Array();
var challenge;
var i;
for (i in requests) {
challenge = requests[i].challenge
appId = requests[i].appId;
authenticateRequests.push({version: requests[i].version, keyHandle: requests[i].keyHandle}); }

u2f.sign(appId, challenge, authenticateRequests, function(resp) {
var form = document.getElementById(‘verify-form’);
form.two_factor_data.value = JSON.stringify(resp);
if(resp.errorCode) {
alert(“U2F failed with error: “ + resp.errorCode); return; }
})
</script>

Unlike the register process (where we could directly drop the POST parameters into the u2_v2 methods) we need to slice up and perform surgery on the POST parameters for them to be accepted into the u2_v2.verify_authenticate method. The verify_authenticate method returns a tuple, touch asserted and a login counter. The touch asserted variable is a binary string which needs to be converted into a Boolean and the counter should be stored with the device model so we can verify it’s incrementing.

devices = http_session.admin.u2f_keys 
devices_dict = [d.to_dict() for d in devices]
authenticate_request = json.loads(http_session.u2f_tmp) two_factor_data = json.loads(self.get_argument(“two_factor_data”)) used_device = None
for d in devices_dict:
if d.get(“keyHandle”) == two_factor_data.get(“keyHandle”):
used_device = d
if not used_device:
self.render(“console/login.html”,
error=”Security Key Failed”)
return
used_request = None
for r in authenticate_request.get(“authenticateRequests”):
if used_device.get(“keyHandle”) == r.get(“keyHandle”):
used_request = r
try:
login_counter, touch_asserted = u2f_v2.verify_authenticate(used_device, used_request,
two_factor_data )
except InvalidSignature:
self.redirect(“console/login”,
error=”Invalid Signature”)
return
# touch asserted is a binary string,
# convert to int
touched = struct.unpack(‘B’, touch_asserted)[0]
if touched:
for d in devices:
if used_device.get(“keyHandle”) == d.key_handle:
if login_counter < d.login_counter:
raise ValueError(“Counter Off”)
d.login_counter = login_counter
http_session.two_factor_authenticated = True
self.redirect("/admin")

At this point, everything should be working as designed. If you’re receiving an error, make sure the appId is correct and you’re running localhost from https and not the default http. If those checkout, here is the full list of U2F error codes.

For more code examples checkout my repositories on github.

--

--

Jared Messenger
Ten Dozen

Jared Messenger is an Angel Investor and iOS engineer. Currently he is focused on computer vision and machine learning models for mobile devices.