diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index 68f144c089..ca5b7a3332 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -288,7 +288,7 @@ RESET_PASSWD_CODE_LIVE_MINUTES = 180
 REGISTER_EMAIL_CONFIRM = false
 ; Disallow registration, only allow admins to create accounts.
 DISABLE_REGISTRATION = false
-; Allow registration only using third part services, it works only when DISABLE_REGISTRATION is false 
+; Allow registration only using third part services, it works only when DISABLE_REGISTRATION is false
 ALLOW_ONLY_EXTERNAL_REGISTRATION = false
 ; User must sign in to view anything.
 REQUIRE_SIGNIN_VIEW = false
@@ -570,6 +570,14 @@ MAX_RESPONSE_ITEMS = 50
 LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR
 NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어
 
+[U2F]
+; Two Factor authentication with security keys
+; https://developers.yubico.com/U2F/App_ID.html
+APP_ID = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s
+; Comma seperated list of truisted facets
+TRUSTED_FACETS = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s
+
+
 ; Used for datetimepicker
 [i18n.datelang]
 en-US = en
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 2a245ffd00..53c4639eb0 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -272,6 +272,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
 - `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view.
 - `GC_ARGS`: **\<empty\>**: Arguments for command `git gc`, e.g. `--aggressive --auto`.
 
+## U2F (`U2F`)
+- `APP_ID`: **`ROOT_URL`**: Declares the facet of the application. Requires HTTPS.
+- `TRUSTED_FACETS`: List of additional facets which are trusted. This is not support by all browsers.
+
 ## Markup (`markup`)
 
 Gitea can support Markup using external tools. The example below will add a markup named `asciidoc`.
diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md
index db0d3b5553..75553a89cd 100644
--- a/docs/content/doc/features/comparison.en-us.md
+++ b/docs/content/doc/features/comparison.en-us.md
@@ -535,6 +535,15 @@ _Symbols used in table:_
       <td>✓</td>
       <td>✓</td>
     </tr>
+    <tr>
+      <td>FIDO U2F (2FA)</td>
+      <td>✓</td>
+      <td>✘</td>
+      <td>✓</td>
+      <td>✓</td>
+      <td>✓</td>
+      <td>✓</td>
+    </tr>
     <tr>
       <td>Webhook support</td>
       <td>✓</td>
diff --git a/models/error.go b/models/error.go
index cdb18d23ce..316f8c34bd 100644
--- a/models/error.go
+++ b/models/error.go
@@ -1237,3 +1237,25 @@ func IsErrExternalLoginUserNotExist(err error) bool {
 func (err ErrExternalLoginUserNotExist) Error() string {
 	return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID)
 }
+
+// ____ ________________________________              .__          __                 __  .__
+// |    |   \_____  \_   _____/\______   \ ____   ____ |__| _______/  |_____________ _/  |_|__| ____   ____
+// |    |   //  ____/|    __)   |       _// __ \ / ___\|  |/  ___/\   __\_  __ \__  \\   __\  |/  _ \ /    \
+// |    |  //       \|     \    |    |   \  ___// /_/  >  |\___ \  |  |  |  | \// __ \|  | |  (  <_> )   |  \
+// |______/ \_______ \___  /    |____|_  /\___  >___  /|__/____  > |__|  |__|  (____  /__| |__|\____/|___|  /
+// \/   \/            \/     \/_____/         \/                   \/                    \/
+
+// ErrU2FRegistrationNotExist represents a "ErrU2FRegistrationNotExist" kind of error.
+type ErrU2FRegistrationNotExist struct {
+	ID int64
+}
+
+func (err ErrU2FRegistrationNotExist) Error() string {
+	return fmt.Sprintf("U2F registration does not exist [id: %d]", err.ID)
+}
+
+// IsErrU2FRegistrationNotExist checks if an error is a ErrU2FRegistrationNotExist.
+func IsErrU2FRegistrationNotExist(err error) bool {
+	_, ok := err.(ErrU2FRegistrationNotExist)
+	return ok
+}
diff --git a/models/fixtures/u2f_registration.yml b/models/fixtures/u2f_registration.yml
new file mode 100644
index 0000000000..4a9d1d9624
--- /dev/null
+++ b/models/fixtures/u2f_registration.yml
@@ -0,0 +1,7 @@
+-
+  id: 1
+  name: "U2F Key"
+  user_id: 1
+  counter: 0
+  created_unix: 946684800
+  updated_unix: 946684800
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index e85da8de79..7c90f1eb1f 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -182,6 +182,8 @@ var migrations = []Migration{
 	NewMigration("add language column for user setting", addLanguageSetting),
 	// v64 -> v65
 	NewMigration("add multiple assignees", addMultipleAssignees),
+	// v65 -> v66
+	NewMigration("add u2f", addU2FReg),
 }
 
 // Migrate database to current version
diff --git a/models/migrations/v65.go b/models/migrations/v65.go
new file mode 100644
index 0000000000..f73e632877
--- /dev/null
+++ b/models/migrations/v65.go
@@ -0,0 +1,19 @@
+package migrations
+
+import (
+	"code.gitea.io/gitea/modules/util"
+	"github.com/go-xorm/xorm"
+)
+
+func addU2FReg(x *xorm.Engine) error {
+	type U2FRegistration struct {
+		ID          int64 `xorm:"pk autoincr"`
+		Name        string
+		UserID      int64 `xorm:"INDEX"`
+		Raw         []byte
+		Counter     uint32
+		CreatedUnix util.TimeStamp `xorm:"INDEX created"`
+		UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
+	}
+	return x.Sync2(&U2FRegistration{})
+}
diff --git a/models/models.go b/models/models.go
index 9213cd3b79..ddf784deee 100644
--- a/models/models.go
+++ b/models/models.go
@@ -120,6 +120,7 @@ func init() {
 		new(LFSLock),
 		new(Reaction),
 		new(IssueAssignees),
+		new(U2FRegistration),
 	)
 
 	gonicNames := []string{"SSL", "UID"}
diff --git a/models/u2f.go b/models/u2f.go
new file mode 100644
index 0000000000..a7b031d9e2
--- /dev/null
+++ b/models/u2f.go
@@ -0,0 +1,120 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
+
+	"github.com/tstranex/u2f"
+)
+
+// U2FRegistration represents the registration data and counter of a security key
+type U2FRegistration struct {
+	ID          int64 `xorm:"pk autoincr"`
+	Name        string
+	UserID      int64 `xorm:"INDEX"`
+	Raw         []byte
+	Counter     uint32
+	CreatedUnix util.TimeStamp `xorm:"INDEX created"`
+	UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
+}
+
+// TableName returns a better table name for U2FRegistration
+func (reg U2FRegistration) TableName() string {
+	return "u2f_registration"
+}
+
+// Parse will convert the db entry U2FRegistration to an u2f.Registration struct
+func (reg *U2FRegistration) Parse() (*u2f.Registration, error) {
+	r := new(u2f.Registration)
+	return r, r.UnmarshalBinary(reg.Raw)
+}
+
+func (reg *U2FRegistration) updateCounter(e Engine) error {
+	_, err := e.ID(reg.ID).Cols("counter").Update(reg)
+	return err
+}
+
+// UpdateCounter will update the database value of counter
+func (reg *U2FRegistration) UpdateCounter() error {
+	return reg.updateCounter(x)
+}
+
+// U2FRegistrationList is a list of *U2FRegistration
+type U2FRegistrationList []*U2FRegistration
+
+// ToRegistrations will convert all U2FRegistrations to u2f.Registrations
+func (list U2FRegistrationList) ToRegistrations() []u2f.Registration {
+	regs := make([]u2f.Registration, len(list))
+	for _, reg := range list {
+		r, err := reg.Parse()
+		if err != nil {
+			log.Fatal(4, "parsing u2f registration: %v", err)
+			continue
+		}
+		regs = append(regs, *r)
+	}
+
+	return regs
+}
+
+func getU2FRegistrationsByUID(e Engine, uid int64) (U2FRegistrationList, error) {
+	regs := make(U2FRegistrationList, 0)
+	return regs, e.Where("user_id = ?", uid).Find(&regs)
+}
+
+// GetU2FRegistrationByID returns U2F registration by id
+func GetU2FRegistrationByID(id int64) (*U2FRegistration, error) {
+	return getU2FRegistrationByID(x, id)
+}
+
+func getU2FRegistrationByID(e Engine, id int64) (*U2FRegistration, error) {
+	reg := new(U2FRegistration)
+	if found, err := e.ID(id).Get(reg); err != nil {
+		return nil, err
+	} else if !found {
+		return nil, ErrU2FRegistrationNotExist{ID: id}
+	}
+	return reg, nil
+}
+
+// GetU2FRegistrationsByUID returns all U2F registrations of the given user
+func GetU2FRegistrationsByUID(uid int64) (U2FRegistrationList, error) {
+	return getU2FRegistrationsByUID(x, uid)
+}
+
+func createRegistration(e Engine, user *User, name string, reg *u2f.Registration) (*U2FRegistration, error) {
+	raw, err := reg.MarshalBinary()
+	if err != nil {
+		return nil, err
+	}
+	r := &U2FRegistration{
+		UserID:  user.ID,
+		Name:    name,
+		Counter: 0,
+		Raw:     raw,
+	}
+	_, err = e.InsertOne(r)
+	if err != nil {
+		return nil, err
+	}
+	return r, nil
+}
+
+// CreateRegistration will create a new U2FRegistration from the given Registration
+func CreateRegistration(user *User, name string, reg *u2f.Registration) (*U2FRegistration, error) {
+	return createRegistration(x, user, name, reg)
+}
+
+// DeleteRegistration will delete U2FRegistration
+func DeleteRegistration(reg *U2FRegistration) error {
+	return deleteRegistration(x, reg)
+}
+
+func deleteRegistration(e Engine, reg *U2FRegistration) error {
+	_, err := e.Delete(reg)
+	return err
+}
diff --git a/models/u2f_test.go b/models/u2f_test.go
new file mode 100644
index 0000000000..6d6cd495ca
--- /dev/null
+++ b/models/u2f_test.go
@@ -0,0 +1,61 @@
+package models
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/tstranex/u2f"
+)
+
+func TestGetU2FRegistrationByID(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	res, err := GetU2FRegistrationByID(1)
+	assert.NoError(t, err)
+	assert.Equal(t, "U2F Key", res.Name)
+
+	_, err = GetU2FRegistrationByID(342432)
+	assert.Error(t, err)
+	assert.True(t, IsErrU2FRegistrationNotExist(err))
+}
+
+func TestGetU2FRegistrationsByUID(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+
+	res, err := GetU2FRegistrationsByUID(1)
+	assert.NoError(t, err)
+	assert.Len(t, res, 1)
+	assert.Equal(t, "U2F Key", res[0].Name)
+}
+
+func TestU2FRegistration_TableName(t *testing.T) {
+	assert.Equal(t, "u2f_registration", U2FRegistration{}.TableName())
+}
+
+func TestU2FRegistration_UpdateCounter(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	reg := AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)
+	reg.Counter = 1
+	assert.NoError(t, reg.UpdateCounter())
+	AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 1})
+}
+
+func TestCreateRegistration(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
+
+	res, err := CreateRegistration(user, "U2F Created Key", &u2f.Registration{Raw: []byte("Test")})
+	assert.NoError(t, err)
+	assert.Equal(t, "U2F Created Key", res.Name)
+	assert.Equal(t, []byte("Test"), res.Raw)
+
+	AssertExistsIf(t, true, &U2FRegistration{Name: "U2F Created Key", UserID: user.ID})
+}
+
+func TestDeleteRegistration(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	reg := AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration)
+
+	assert.NoError(t, DeleteRegistration(reg))
+	AssertNotExistsBean(t, &U2FRegistration{ID: 1})
+}
diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go
index 1b00f62634..0c342df86a 100644
--- a/modules/auth/user_form.go
+++ b/modules/auth/user_form.go
@@ -211,3 +211,23 @@ type TwoFactorScratchAuthForm struct {
 func (f *TwoFactorScratchAuthForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
 	return validate(errs, ctx.Data, f, ctx.Locale)
 }
+
+// U2FRegistrationForm for reserving an U2F name
+type U2FRegistrationForm struct {
+	Name string `binding:"Required"`
+}
+
+// Validate valideates the fields
+func (f *U2FRegistrationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+	return validate(errs, ctx.Data, f, ctx.Locale)
+}
+
+// U2FDeleteForm for deleting U2F keys
+type U2FDeleteForm struct {
+	ID int64 `binding:"Required"`
+}
+
+// Validate valideates the fields
+func (f *U2FDeleteForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+	return validate(errs, ctx.Data, f, ctx.Locale)
+}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 9f20d955cc..adf4bb74fd 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -521,6 +521,11 @@ var (
 		MaxResponseItems:      50,
 	}
 
+	U2F = struct {
+		AppID         string
+		TrustedFacets []string
+	}{}
+
 	// I18n settings
 	Langs     []string
 	Names     []string
@@ -1135,6 +1140,9 @@ func NewContext() {
 			IsInputFile:    sec.Key("IS_INPUT_FILE").MustBool(false),
 		})
 	}
+	sec = Cfg.Section("U2F")
+	U2F.TrustedFacets, _ = shellquote.Split(sec.Key("TRUSTED_FACETS").MustString(strings.TrimRight(AppURL, "/")))
+	U2F.AppID = sec.Key("APP_ID").MustString(strings.TrimRight(AppURL, "/"))
 }
 
 // Service settings
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 30f07dc131..e587c27d68 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -31,6 +31,19 @@ twofa = Two-Factor Authentication
 twofa_scratch = Two-Factor Scratch Code
 passcode = Passcode
 
+u2f_insert_key = Insert your security key
+u2f_sign_in = Press the button on your security key. If you can't find a button, re-insert it.
+u2f_press_button = Please press the button on your security key…
+u2f_use_twofa = Use a two-factor code from your phone
+u2f_error = We can't read your security key!
+u2f_unsupported_browser = Your browser don't support U2F keys. Please try another browser.
+u2f_error_1 = An unknown error occured. Please retry.
+u2f_error_2 = Please make sure that you're using an encrypted connection (https://) and visiting the correct URL.
+u2f_error_3 = The server could not proceed your request.
+u2f_error_4 = The presented key is not eligible for this request. If you try to register it, make sure that the key isn't already registered.
+u2f_error_5 = Timeout reached before your key could be read. Please reload to retry.
+u2f_reload = Reload
+
 repository = Repository
 organization = Organization
 mirror = Mirror
@@ -320,6 +333,7 @@ twofa = Two-Factor Authentication
 account_link = Linked Accounts
 organization = Organizations
 uid = Uid
+u2f = Security Keys
 
 public_profile = Public Profile
 profile_desc = Your email address will be used for notifications and other operations.
@@ -449,6 +463,14 @@ then_enter_passcode = And enter the passcode shown in the application:
 passcode_invalid = The passcode is incorrect. Try again.
 twofa_enrolled = Your account has been enrolled into two-factor authentication. Store your scratch token (%s) in a safe place as it is only shown once!
 
+u2f_desc = Security keys are hardware devices containing cryptograhic keys. They could be used for two factor authentication. The security key must support the <a href="https://fidoalliance.org/">FIDO U2F</a> standard.
+u2f_require_twofa = Two-Factor-Authentication must be enrolled in order to use security keys.
+u2f_register_key = Add Security Key
+u2f_nickname = Nickname
+u2f_press_button = Press the button on your security key to register it.
+u2f_delete_key = Remove Security Key
+u2f_delete_key_desc= If you remove a security key you cannot login with it anymore. Are you sure?
+
 manage_account_links = Manage Linked Accounts
 manage_account_links_desc = These external accounts are linked to your Gitea account.
 account_links_not_available = There are currently no external accounts linked to your Gitea account.
diff --git a/public/js/index.js b/public/js/index.js
index e826c2f3f3..e98a3fe6de 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -1432,6 +1432,130 @@ function initCodeView() {
     }
 }
 
+function initU2FAuth() {
+    if($('#wait-for-key').length === 0) {
+        return
+    }
+    u2fApi.ensureSupport()
+        .then(function () {
+            $.getJSON('/user/u2f/challenge').success(function(req) {
+                u2fApi.sign(req.appId, req.challenge, req.registeredKeys, 30)
+                    .then(u2fSigned)
+                    .catch(function (err) {
+                        if(err === undefined) {
+                            u2fError(1);
+                            return
+                        }
+                        u2fError(err.metaData.code);
+                    });
+            });
+        }).catch(function () {
+            // Fallback in case browser do not support U2F
+            window.location.href = "/user/two_factor"
+        })
+}
+function u2fSigned(resp) {
+    $.ajax({
+        url:'/user/u2f/sign',
+        type:"POST",
+        headers: {"X-Csrf-Token": csrf},
+        data: JSON.stringify(resp),
+        contentType:"application/json; charset=utf-8",
+    }).done(function(res){
+        window.location.replace(res);
+    }).fail(function (xhr, textStatus) {
+        u2fError(1);
+    });
+}
+
+function u2fRegistered(resp) {
+    if (checkError(resp)) {
+        return;
+    }
+    $.ajax({
+        url:'/user/settings/security/u2f/register',
+        type:"POST",
+        headers: {"X-Csrf-Token": csrf},
+        data: JSON.stringify(resp),
+        contentType:"application/json; charset=utf-8",
+        success: function(){
+            window.location.reload();
+        },
+        fail: function (xhr, textStatus) {
+            u2fError(1);
+        }
+    });
+}
+
+function checkError(resp) {
+    if (!('errorCode' in resp)) {
+        return false;
+    }
+    if (resp.errorCode === 0) {
+        return false;
+    }
+    u2fError(resp.errorCode);
+    return true;
+}
+
+
+function u2fError(errorType) {
+    var u2fErrors = {
+        'browser': $('#unsupported-browser'),
+        1: $('#u2f-error-1'),
+        2: $('#u2f-error-2'),
+        3: $('#u2f-error-3'),
+        4: $('#u2f-error-4'),
+        5: $('.u2f-error-5')
+    };
+    u2fErrors[errorType].removeClass('hide');
+    for(var type in u2fErrors){
+        if(type != errorType){
+            u2fErrors[type].addClass('hide');
+        }
+    }
+    $('#u2f-error').modal('show');
+}
+
+function initU2FRegister() {
+    $('#register-device').modal({allowMultiple: false});
+    $('#u2f-error').modal({allowMultiple: false});
+    $('#register-security-key').on('click', function(e) {
+        e.preventDefault();
+        u2fApi.ensureSupport()
+            .then(u2fRegisterRequest)
+            .catch(function() {
+                u2fError('browser');
+            })
+    })
+}
+
+function u2fRegisterRequest() {
+    $.post("/user/settings/security/u2f/request_register", {
+        "_csrf": csrf,
+        "name": $('#nickname').val()
+    }).success(function(req) {
+        $("#nickname").closest("div.field").removeClass("error");
+        $('#register-device').modal('show');
+        if(req.registeredKeys === null) {
+            req.registeredKeys = []
+        }
+        u2fApi.register(req.appId, req.registerRequests, req.registeredKeys, 30)
+            .then(u2fRegistered)
+            .catch(function (reason) {
+                if(reason === undefined) {
+                    u2fError(1);
+                    return
+                }
+                u2fError(reason.metaData.code);
+            });
+    }).fail(function(xhr, status, error) {
+        if(xhr.status === 409) {
+            $("#nickname").closest("div.field").addClass("error");
+        }
+    });
+}
+
 $(document).ready(function () {
     csrf = $('meta[name=_csrf]').attr("content");
     suburl = $('meta[name=_suburl]').attr("content");
@@ -1643,6 +1767,8 @@ $(document).ready(function () {
     initCtrlEnterSubmit();
     initNavbarContentToggle();
     initTopicbar();
+    initU2FAuth();
+    initU2FRegister();
 
     // Repo clone url.
     if ($('#repo-clone-url').length > 0) {
@@ -2201,7 +2327,7 @@ function initTopicbar() {
                     return
                 }
                 var topicArray = topics.split(",");
-                
+
                 var last = viewDiv.children("a").last();
                 for (var i=0;i < topicArray.length; i++) {
                     $('<div class="ui green basic label topic" style="cursor:pointer;">'+topicArray[i]+'</div>').insertBefore(last)
diff --git a/public/vendor/librejs.html b/public/vendor/librejs.html
index 68586064c6..e24bb9c3eb 100644
--- a/public/vendor/librejs.html
+++ b/public/vendor/librejs.html
@@ -110,6 +110,11 @@
           <td><a href="https://github.com/mozilla/pdf.js/blob/master/LICENSE">Apache-2.0-only</a></td>
           <td><a href="https://github.com/mozilla/pdf.js/archive/v1.4.20.tar.gz">pdf.js-v1.4.20.tar.gz</a></td>
         </tr>
+		<tr>
+		  <td><a href="/vendor/plugins/u2f/">u2f-api</a></td>
+		  <td><a href="https://github.com/go-gitea/u2f-api/blob/master/LICENSE">Expat</a></td>
+		  <td><a href="https://github.com/go-gitea/u2f-api/archive/v1.0.8.zip">u2f-api-1.0.8.zip</a></td>
+		</tr>
         <tr>
           <td><a href="/vendor/assets/font-awesome/fonts/">font-awesome - fonts</a></td>
           <td><a href="http://fontawesome.io/license/">OFL</a></td>
diff --git a/public/vendor/plugins/u2f/index.js b/public/vendor/plugins/u2f/index.js
new file mode 100644
index 0000000000..1413f51d7b
--- /dev/null
+++ b/public/vendor/plugins/u2f/index.js
@@ -0,0 +1 @@
+this.u2fApi=function(e){var t={};function r(o){if(t[o])return t[o].exports;var n=t[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=e,r.c=t,r.d=function(e,t,o){r.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:o})},r.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(e,t,r){"use strict";var o,n=n||{};e.exports=n,n.EXTENSION_ID="kmendfapggjehodndflmmgagdbamhnfd",n.MessageTypes={U2F_REGISTER_REQUEST:"u2f_register_request",U2F_REGISTER_RESPONSE:"u2f_register_response",U2F_SIGN_REQUEST:"u2f_sign_request",U2F_SIGN_RESPONSE:"u2f_sign_response",U2F_GET_API_VERSION_REQUEST:"u2f_get_api_version_request",U2F_GET_API_VERSION_RESPONSE:"u2f_get_api_version_response"},n.ErrorCodes={OK:0,OTHER_ERROR:1,BAD_REQUEST:2,CONFIGURATION_UNSUPPORTED:3,DEVICE_INELIGIBLE:4,TIMEOUT:5},n.U2fRequest,n.U2fResponse,n.Error,n.Transport,n.Transports,n.SignRequest,n.SignResponse,n.RegisterRequest,n.RegisterResponse,n.RegisteredKey,n.GetJsApiVersionResponse,n.getMessagePort=function(e){if("undefined"!=typeof chrome&&chrome.runtime){var t={type:n.MessageTypes.U2F_SIGN_REQUEST,signRequests:[]};chrome.runtime.sendMessage(n.EXTENSION_ID,t,function(){chrome.runtime.lastError?n.getIframePort_(e):n.getChromeRuntimePort_(e)})}else n.isAndroidChrome_()?n.getAuthenticatorPort_(e):n.isIosChrome_()?n.getIosPort_(e):n.getIframePort_(e)},n.isAndroidChrome_=function(){var e=navigator.userAgent;return-1!=e.indexOf("Chrome")&&-1!=e.indexOf("Android")},n.isIosChrome_=function(){return["iPhone","iPad","iPod"].indexOf(navigator.platform)>-1},n.getChromeRuntimePort_=function(e){var t=chrome.runtime.connect(n.EXTENSION_ID,{includeTlsChannelId:!0});setTimeout(function(){e(new n.WrappedChromeRuntimePort_(t))},0)},n.getAuthenticatorPort_=function(e){setTimeout(function(){e(new n.WrappedAuthenticatorPort_)},0)},n.getIosPort_=function(e){setTimeout(function(){e(new n.WrappedIosPort_)},0)},n.WrappedChromeRuntimePort_=function(e){this.port_=e},n.formatSignRequest_=function(e,t,r,s,i){if(void 0===o||o<1.1){for(var a=[],u=0;u<r.length;u++)a[u]={version:r[u].version,challenge:t,keyHandle:r[u].keyHandle,appId:e};return{type:n.MessageTypes.U2F_SIGN_REQUEST,signRequests:a,timeoutSeconds:s,requestId:i}}return{type:n.MessageTypes.U2F_SIGN_REQUEST,appId:e,challenge:t,registeredKeys:r,timeoutSeconds:s,requestId:i}},n.formatRegisterRequest_=function(e,t,r,s,i){if(void 0===o||o<1.1){for(var a=0;a<r.length;a++)r[a].appId=e;var u=[];for(a=0;a<t.length;a++)u[a]={version:t[a].version,challenge:r[0],keyHandle:t[a].keyHandle,appId:e};return{type:n.MessageTypes.U2F_REGISTER_REQUEST,signRequests:u,registerRequests:r,timeoutSeconds:s,requestId:i}}return{type:n.MessageTypes.U2F_REGISTER_REQUEST,appId:e,registerRequests:r,registeredKeys:t,timeoutSeconds:s,requestId:i}},n.WrappedChromeRuntimePort_.prototype.postMessage=function(e){this.port_.postMessage(e)},n.WrappedChromeRuntimePort_.prototype.addEventListener=function(e,t){var r=e.toLowerCase();"message"==r||"onmessage"==r?this.port_.onMessage.addListener(function(e){t({data:e})}):console.error("WrappedChromeRuntimePort only supports onMessage")},n.WrappedAuthenticatorPort_=function(){this.requestId_=-1,this.requestObject_=null},n.WrappedAuthenticatorPort_.prototype.postMessage=function(e){var t=n.WrappedAuthenticatorPort_.INTENT_URL_BASE_+";S.request="+encodeURIComponent(JSON.stringify(e))+";end";document.location=t},n.WrappedAuthenticatorPort_.prototype.getPortType=function(){return"WrappedAuthenticatorPort_"},n.WrappedAuthenticatorPort_.prototype.addEventListener=function(e,t){if("message"==e.toLowerCase()){window.addEventListener("message",this.onRequestUpdate_.bind(this,t),!1)}else console.error("WrappedAuthenticatorPort only supports message")},n.WrappedAuthenticatorPort_.prototype.onRequestUpdate_=function(e,t){var r=JSON.parse(t.data),o=(r.intentURL,r.errorCode,null);r.hasOwnProperty("data")&&(o=JSON.parse(r.data)),e({data:o})},n.WrappedAuthenticatorPort_.INTENT_URL_BASE_="intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE",n.WrappedIosPort_=function(){},n.WrappedIosPort_.prototype.postMessage=function(e){var t=JSON.stringify(e),r="u2f://auth?"+encodeURI(t);location.replace(r)},n.WrappedIosPort_.prototype.getPortType=function(){return"WrappedIosPort_"},n.WrappedIosPort_.prototype.addEventListener=function(e,t){"message"!==e.toLowerCase()&&console.error("WrappedIosPort only supports message")},n.getIframePort_=function(e){var t="chrome-extension://"+n.EXTENSION_ID,r=document.createElement("iframe");r.src=t+"/u2f-comms.html",r.setAttribute("style","display:none"),document.body.appendChild(r);var o=new MessageChannel,s=function(t){"ready"==t.data?(o.port1.removeEventListener("message",s),e(o.port1)):console.error('First event on iframe port was not "ready"')};o.port1.addEventListener("message",s),o.port1.start(),r.addEventListener("load",function(){r.contentWindow.postMessage("init",t,[o.port2])})},n.EXTENSION_TIMEOUT_SEC=30,n.port_=null,n.waitingForPort_=[],n.reqCounter_=0,n.callbackMap_={},n.getPortSingleton_=function(e){n.port_?e(n.port_):(0==n.waitingForPort_.length&&n.getMessagePort(function(e){for(n.port_=e,n.port_.addEventListener("message",n.responseHandler_);n.waitingForPort_.length;)n.waitingForPort_.shift()(n.port_)}),n.waitingForPort_.push(e))},n.responseHandler_=function(e){var t=e.data,r=t.requestId;if(r&&n.callbackMap_[r]){var o=n.callbackMap_[r];delete n.callbackMap_[r],o(t.responseData)}else console.error("Unknown or missing requestId in response.")},n.isSupported=function(e){var t=!1;function r(r){t||(t=!0,e(r))}n.getApiVersion(function(e){o=void 0===e.js_api_version?0:e.js_api_version,r(!0)}),setTimeout(r.bind(null,!1),500)},n.sign=function(e,t,r,s,i){void 0===o?n.getApiVersion(function(a){o=void 0===a.js_api_version?0:a.js_api_version,console.log("Extension JS API Version: ",o),n.sendSignRequest(e,t,r,s,i)}):n.sendSignRequest(e,t,r,s,i)},n.sendSignRequest=function(e,t,r,o,s){n.getPortSingleton_(function(i){var a=++n.reqCounter_;n.callbackMap_[a]=o;var u=void 0!==s?s:n.EXTENSION_TIMEOUT_SEC,p=n.formatSignRequest_(e,t,r,u,a);i.postMessage(p)})},n.register=function(e,t,r,s,i){void 0===o?n.getApiVersion(function(a){o=void 0===a.js_api_version?0:a.js_api_version,console.log("Extension JS API Version: ",o),n.sendRegisterRequest(e,t,r,s,i)}):n.sendRegisterRequest(e,t,r,s,i)},n.sendRegisterRequest=function(e,t,r,o,s){n.getPortSingleton_(function(i){var a=++n.reqCounter_;n.callbackMap_[a]=o;var u=void 0!==s?s:n.EXTENSION_TIMEOUT_SEC,p=n.formatRegisterRequest_(e,r,t,u,a);i.postMessage(p)})},n.getApiVersion=function(e,t){n.getPortSingleton_(function(r){if(r.getPortType){var o;switch(r.getPortType()){case"WrappedIosPort_":case"WrappedAuthenticatorPort_":o=1.1;break;default:o=0}e({js_api_version:o})}else{var s=++n.reqCounter_;n.callbackMap_[s]=e;var i={type:n.MessageTypes.U2F_GET_API_VERSION_REQUEST,timeoutSeconds:void 0!==t?t:n.EXTENSION_TIMEOUT_SEC,requestId:s};r.postMessage(i)}})}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=r(0),n="undefined"!=typeof navigator&&!!navigator.userAgent,s=n&&navigator.userAgent.match(/Safari\//)&&!navigator.userAgent.match(/Chrome\//),i=n&&navigator.userAgent.match(/Edge\/1[2345]/),a=null;function u(){return a||(a=new Promise(function(e,t){function r(){e({u2f:null})}return n?s?r():void 0!==window.u2f&&"function"==typeof window.u2f.sign?e({u2f:window.u2f}):i?r():"http:"===location.protocol?r():"undefined"==typeof MessageChannel?r():void o.isSupported(function(t){t?e({u2f:o}):r()}):r()})),a}function p(e,r){var o=null!=r?r.errorCode:1,n=t.ErrorNames[""+o],s=new Error(e);return s.metaData={type:n,code:o},s}function d(e){if(!e.u2f){if("http:"===location.protocol)throw new Error("U2F isn't supported over http, only https");throw new Error("U2F not supported")}}t.ErrorCodes={OK:0,OTHER_ERROR:1,BAD_REQUEST:2,CONFIGURATION_UNSUPPORTED:3,DEVICE_INELIGIBLE:4,TIMEOUT:5},t.ErrorNames={0:"OK",1:"OTHER_ERROR",2:"BAD_REQUEST",3:"CONFIGURATION_UNSUPPORTED",4:"DEVICE_INELIGIBLE",5:"TIMEOUT"},t.isSupported=function(){return u().then(function(e){return!!e.u2f})},t.ensureSupport=function(){return u().then(d)},t.register=function(e,t,r,o){return Array.isArray(t)||(t=[t]),"number"==typeof r&&void 0===o&&(o=r,r=null),r||(r=[]),u().then(function(n){d(n);var s=n.u2f;return new Promise(function(n,i){s.register(e,t,r,function(e){e.errorCode?i(p("Registration failed",e)):(delete e.errorCode,n(e))},o)})})},t.sign=function(e,t,r,o){return Array.isArray(r)||(r=[r]),u().then(function(n){d(n);var s=n.u2f;return new Promise(function(n,i){s.sign(e,t,r,function(e){e.errorCode?i(p("Sign failed",e)):(delete e.errorCode,n(e))},o)})})}}]);
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 07be6653a6..1585a0876d 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -5,6 +5,8 @@
 package routes
 
 import (
+	"encoding/gob"
+	"net/http"
 	"os"
 	"path"
 	"time"
@@ -37,12 +39,13 @@ import (
 	"github.com/go-macaron/i18n"
 	"github.com/go-macaron/session"
 	"github.com/go-macaron/toolbox"
+	"github.com/tstranex/u2f"
 	"gopkg.in/macaron.v1"
-	"net/http"
 )
 
 // NewMacaron initializes Macaron instance.
 func NewMacaron() *macaron.Macaron {
+	gob.Register(&u2f.Challenge{})
 	m := macaron.New()
 	if !setting.DisableRouterLog {
 		m.Use(macaron.Logger())
@@ -214,6 +217,12 @@ func RegisterRoutes(m *macaron.Macaron) {
 			m.Get("/scratch", user.TwoFactorScratch)
 			m.Post("/scratch", bindIgnErr(auth.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost)
 		})
+		m.Group("/u2f", func() {
+			m.Get("", user.U2F)
+			m.Get("/challenge", user.U2FChallenge)
+			m.Post("/sign", bindIgnErr(u2f.SignResponse{}), user.U2FSign)
+
+		})
 	}, reqSignOut)
 
 	m.Group("/user/settings", func() {
@@ -235,6 +244,11 @@ func RegisterRoutes(m *macaron.Macaron) {
 				m.Get("/enroll", userSetting.EnrollTwoFactor)
 				m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost)
 			})
+			m.Group("/u2f", func() {
+				m.Post("/request_register", bindIgnErr(auth.U2FRegistrationForm{}), userSetting.U2FRegister)
+				m.Post("/register", bindIgnErr(u2f.RegisterResponse{}), userSetting.U2FRegisterPost)
+				m.Post("/delete", bindIgnErr(auth.U2FDeleteForm{}), userSetting.U2FDelete)
+			})
 			m.Group("/openid", func() {
 				m.Post("", bindIgnErr(auth.AddOpenIDForm{}), userSetting.OpenIDPost)
 				m.Post("/delete", userSetting.DeleteOpenID)
diff --git a/routers/user/auth.go b/routers/user/auth.go
index c8e1ada0db..9a59f52db2 100644
--- a/routers/user/auth.go
+++ b/routers/user/auth.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/go-macaron/captcha"
 	"github.com/markbates/goth"
+	"github.com/tstranex/u2f"
 )
 
 const (
@@ -35,6 +36,7 @@ const (
 	tplTwofa          base.TplName = "user/auth/twofa"
 	tplTwofaScratch   base.TplName = "user/auth/twofa_scratch"
 	tplLinkAccount    base.TplName = "user/auth/link_account"
+	tplU2F            base.TplName = "user/auth/u2f"
 )
 
 // AutoSignIn reads cookie and try to auto-login.
@@ -159,7 +161,6 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) {
 		}
 		return
 	}
-
 	// If this user is enrolled in 2FA, we can't sign the user in just yet.
 	// Instead, redirect them to the 2FA authentication page.
 	_, err = models.GetTwoFactorByUID(u.ID)
@@ -175,6 +176,13 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) {
 	// User needs to use 2FA, save data and redirect to 2FA page.
 	ctx.Session.Set("twofaUid", u.ID)
 	ctx.Session.Set("twofaRemember", form.Remember)
+
+	regs, err := models.GetU2FRegistrationsByUID(u.ID)
+	if err == nil && len(regs) > 0 {
+		ctx.Redirect(setting.AppSubURL + "/user/u2f")
+		return
+	}
+
 	ctx.Redirect(setting.AppSubURL + "/user/two_factor")
 }
 
@@ -317,12 +325,115 @@ func TwoFactorScratchPost(ctx *context.Context, form auth.TwoFactorScratchAuthFo
 	ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, auth.TwoFactorScratchAuthForm{})
 }
 
+// U2F shows the U2F login page
+func U2F(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("twofa")
+	ctx.Data["RequireU2F"] = true
+	// Check auto-login.
+	if checkAutoLogin(ctx) {
+		return
+	}
+
+	// Ensure user is in a 2FA session.
+	if ctx.Session.Get("twofaUid") == nil {
+		ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
+		return
+	}
+
+	ctx.HTML(200, tplU2F)
+}
+
+// U2FChallenge submits a sign challenge to the browser
+func U2FChallenge(ctx *context.Context) {
+	// Ensure user is in a U2F session.
+	idSess := ctx.Session.Get("twofaUid")
+	if idSess == nil {
+		ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
+		return
+	}
+	id := idSess.(int64)
+	regs, err := models.GetU2FRegistrationsByUID(id)
+	if err != nil {
+		ctx.ServerError("UserSignIn", err)
+		return
+	}
+	if len(regs) == 0 {
+		ctx.ServerError("UserSignIn", errors.New("no device registered"))
+		return
+	}
+	challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
+	if err = ctx.Session.Set("u2fChallenge", challenge); err != nil {
+		ctx.ServerError("UserSignIn", err)
+		return
+	}
+	ctx.JSON(200, challenge.SignRequest(regs.ToRegistrations()))
+}
+
+// U2FSign authenticates the user by signResp
+func U2FSign(ctx *context.Context, signResp u2f.SignResponse) {
+	challSess := ctx.Session.Get("u2fChallenge")
+	idSess := ctx.Session.Get("twofaUid")
+	if challSess == nil || idSess == nil {
+		ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
+		return
+	}
+	challenge := challSess.(*u2f.Challenge)
+	id := idSess.(int64)
+	regs, err := models.GetU2FRegistrationsByUID(id)
+	if err != nil {
+		ctx.ServerError("UserSignIn", err)
+		return
+	}
+	for _, reg := range regs {
+		r, err := reg.Parse()
+		if err != nil {
+			log.Fatal(4, "parsing u2f registration: %v", err)
+			continue
+		}
+		newCounter, authErr := r.Authenticate(signResp, *challenge, reg.Counter)
+		if authErr == nil {
+			reg.Counter = newCounter
+			user, err := models.GetUserByID(id)
+			if err != nil {
+				ctx.ServerError("UserSignIn", err)
+				return
+			}
+			remember := ctx.Session.Get("twofaRemember").(bool)
+			if err := reg.UpdateCounter(); err != nil {
+				ctx.ServerError("UserSignIn", err)
+				return
+			}
+
+			if ctx.Session.Get("linkAccount") != nil {
+				gothUser := ctx.Session.Get("linkAccountGothUser")
+				if gothUser == nil {
+					ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
+					return
+				}
+
+				err = models.LinkAccountToUser(user, gothUser.(goth.User))
+				if err != nil {
+					ctx.ServerError("UserSignIn", err)
+					return
+				}
+			}
+			redirect := handleSignInFull(ctx, user, remember, false)
+			if redirect == "" {
+				redirect = setting.AppSubURL + "/"
+			}
+			ctx.PlainText(200, []byte(redirect))
+			return
+		}
+	}
+	ctx.Error(401)
+}
+
 // This handles the final part of the sign-in process of the user.
 func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
 	handleSignInFull(ctx, u, remember, true)
 }
 
-func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) {
+func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) string {
 	if remember {
 		days := 86400 * setting.LogInRememberDays
 		ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL)
@@ -336,6 +447,8 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
 	ctx.Session.Delete("openid_determined_username")
 	ctx.Session.Delete("twofaUid")
 	ctx.Session.Delete("twofaRemember")
+	ctx.Session.Delete("u2fChallenge")
+	ctx.Session.Delete("linkAccount")
 	ctx.Session.Set("uid", u.ID)
 	ctx.Session.Set("uname", u.Name)
 
@@ -345,7 +458,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
 		u.Language = ctx.Locale.Language()
 		if err := models.UpdateUserCols(u, "language"); err != nil {
 			log.Error(4, fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
-			return
+			return setting.AppSubURL + "/"
 		}
 	}
 
@@ -358,7 +471,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
 	u.SetLastLogin()
 	if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
 		ctx.ServerError("UpdateUserCols", err)
-		return
+		return setting.AppSubURL + "/"
 	}
 
 	if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
@@ -366,12 +479,13 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
 		if obeyRedirect {
 			ctx.RedirectToFirst(redirectTo)
 		}
-		return
+		return redirectTo
 	}
 
 	if obeyRedirect {
 		ctx.Redirect(setting.AppSubURL + "/")
 	}
+	return setting.AppSubURL + "/"
 }
 
 // SignInOAuth handles the OAuth2 login buttons
@@ -467,6 +581,14 @@ func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context
 	// User needs to use 2FA, save data and redirect to 2FA page.
 	ctx.Session.Set("twofaUid", u.ID)
 	ctx.Session.Set("twofaRemember", false)
+
+	// If U2F is enrolled -> Redirect to U2F instead
+	regs, err := models.GetU2FRegistrationsByUID(u.ID)
+	if err == nil && len(regs) > 0 {
+		ctx.Redirect(setting.AppSubURL + "/user/u2f")
+		return
+	}
+
 	ctx.Redirect(setting.AppSubURL + "/user/two_factor")
 }
 
@@ -593,6 +715,13 @@ func LinkAccountPostSignIn(ctx *context.Context, signInForm auth.SignInForm) {
 	ctx.Session.Set("twofaRemember", signInForm.Remember)
 	ctx.Session.Set("linkAccount", true)
 
+	// If U2F is enrolled -> Redirect to U2F instead
+	regs, err := models.GetU2FRegistrationsByUID(u.ID)
+	if err == nil && len(regs) > 0 {
+		ctx.Redirect(setting.AppSubURL + "/user/u2f")
+		return
+	}
+
 	ctx.Redirect(setting.AppSubURL + "/user/two_factor")
 }
 
diff --git a/routers/user/setting/security.go b/routers/user/setting/security.go
index 5346f349ff..860730303f 100644
--- a/routers/user/setting/security.go
+++ b/routers/user/setting/security.go
@@ -33,6 +33,14 @@ func Security(ctx *context.Context) {
 		}
 	}
 	ctx.Data["TwofaEnrolled"] = enrolled
+	if enrolled {
+		ctx.Data["U2FRegistrations"], err = models.GetU2FRegistrationsByUID(ctx.User.ID)
+		if err != nil {
+			ctx.ServerError("GetU2FRegistrationsByUID", err)
+			return
+		}
+		ctx.Data["RequireU2F"] = true
+	}
 
 	tokens, err := models.ListAccessTokens(ctx.User.ID)
 	if err != nil {
diff --git a/routers/user/setting/security_u2f.go b/routers/user/setting/security_u2f.go
new file mode 100644
index 0000000000..c1d6eab967
--- /dev/null
+++ b/routers/user/setting/security_u2f.go
@@ -0,0 +1,99 @@
+// Copyright 2018 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package setting
+
+import (
+	"errors"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/auth"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/tstranex/u2f"
+)
+
+// U2FRegister initializes the u2f registration procedure
+func U2FRegister(ctx *context.Context, form auth.U2FRegistrationForm) {
+	if form.Name == "" {
+		ctx.Error(409)
+		return
+	}
+	challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
+	if err != nil {
+		ctx.ServerError("NewChallenge", err)
+		return
+	}
+	err = ctx.Session.Set("u2fChallenge", challenge)
+	if err != nil {
+		ctx.ServerError("Session.Set", err)
+		return
+	}
+	regs, err := models.GetU2FRegistrationsByUID(ctx.User.ID)
+	if err != nil {
+		ctx.ServerError("GetU2FRegistrationsByUID", err)
+		return
+	}
+	for _, reg := range regs {
+		if reg.Name == form.Name {
+			ctx.Error(409, "Name already taken")
+			return
+		}
+	}
+	ctx.Session.Set("u2fName", form.Name)
+	ctx.JSON(200, u2f.NewWebRegisterRequest(challenge, regs.ToRegistrations()))
+}
+
+// U2FRegisterPost receives the response of the security key
+func U2FRegisterPost(ctx *context.Context, response u2f.RegisterResponse) {
+	challSess := ctx.Session.Get("u2fChallenge")
+	u2fName := ctx.Session.Get("u2fName")
+	if challSess == nil || u2fName == nil {
+		ctx.ServerError("U2FRegisterPost", errors.New("not in U2F session"))
+		return
+	}
+	challenge := challSess.(*u2f.Challenge)
+	name := u2fName.(string)
+	config := &u2f.Config{
+		// Chrome 66+ doesn't return the device's attestation
+		// certificate by default.
+		SkipAttestationVerify: true,
+	}
+	reg, err := u2f.Register(response, *challenge, config)
+	if err != nil {
+		ctx.ServerError("u2f.Register", err)
+		return
+	}
+	if _, err = models.CreateRegistration(ctx.User, name, reg); err != nil {
+		ctx.ServerError("u2f.Register", err)
+		return
+	}
+	ctx.Status(200)
+}
+
+// U2FDelete deletes an security key by id
+func U2FDelete(ctx *context.Context, form auth.U2FDeleteForm) {
+	reg, err := models.GetU2FRegistrationByID(form.ID)
+	if err != nil {
+		if models.IsErrU2FRegistrationNotExist(err) {
+			ctx.Status(200)
+			return
+		}
+		ctx.ServerError("GetU2FRegistrationByID", err)
+		return
+	}
+	if reg.UserID != ctx.User.ID {
+		ctx.Status(401)
+		return
+	}
+	if err := models.DeleteRegistration(reg); err != nil {
+		ctx.ServerError("DeleteRegistration", err)
+		return
+	}
+	ctx.JSON(200, map[string]interface{}{
+		"redirect": setting.AppSubURL + "/user/settings/security",
+	})
+	return
+}
diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl
index c0f5a83d7b..3ad5358d41 100644
--- a/templates/base/footer.tmpl
+++ b/templates/base/footer.tmpl
@@ -64,6 +64,9 @@
 {{if .RequireDropzone}}
 	<script src="{{AppSubUrl}}/vendor/plugins/dropzone/dropzone.js"></script>
 {{end}}
+{{if .RequireU2F}}
+	<script src="{{AppSubUrl}}/vendor/plugins/u2f/index.js"></script>
+{{end}}
 {{if .RequireTribute}}
 	<script src="{{AppSubUrl}}/vendor/plugins/tribute/tribute.min.js"></script>
 
diff --git a/templates/user/auth/u2f.tmpl b/templates/user/auth/u2f.tmpl
new file mode 100644
index 0000000000..fa5904fc38
--- /dev/null
+++ b/templates/user/auth/u2f.tmpl
@@ -0,0 +1,22 @@
+{{template "base/head" .}}
+<div class="user signin">
+	<div class="ui middle centered very relaxed page grid">
+		<div class="column">
+			<h3 class="ui top attached header">
+			{{.i18n.Tr "twofa"}}
+			</h3>
+			<div class="ui attached segment">
+				<i class="huge key icon"></i>
+				<h3>{{.i18n.Tr "u2f_insert_key"}}</h3>
+				{{template "base/alert" .}}
+				<p>{{.i18n.Tr "u2f_sign_in"}}</p>
+			</div>
+			<div id="wait-for-key" class="ui attached segment"><div class="ui active indeterminate inline loader"></div> {{.i18n.Tr "u2f_press_button"}} </div>
+			<div class="ui attached segment">
+				<a href="/user/two_factor">{{.i18n.Tr "u2f_use_twofa"}}</a>
+			</div>
+		</div>
+	</div>
+</div>
+{{template "user/auth/u2f_error" .}}
+{{template "base/footer" .}}
diff --git a/templates/user/auth/u2f_error.tmpl b/templates/user/auth/u2f_error.tmpl
new file mode 100644
index 0000000000..e30b064720
--- /dev/null
+++ b/templates/user/auth/u2f_error.tmpl
@@ -0,0 +1,32 @@
+<div class="ui small modal" id="u2f-error">
+	<div class="header">{{.i18n.Tr "u2f_error"}}</div>
+	<div class="content">
+		<div class="ui negative message">
+			<div class="header">
+			{{.i18n.Tr "u2f_error"}}
+			</div>
+			<div class="hide" id="unsupported-browser">
+			{{.i18n.Tr "u2f_unsupported_browser"}}
+			</div>
+			<div class="hide" id="u2f-error-1">
+			{{.i18n.Tr "u2f_error_1"}}
+			</div>
+			<div class="hide" id="u2f-error-2">
+			{{.i18n.Tr "u2f_error_2"}}
+			</div>
+			<div class="hide" id="u2f-error-3">
+			{{.i18n.Tr "u2f_error_3"}}
+			</div>
+			<div class="hide" id="u2f-error-4">
+			{{.i18n.Tr "u2f_error_4"}}
+			</div>
+			<div class="hide u2f-error-5">
+			{{.i18n.Tr "u2f_error_5"}}
+			</div>
+		</div>
+	</div>
+	<div class="actions">
+		<button onclick="window.location.reload()" class="success ui button hide u2f_error_5">{{.i18n.Tr "u2f_reload"}}</button>
+		<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div>
+	</div>
+</div>
diff --git a/templates/user/settings/security.tmpl b/templates/user/settings/security.tmpl
index 8e7044f7df..c2c99c7979 100644
--- a/templates/user/settings/security.tmpl
+++ b/templates/user/settings/security.tmpl
@@ -4,6 +4,7 @@
 	<div class="ui container">
 		{{template "base/alert" .}}
 		{{template "user/settings/security_twofa" .}}
+		{{template "user/settings/security_u2f" .}}
 		{{template "user/settings/security_accountlinks" .}}
 		{{if .EnableOpenIDSignIn}}
 		{{template "user/settings/security_openid" .}}
diff --git a/templates/user/settings/security_openid.tmpl b/templates/user/settings/security_openid.tmpl
index 82a05f3885..a16501acfa 100644
--- a/templates/user/settings/security_openid.tmpl
+++ b/templates/user/settings/security_openid.tmpl
@@ -43,7 +43,7 @@
 		{{.CsrfTokenHtml}}
 		<div class="required field {{if .Err_OpenID}}error{{end}}">
 			<label for="openid">{{.i18n.Tr "settings.add_new_openid"}}</label>
-			<input id="openid" name="openid" type="text" autofocus required>
+			<input id="openid" name="openid" type="text" required>
 		</div>
 		<button class="ui green button">
 			{{.i18n.Tr "settings.add_openid"}}
diff --git a/templates/user/settings/security_u2f.tmpl b/templates/user/settings/security_u2f.tmpl
new file mode 100644
index 0000000000..4703f9deb9
--- /dev/null
+++ b/templates/user/settings/security_u2f.tmpl
@@ -0,0 +1,56 @@
+<h4 class="ui top attached header">
+{{.i18n.Tr "settings.u2f"}}
+</h4>
+<div class="ui attached segment">
+	<p>{{.i18n.Tr "settings.u2f_desc" | Str2html}}</p>
+	{{if .TwofaEnrolled}}
+		<div class="ui key list">
+			{{range .U2FRegistrations}}
+			    <div class="item">
+			    	<div class="right floated content">
+			    		<button class="ui red tiny button delete-button" id="delete-registration" data-url="{{$.Link}}/u2f/delete" data-id="{{.ID}}">
+			    		{{$.i18n.Tr "settings.delete_key"}}
+			    		</button>
+			    	</div>
+			    	<div class="content">
+			    		<strong>{{.Name}}</strong>
+			    	</div>
+			    </div>
+			{{end}}
+		</div>
+		<div class="ui form">
+			{{.CsrfTokenHtml}}
+			<div class="required field">
+				<label for="nickname">{{.i18n.Tr "settings.u2f_nickname"}}</label>
+				<input id="nickname" name="nickname" type="text" required>
+			</div>
+			<button id="register-security-key" class="positive ui labeled icon button"><i class="usb icon"></i>{{.i18n.Tr "settings.u2f_register_key"}}</button>
+		</div>
+	{{else}}
+		<b>{{.i18n.Tr "settings.u2f_require_twofa"}}</b>
+	{{end}}
+</div>
+
+<div class="ui small modal" id="register-device">
+	<div class="header">{{.i18n.Tr "settings.u2f_register_key"}}</div>
+	<div class="content">
+		<i class="notched spinner loading icon"></i> {{.i18n.Tr "settings.u2f_press_button"}}
+	</div>
+	<div class="actions">
+		<div class="ui cancel button">{{.i18n.Tr "cancel"}}</div>
+	</div>
+</div>
+
+{{template "user/auth/u2f_error" .}}
+
+<div class="ui small basic delete modal" id="delete-registration">
+	<div class="ui icon header">
+		<i class="trash icon"></i>
+	{{.i18n.Tr "settings.u2f_delete_key"}}
+	</div>
+	<div class="content">
+		<p>{{.i18n.Tr "settings.u2f_delete_key_desc"}}</p>
+	</div>
+	{{template "base/delete_modal_actions" .}}
+</div>
+
diff --git a/vendor/github.com/tstranex/u2f/LICENSE b/vendor/github.com/tstranex/u2f/LICENSE
new file mode 100644
index 0000000000..3c7279c6fc
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 The Go FIDO U2F Library Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/github.com/tstranex/u2f/README.md b/vendor/github.com/tstranex/u2f/README.md
new file mode 100644
index 0000000000..95de78f8b5
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/README.md
@@ -0,0 +1,97 @@
+# Go FIDO U2F Library
+
+This Go package implements the parts of the FIDO U2F specification required on
+the server side of an application.
+
+[![Build Status](https://travis-ci.org/tstranex/u2f.svg?branch=master)](https://travis-ci.org/tstranex/u2f)
+
+## Features
+
+- Native Go implementation
+- No dependancies other than the Go standard library
+- Token attestation certificate verification
+
+## Usage
+
+Please visit http://godoc.org/github.com/tstranex/u2f for the full
+documentation.
+
+### How to enrol a new token
+
+```go
+app_id := "http://localhost"
+
+// Send registration request to the browser.
+c, _ := NewChallenge(app_id, []string{app_id})
+req, _ := c.RegisterRequest()
+
+// Read response from the browser.
+var resp RegisterResponse
+reg, err := Register(resp, c, nil)
+if err != nil {
+    // Registration failed.
+}
+
+// Store registration in the database.
+```
+
+### How to perform an authentication
+
+```go
+// Fetch registration and counter from the database.
+var reg Registration
+var counter uint32
+
+// Send authentication request to the browser.
+c, _ := NewChallenge(app_id, []string{app_id})
+req, _ := c.SignRequest(reg)
+
+// Read response from the browser.
+var resp SignResponse
+newCounter, err := reg.Authenticate(resp, c, counter)
+if err != nil {
+    // Authentication failed.
+}
+
+// Store updated counter in the database.
+```
+
+## Installation
+
+```
+$ go get github.com/tstranex/u2f
+```
+
+## Example
+
+See u2fdemo/main.go for an full example server. To run it:
+
+```
+$ go install github.com/tstranex/u2f/u2fdemo
+$ ./bin/u2fdemo
+```
+
+Open https://localhost:3483 in Chrome.
+Ignore the SSL warning (due to the self-signed certificate for localhost).
+You can then test registering and authenticating using your token.
+
+## Changelog
+
+- 2016-12-18: The package has been updated to work with the new
+  U2F Javascript 1.1 API specification. This causes some breaking changes.
+
+  `SignRequest` has been replaced by `WebSignRequest` which now includes
+  multiple registrations. This is useful when the user has multiple devices
+  registered since you can now authenticate against any of them with a single
+  request.
+
+  `WebRegisterRequest` has been introduced, which should generally be used
+  instead of using `RegisterRequest` directly. It includes the list of existing
+  registrations with the new registration request. If the user's device already
+  matches one of the existing registrations, it will refuse to re-register.
+
+  `Challenge.RegisterRequest` has been replaced by `NewWebRegisterRequest`.
+
+## License
+
+The Go FIDO U2F Library is licensed under the MIT License.
diff --git a/vendor/github.com/tstranex/u2f/auth.go b/vendor/github.com/tstranex/u2f/auth.go
new file mode 100644
index 0000000000..05c25f5731
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/auth.go
@@ -0,0 +1,136 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+package u2f
+
+import (
+	"crypto/ecdsa"
+	"crypto/sha256"
+	"encoding/asn1"
+	"errors"
+	"math/big"
+	"time"
+)
+
+// SignRequest creates a request to initiate an authentication.
+func (c *Challenge) SignRequest(regs []Registration) *WebSignRequest {
+	var sr WebSignRequest
+	sr.AppID = c.AppID
+	sr.Challenge = encodeBase64(c.Challenge)
+	for _, r := range regs {
+		rk := getRegisteredKey(c.AppID, r)
+		sr.RegisteredKeys = append(sr.RegisteredKeys, rk)
+	}
+	return &sr
+}
+
+// ErrCounterTooLow is raised when the counter value received from the device is
+// lower than last stored counter value. This may indicate that the device has
+// been cloned (or is malfunctioning). The application may choose to disable
+// the particular device as precaution.
+var ErrCounterTooLow = errors.New("u2f: counter too low")
+
+// Authenticate validates a SignResponse authentication response.
+// An error is returned if any part of the response fails to validate.
+// The counter should be the counter associated with appropriate device
+// (i.e. resp.KeyHandle).
+// The latest counter value is returned, which the caller should store.
+func (reg *Registration) Authenticate(resp SignResponse, c Challenge, counter uint32) (newCounter uint32, err error) {
+	if time.Now().Sub(c.Timestamp) > timeout {
+		return 0, errors.New("u2f: challenge has expired")
+	}
+	if resp.KeyHandle != encodeBase64(reg.KeyHandle) {
+		return 0, errors.New("u2f: wrong key handle")
+	}
+
+	sigData, err := decodeBase64(resp.SignatureData)
+	if err != nil {
+		return 0, err
+	}
+
+	clientData, err := decodeBase64(resp.ClientData)
+	if err != nil {
+		return 0, err
+	}
+
+	ar, err := parseSignResponse(sigData)
+	if err != nil {
+		return 0, err
+	}
+
+	if ar.Counter < counter {
+		return 0, ErrCounterTooLow
+	}
+
+	if err := verifyClientData(clientData, c); err != nil {
+		return 0, err
+	}
+
+	if err := verifyAuthSignature(*ar, &reg.PubKey, c.AppID, clientData); err != nil {
+		return 0, err
+	}
+
+	if !ar.UserPresenceVerified {
+		return 0, errors.New("u2f: user was not present")
+	}
+
+	return ar.Counter, nil
+}
+
+type ecdsaSig struct {
+	R, S *big.Int
+}
+
+type authResp struct {
+	UserPresenceVerified bool
+	Counter              uint32
+	sig                  ecdsaSig
+	raw                  []byte
+}
+
+func parseSignResponse(sd []byte) (*authResp, error) {
+	if len(sd) < 5 {
+		return nil, errors.New("u2f: data is too short")
+	}
+
+	var ar authResp
+
+	userPresence := sd[0]
+	if userPresence|1 != 1 {
+		return nil, errors.New("u2f: invalid user presence byte")
+	}
+	ar.UserPresenceVerified = userPresence == 1
+
+	ar.Counter = uint32(sd[1])<<24 | uint32(sd[2])<<16 | uint32(sd[3])<<8 | uint32(sd[4])
+
+	ar.raw = sd[:5]
+
+	rest, err := asn1.Unmarshal(sd[5:], &ar.sig)
+	if err != nil {
+		return nil, err
+	}
+	if len(rest) != 0 {
+		return nil, errors.New("u2f: trailing data")
+	}
+
+	return &ar, nil
+}
+
+func verifyAuthSignature(ar authResp, pubKey *ecdsa.PublicKey, appID string, clientData []byte) error {
+	appParam := sha256.Sum256([]byte(appID))
+	challenge := sha256.Sum256(clientData)
+
+	var buf []byte
+	buf = append(buf, appParam[:]...)
+	buf = append(buf, ar.raw...)
+	buf = append(buf, challenge[:]...)
+	hash := sha256.Sum256(buf)
+
+	if !ecdsa.Verify(pubKey, hash[:], ar.sig.R, ar.sig.S) {
+		return errors.New("u2f: invalid signature")
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/tstranex/u2f/certs.go b/vendor/github.com/tstranex/u2f/certs.go
new file mode 100644
index 0000000000..14d745a009
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/certs.go
@@ -0,0 +1,89 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+package u2f
+
+import (
+	"crypto/x509"
+	"log"
+)
+
+const plugUpCert = `-----BEGIN CERTIFICATE-----
+MIIBrjCCAVSgAwIBAgIJAMGSvUZlGSGVMAoGCCqGSM49BAMCMDIxMDAuBgNVBAMM
+J1BsdWctdXAgRklETyBJbnRlcm5hbCBBdHRlc3RhdGlvbiBDQSAjMTAeFw0xNDA5
+MjMxNjM3NTFaFw0zNDA5MjMxNjM3NTFaMDIxMDAuBgNVBAMMJ1BsdWctdXAgRklE
+TyBJbnRlcm5hbCBBdHRlc3RhdGlvbiBDQSAjMTBZMBMGByqGSM49AgEGCCqGSM49
+AwEHA0IABH9mscDgEHo4AUh7J8JHqRxsSVxbvsbe6Pxy5cUFKfQlWNjxRrZcbhOb
+UY3WsAwmKuUdOcghbpTILhdp8LG9z5GjUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYD
+VR0OBBYEFM+nRPKhYlDwOemShePaUOd9sDqoMB8GA1UdIwQYMBaAFM+nRPKhYlDw
+OemShePaUOd9sDqoMAoGCCqGSM49BAMCA0gAMEUCIQDVzqnX1rgvyJaZ7WZUm1ED
+hJKSsDxRXEnH+/voqpq/zgIgH4RUR6vr9YNrkzuCq5R07gF7P4qhtg/4jy+dhl7o
+NAU=
+-----END CERTIFICATE-----
+`
+
+const neowaveCert = `-----BEGIN CERTIFICATE-----
+MIICJDCCAcugAwIBAgIJAIo+0R9DGvSBMAoGCCqGSM49BAMCMG8xCzAJBgNVBAYT
+AkZSMQ8wDQYDVQQIDAZGcmFuY2UxETAPBgNVBAcMCEdhcmRhbm5lMRAwDgYDVQQK
+DAdOZW93YXZlMSowKAYDVQQDDCFOZW93YXZlIEtFWURPIEZJRE8gVTJGIENBIEJh
+dGNoIDEwHhcNMTUwMTI4MTA1ODM1WhcNMjUwMTI1MTA1ODM1WjBvMQswCQYDVQQG
+EwJGUjEPMA0GA1UECAwGRnJhbmNlMREwDwYDVQQHDAhHYXJkYW5uZTEQMA4GA1UE
+CgwHTmVvd2F2ZTEqMCgGA1UEAwwhTmVvd2F2ZSBLRVlETyBGSURPIFUyRiBDQSBC
+YXRjaCAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBlUmE1BRE/M/CE/ZCN+x
+eutfnVsThMwIDN+4DL9gqXoKCeRMiDQ1zwm/yQS80BYSEz7Du9RU+2mlnyhwhu+f
+BqNQME4wHQYDVR0OBBYEFF42te8/iq5HGom4sIhgkJWLq5jkMB8GA1UdIwQYMBaA
+FF42te8/iq5HGom4sIhgkJWLq5jkMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwID
+RwAwRAIgVTxBFb2Hclq5Yi5gQp6WoZAcHETfKASvTQVOE88REGQCIA5DcwGVLsZB
+QTb94Xgtb/WUieCvmwukFl/gEO15f3uA
+-----END CERTIFICATE-----
+`
+
+const yubicoRootCert = `-----BEGIN CERTIFICATE-----
+MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ
+dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw
+MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290
+IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk
+5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep
+8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw
+nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
+9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw
+LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ
+hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
+BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4
+MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt
+hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k
+LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U
+sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc
+U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==
+-----END CERTIFICATE-----
+`
+
+const entersektCert = `-----BEGIN CERTIFICATE-----
+MIICHjCCAcOgAwIBAgIBADAKBggqhkjOPQQDAjBvMQswCQYDVQQGEwJaQTEVMBMG
+A1UECAwMV2VzdGVybiBDYXBlMRUwEwYDVQQHDAxTdGVsbGVuYm9zY2gxEjAQBgNV
+BAoMCUVudGVyc2VrdDELMAkGA1UECwwCSVQxETAPBgNVBAMMCFRyYW5zYWt0MB4X
+DTE0MTEwMTExMjczNFoXDTE1MTEwMTExMjczNFowbzELMAkGA1UEBhMCWkExFTAT
+BgNVBAgMDFdlc3Rlcm4gQ2FwZTEVMBMGA1UEBwwMU3RlbGxlbmJvc2NoMRIwEAYD
+VQQKDAlFbnRlcnNla3QxCzAJBgNVBAsMAklUMREwDwYDVQQDDAhUcmFuc2FrdDBZ
+MBMGByqGSM49AgEGCCqGSM49AwEHA0IABBh10blFheMZy3k2iqW9TzLhS1DbJ/Xf
+DxqQJJkpqTLq7vI+K3O4C20YtN0jsVrj7UylWoSRlPL5F7IkbeQ6aZ6jUDBOMB0G
+A1UdDgQWBBQWRFF7mVAipWTdfBWk2B8Dv4Ab4jAfBgNVHSMEGDAWgBQWRFF7mVAi
+pWTdfBWk2B8Dv4Ab4jAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQCo
+bMURXOxv6pqz6ECBh0zgL2vVhEfTOZJOW0PACGalWgIhAME0LHGi6ZS7z9yzHNqi
+cnRb+okM+PIy/hBcBuqTWCbw
+-----END CERTIFICATE-----
+`
+
+func mustLoadPool(pemCerts []byte) *x509.CertPool {
+	p := x509.NewCertPool()
+	if !p.AppendCertsFromPEM(pemCerts) {
+		log.Fatal("u2f: Error loading root cert pool.")
+		return nil
+	}
+	return p
+}
+
+var roots = mustLoadPool([]byte(yubicoRootCert + entersektCert + neowaveCert + plugUpCert))
diff --git a/vendor/github.com/tstranex/u2f/messages.go b/vendor/github.com/tstranex/u2f/messages.go
new file mode 100644
index 0000000000..a78038dea2
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/messages.go
@@ -0,0 +1,87 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+package u2f
+
+import (
+	"encoding/json"
+)
+
+// JwkKey represents a public key used by a browser for the Channel ID TLS
+// extension.
+type JwkKey struct {
+	KTy string `json:"kty"`
+	Crv string `json:"crv"`
+	X   string `json:"x"`
+	Y   string `json:"y"`
+}
+
+// ClientData as defined by the FIDO U2F Raw Message Formats specification.
+type ClientData struct {
+	Typ       string          `json:"typ"`
+	Challenge string          `json:"challenge"`
+	Origin    string          `json:"origin"`
+	CIDPubKey json.RawMessage `json:"cid_pubkey"`
+}
+
+// RegisterRequest as defined by the FIDO U2F Javascript API 1.1.
+type RegisterRequest struct {
+	Version   string `json:"version"`
+	Challenge string `json:"challenge"`
+}
+
+// WebRegisterRequest contains the parameters needed for the u2f.register()
+// high-level Javascript API function as defined by the
+// FIDO U2F Javascript API 1.1.
+type WebRegisterRequest struct {
+	AppID            string            `json:"appId"`
+	RegisterRequests []RegisterRequest `json:"registerRequests"`
+	RegisteredKeys   []RegisteredKey   `json:"registeredKeys"`
+}
+
+// RegisterResponse as defined by the FIDO U2F Javascript API 1.1.
+type RegisterResponse struct {
+	Version          string `json:"version"`
+	RegistrationData string `json:"registrationData"`
+	ClientData       string `json:"clientData"`
+}
+
+// RegisteredKey as defined by the FIDO U2F Javascript API 1.1.
+type RegisteredKey struct {
+	Version   string `json:"version"`
+	KeyHandle string `json:"keyHandle"`
+	AppID     string `json:"appId"`
+}
+
+// WebSignRequest contains the parameters needed for the u2f.sign()
+// high-level Javascript API function as defined by the
+// FIDO U2F Javascript API 1.1.
+type WebSignRequest struct {
+	AppID          string          `json:"appId"`
+	Challenge      string          `json:"challenge"`
+	RegisteredKeys []RegisteredKey `json:"registeredKeys"`
+}
+
+// SignResponse as defined by the FIDO U2F Javascript API 1.1.
+type SignResponse struct {
+	KeyHandle     string `json:"keyHandle"`
+	SignatureData string `json:"signatureData"`
+	ClientData    string `json:"clientData"`
+}
+
+// TrustedFacets as defined by the FIDO AppID and Facet Specification.
+type TrustedFacets struct {
+	Version struct {
+		Major int `json:"major"`
+		Minor int `json:"minor"`
+	} `json:"version"`
+	Ids []string `json:"ids"`
+}
+
+// TrustedFacetsEndpoint is a container of TrustedFacets.
+// It is used as the response for an appId URL endpoint.
+type TrustedFacetsEndpoint struct {
+	TrustedFacets []TrustedFacets `json:"trustedFacets"`
+}
diff --git a/vendor/github.com/tstranex/u2f/register.go b/vendor/github.com/tstranex/u2f/register.go
new file mode 100644
index 0000000000..da0c1cce24
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/register.go
@@ -0,0 +1,230 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+package u2f
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/asn1"
+	"encoding/hex"
+	"errors"
+	"time"
+)
+
+// Registration represents a single enrolment or pairing between an
+// application and a token. This data will typically be stored in a database.
+type Registration struct {
+	// Raw serialized registration data as received from the token.
+	Raw []byte
+
+	KeyHandle []byte
+	PubKey    ecdsa.PublicKey
+
+	// AttestationCert can be nil for Authenticate requests.
+	AttestationCert *x509.Certificate
+}
+
+// Config contains configurable options for the package.
+type Config struct {
+	// SkipAttestationVerify controls whether the token attestation
+	// certificate should be verified on registration. Ideally it should
+	// always be verified. However, there is currently no public list of
+	// trusted attestation root certificates so it may be necessary to skip.
+	SkipAttestationVerify bool
+
+	// RootAttestationCertPool overrides the default root certificates used
+	// to verify client attestations. If nil, this defaults to the roots that are
+	// bundled in this library.
+	RootAttestationCertPool *x509.CertPool
+}
+
+// Register validates a RegisterResponse message to enrol a new token.
+// An error is returned if any part of the response fails to validate.
+// The returned Registration should be stored by the caller.
+func Register(resp RegisterResponse, c Challenge, config *Config) (*Registration, error) {
+	if config == nil {
+		config = &Config{}
+	}
+
+	if time.Now().Sub(c.Timestamp) > timeout {
+		return nil, errors.New("u2f: challenge has expired")
+	}
+
+	regData, err := decodeBase64(resp.RegistrationData)
+	if err != nil {
+		return nil, err
+	}
+
+	clientData, err := decodeBase64(resp.ClientData)
+	if err != nil {
+		return nil, err
+	}
+
+	reg, sig, err := parseRegistration(regData)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := verifyClientData(clientData, c); err != nil {
+		return nil, err
+	}
+
+	if err := verifyAttestationCert(*reg, config); err != nil {
+		return nil, err
+	}
+
+	if err := verifyRegistrationSignature(*reg, sig, c.AppID, clientData); err != nil {
+		return nil, err
+	}
+
+	return reg, nil
+}
+
+func parseRegistration(buf []byte) (*Registration, []byte, error) {
+	if len(buf) < 1+65+1+1+1 {
+		return nil, nil, errors.New("u2f: data is too short")
+	}
+
+	var r Registration
+	r.Raw = buf
+
+	if buf[0] != 0x05 {
+		return nil, nil, errors.New("u2f: invalid reserved byte")
+	}
+	buf = buf[1:]
+
+	x, y := elliptic.Unmarshal(elliptic.P256(), buf[:65])
+	if x == nil {
+		return nil, nil, errors.New("u2f: invalid public key")
+	}
+	r.PubKey.Curve = elliptic.P256()
+	r.PubKey.X = x
+	r.PubKey.Y = y
+	buf = buf[65:]
+
+	khLen := int(buf[0])
+	buf = buf[1:]
+	if len(buf) < khLen {
+		return nil, nil, errors.New("u2f: invalid key handle")
+	}
+	r.KeyHandle = buf[:khLen]
+	buf = buf[khLen:]
+
+	// The length of the x509 cert isn't specified so it has to be inferred
+	// by parsing. We can't use x509.ParseCertificate yet because it returns
+	// an error if there are any trailing bytes. So parse raw asn1 as a
+	// workaround to get the length.
+	sig, err := asn1.Unmarshal(buf, &asn1.RawValue{})
+	if err != nil {
+		return nil, nil, err
+	}
+
+	buf = buf[:len(buf)-len(sig)]
+	fixCertIfNeed(buf)
+	cert, err := x509.ParseCertificate(buf)
+	if err != nil {
+		return nil, nil, err
+	}
+	r.AttestationCert = cert
+
+	return &r, sig, nil
+}
+
+// UnmarshalBinary implements encoding.BinaryMarshaler.
+func (r *Registration) UnmarshalBinary(data []byte) error {
+	reg, _, err := parseRegistration(data)
+	if err != nil {
+		return err
+	}
+	*r = *reg
+	return nil
+}
+
+// MarshalBinary implements encoding.BinaryUnmarshaler.
+func (r *Registration) MarshalBinary() ([]byte, error) {
+	return r.Raw, nil
+}
+
+func verifyAttestationCert(r Registration, config *Config) error {
+	if config.SkipAttestationVerify {
+		return nil
+	}
+	rootCertPool := roots
+	if config.RootAttestationCertPool != nil {
+		rootCertPool = config.RootAttestationCertPool
+	}
+
+	opts := x509.VerifyOptions{Roots: rootCertPool}
+	_, err := r.AttestationCert.Verify(opts)
+	return err
+}
+
+func verifyRegistrationSignature(
+	r Registration, signature []byte, appid string, clientData []byte) error {
+
+	appParam := sha256.Sum256([]byte(appid))
+	challenge := sha256.Sum256(clientData)
+
+	buf := []byte{0}
+	buf = append(buf, appParam[:]...)
+	buf = append(buf, challenge[:]...)
+	buf = append(buf, r.KeyHandle...)
+	pk := elliptic.Marshal(r.PubKey.Curve, r.PubKey.X, r.PubKey.Y)
+	buf = append(buf, pk...)
+
+	return r.AttestationCert.CheckSignature(
+		x509.ECDSAWithSHA256, buf, signature)
+}
+
+func getRegisteredKey(appID string, r Registration) RegisteredKey {
+	return RegisteredKey{
+		Version:   u2fVersion,
+		KeyHandle: encodeBase64(r.KeyHandle),
+		AppID:     appID,
+	}
+}
+
+// fixCertIfNeed fixes broken certificates described in
+// https://github.com/Yubico/php-u2flib-server/blob/master/src/u2flib_server/U2F.php#L84
+func fixCertIfNeed(cert []byte) {
+	h := sha256.Sum256(cert)
+	switch hex.EncodeToString(h[:]) {
+	case
+		"349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8",
+		"dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f",
+		"1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae",
+		"d0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb",
+		"6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897",
+		"ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511":
+
+		// clear the offending byte.
+		cert[len(cert)-257] = 0
+	}
+}
+
+// NewWebRegisterRequest creates a request to enrol a new token.
+// regs is the list of the user's existing registration. The browser will
+// refuse to re-register a device if it has an existing registration.
+func NewWebRegisterRequest(c *Challenge, regs []Registration) *WebRegisterRequest {
+	req := RegisterRequest{
+		Version:   u2fVersion,
+		Challenge: encodeBase64(c.Challenge),
+	}
+
+	rr := WebRegisterRequest{
+		AppID:            c.AppID,
+		RegisterRequests: []RegisterRequest{req},
+	}
+
+	for _, r := range regs {
+		rk := getRegisteredKey(c.AppID, r)
+		rr.RegisteredKeys = append(rr.RegisteredKeys, rk)
+	}
+
+	return &rr
+}
diff --git a/vendor/github.com/tstranex/u2f/util.go b/vendor/github.com/tstranex/u2f/util.go
new file mode 100644
index 0000000000..f035aa417b
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/util.go
@@ -0,0 +1,125 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+/*
+Package u2f implements the server-side parts of the
+FIDO Universal 2nd Factor (U2F) specification.
+
+Applications will usually persist Challenge and Registration objects in a
+database.
+
+To enrol a new token:
+
+    app_id := "http://localhost"
+    c, _ := NewChallenge(app_id, []string{app_id})
+    req, _ := u2f.NewWebRegisterRequest(c, existingTokens)
+    // Send the request to the browser.
+    var resp RegisterResponse
+    // Read resp from the browser.
+    reg, err := Register(resp, c)
+    if err != nil {
+         // Registration failed.
+    }
+    // Store reg in the database.
+
+To perform an authentication:
+
+    var regs []Registration
+    // Fetch regs from the database.
+    c, _ := NewChallenge(app_id, []string{app_id})
+    req, _ := c.SignRequest(regs)
+    // Send the request to the browser.
+    var resp SignResponse
+    // Read resp from the browser.
+    new_counter, err := reg.Authenticate(resp, c)
+    if err != nil {
+        // Authentication failed.
+    }
+    reg.Counter = new_counter
+    // Store updated Registration in the database.
+
+The FIDO U2F specification can be found here:
+https://fidoalliance.org/specifications/download
+*/
+package u2f
+
+import (
+	"crypto/rand"
+	"crypto/subtle"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"strings"
+	"time"
+)
+
+const u2fVersion = "U2F_V2"
+const timeout = 5 * time.Minute
+
+func decodeBase64(s string) ([]byte, error) {
+	for i := 0; i < len(s)%4; i++ {
+		s += "="
+	}
+	return base64.URLEncoding.DecodeString(s)
+}
+
+func encodeBase64(buf []byte) string {
+	s := base64.URLEncoding.EncodeToString(buf)
+	return strings.TrimRight(s, "=")
+}
+
+// Challenge represents a single transaction between the server and
+// authenticator. This data will typically be stored in a database.
+type Challenge struct {
+	Challenge     []byte
+	Timestamp     time.Time
+	AppID         string
+	TrustedFacets []string
+}
+
+// NewChallenge generates a challenge for the given application.
+func NewChallenge(appID string, trustedFacets []string) (*Challenge, error) {
+	challenge := make([]byte, 32)
+	n, err := rand.Read(challenge)
+	if err != nil {
+		return nil, err
+	}
+	if n != 32 {
+		return nil, errors.New("u2f: unable to generate random bytes")
+	}
+
+	var c Challenge
+	c.Challenge = challenge
+	c.Timestamp = time.Now()
+	c.AppID = appID
+	c.TrustedFacets = trustedFacets
+	return &c, nil
+}
+
+func verifyClientData(clientData []byte, challenge Challenge) error {
+	var cd ClientData
+	if err := json.Unmarshal(clientData, &cd); err != nil {
+		return err
+	}
+
+	foundFacetID := false
+	for _, facetID := range challenge.TrustedFacets {
+		if facetID == cd.Origin {
+			foundFacetID = true
+			break
+		}
+	}
+	if !foundFacetID {
+		return errors.New("u2f: untrusted facet id")
+	}
+
+	c := encodeBase64(challenge.Challenge)
+	if len(c) != len(cd.Challenge) ||
+		subtle.ConstantTimeCompare([]byte(c), []byte(cd.Challenge)) != 1 {
+		return errors.New("u2f: challenge does not match")
+	}
+
+	return nil
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index d056f48f39..1d6e29dfc3 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -1368,6 +1368,12 @@
 			"revision": "917f41c560270110ceb73c5b38be2a9127387071",
 			"revisionTime": "2016-03-11T05:04:36Z"
 		},
+		{
+			"checksumSHA1": "NE1kNfAZ0AAXCUbwx196os/DSUE=",
+			"path": "github.com/tstranex/u2f",
+			"revision": "d21a03e0b1d9fc1df59ff54e7a513655c1748b0c",
+			"revisionTime": "2018-05-05T18:51:14Z"
+		},
 		{
 			"checksumSHA1": "MfWqWj0xRPdk1DpXCN0EXyBCa4Q=",
 			"path": "github.com/tinylib/msgp/msgp",