Add support for FIDO U2F (#3971)

* Add support for U2F

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add vendor library
Add missing translations

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Minor improvements

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add U2F support for Firefox, Chrome (Android) by introducing a custom JS library
Add U2F error handling

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add U2F login page to OAuth

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Move U2F user settings to a separate file

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add unit tests for u2f model
Renamed u2f table name

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Fix problems caused by refactoring

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add U2F documentation

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Remove not needed console.log-s

Signed-off-by: Jonas Franz <info@jonasfranz.software>

* Add default values to app.ini.sample
Add FIDO U2F to comparison

Signed-off-by: Jonas Franz <info@jonasfranz.software>
tokarchuk/v1.17
Jonas Franz 7 years ago committed by Lauris BH
parent f933bcdfee
commit 951309f76a
  1. 8
      custom/conf/app.ini.sample
  2. 4
      docs/content/doc/advanced/config-cheat-sheet.en-us.md
  3. 9
      docs/content/doc/features/comparison.en-us.md
  4. 22
      models/error.go
  5. 7
      models/fixtures/u2f_registration.yml
  6. 2
      models/migrations/migrations.go
  7. 19
      models/migrations/v65.go
  8. 1
      models/models.go
  9. 120
      models/u2f.go
  10. 61
      models/u2f_test.go
  11. 20
      modules/auth/user_form.go
  12. 8
      modules/setting/setting.go
  13. 22
      options/locale/locale_en-US.ini
  14. 126
      public/js/index.js
  15. 5
      public/vendor/librejs.html
  16. 1
      public/vendor/plugins/u2f/index.js
  17. 16
      routers/routes/routes.go
  18. 139
      routers/user/auth.go
  19. 8
      routers/user/setting/security.go
  20. 99
      routers/user/setting/security_u2f.go
  21. 3
      templates/base/footer.tmpl
  22. 22
      templates/user/auth/u2f.tmpl
  23. 32
      templates/user/auth/u2f_error.tmpl
  24. 1
      templates/user/settings/security.tmpl
  25. 2
      templates/user/settings/security_openid.tmpl
  26. 56
      templates/user/settings/security_u2f.tmpl
  27. 21
      vendor/github.com/tstranex/u2f/LICENSE
  28. 97
      vendor/github.com/tstranex/u2f/README.md
  29. 136
      vendor/github.com/tstranex/u2f/auth.go
  30. 89
      vendor/github.com/tstranex/u2f/certs.go
  31. 87
      vendor/github.com/tstranex/u2f/messages.go
  32. 230
      vendor/github.com/tstranex/u2f/register.go
  33. 125
      vendor/github.com/tstranex/u2f/util.go
  34. 6
      vendor/vendor.json

@ -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 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,한국어 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 ; Used for datetimepicker
[i18n.datelang] [i18n.datelang]
en-US = en en-US = en

@ -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. - `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`. - `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`) ## Markup (`markup`)
Gitea can support Markup using external tools. The example below will add a markup named `asciidoc`. Gitea can support Markup using external tools. The example below will add a markup named `asciidoc`.

@ -535,6 +535,15 @@ _Symbols used in table:_
<td></td> <td></td>
<td></td> <td></td>
</tr> </tr>
<tr>
<td>FIDO U2F (2FA)</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr> <tr>
<td>Webhook support</td> <td>Webhook support</td>
<td></td> <td></td>

@ -1237,3 +1237,25 @@ func IsErrExternalLoginUserNotExist(err error) bool {
func (err ErrExternalLoginUserNotExist) Error() string { func (err ErrExternalLoginUserNotExist) Error() string {
return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID) 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
}

@ -0,0 +1,7 @@
-
id: 1
name: "U2F Key"
user_id: 1
counter: 0
created_unix: 946684800
updated_unix: 946684800

@ -182,6 +182,8 @@ var migrations = []Migration{
NewMigration("add language column for user setting", addLanguageSetting), NewMigration("add language column for user setting", addLanguageSetting),
// v64 -> v65 // v64 -> v65
NewMigration("add multiple assignees", addMultipleAssignees), NewMigration("add multiple assignees", addMultipleAssignees),
// v65 -> v66
NewMigration("add u2f", addU2FReg),
} }
// Migrate database to current version // Migrate database to current version

@ -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{})
}

@ -120,6 +120,7 @@ func init() {
new(LFSLock), new(LFSLock),
new(Reaction), new(Reaction),
new(IssueAssignees), new(IssueAssignees),
new(U2FRegistration),
) )
gonicNames := []string{"SSL", "UID"} gonicNames := []string{"SSL", "UID"}

@ -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
}

@ -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})
}

@ -211,3 +211,23 @@ type TwoFactorScratchAuthForm struct {
func (f *TwoFactorScratchAuthForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { func (f *TwoFactorScratchAuthForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale) 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)
}

@ -521,6 +521,11 @@ var (
MaxResponseItems: 50, MaxResponseItems: 50,
} }
U2F = struct {
AppID string
TrustedFacets []string
}{}
// I18n settings // I18n settings
Langs []string Langs []string
Names []string Names []string
@ -1135,6 +1140,9 @@ func NewContext() {
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false), 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 // Service settings

@ -31,6 +31,19 @@ twofa = Two-Factor Authentication
twofa_scratch = Two-Factor Scratch Code twofa_scratch = Two-Factor Scratch Code
passcode = Passcode 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 repository = Repository
organization = Organization organization = Organization
mirror = Mirror mirror = Mirror
@ -320,6 +333,7 @@ twofa = Two-Factor Authentication
account_link = Linked Accounts account_link = Linked Accounts
organization = Organizations organization = Organizations
uid = Uid uid = Uid
u2f = Security Keys
public_profile = Public Profile public_profile = Public Profile
profile_desc = Your email address will be used for notifications and other operations. 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. 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! 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 = Manage Linked Accounts
manage_account_links_desc = These external accounts are linked to your Gitea account. 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. account_links_not_available = There are currently no external accounts linked to your Gitea account.

@ -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 () { $(document).ready(function () {
csrf = $('meta[name=_csrf]').attr("content"); csrf = $('meta[name=_csrf]').attr("content");
suburl = $('meta[name=_suburl]').attr("content"); suburl = $('meta[name=_suburl]').attr("content");
@ -1643,6 +1767,8 @@ $(document).ready(function () {
initCtrlEnterSubmit(); initCtrlEnterSubmit();
initNavbarContentToggle(); initNavbarContentToggle();
initTopicbar(); initTopicbar();
initU2FAuth();
initU2FRegister();
// Repo clone url. // Repo clone url.
if ($('#repo-clone-url').length > 0) { if ($('#repo-clone-url').length > 0) {

@ -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/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> <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>
<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> <tr>
<td><a href="/vendor/assets/font-awesome/fonts/">font-awesome - fonts</a></td> <td><a href="/vendor/assets/font-awesome/fonts/">font-awesome - fonts</a></td>
<td><a href="http://fontawesome.io/license/">OFL</a></td> <td><a href="http://fontawesome.io/license/">OFL</a></td>

File diff suppressed because one or more lines are too long

@ -5,6 +5,8 @@
package routes package routes
import ( import (
"encoding/gob"
"net/http"
"os" "os"
"path" "path"
"time" "time"
@ -37,12 +39,13 @@ import (
"github.com/go-macaron/i18n" "github.com/go-macaron/i18n"
"github.com/go-macaron/session" "github.com/go-macaron/session"
"github.com/go-macaron/toolbox" "github.com/go-macaron/toolbox"
"github.com/tstranex/u2f"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
"net/http"
) )
// NewMacaron initializes Macaron instance. // NewMacaron initializes Macaron instance.
func NewMacaron() *macaron.Macaron { func NewMacaron() *macaron.Macaron {
gob.Register(&u2f.Challenge{})
m := macaron.New() m := macaron.New()
if !setting.DisableRouterLog { if !setting.DisableRouterLog {
m.Use(macaron.Logger()) m.Use(macaron.Logger())
@ -214,6 +217,12 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/scratch", user.TwoFactorScratch) m.Get("/scratch", user.TwoFactorScratch)
m.Post("/scratch", bindIgnErr(auth.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost) 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) }, reqSignOut)
m.Group("/user/settings", func() { m.Group("/user/settings", func() {
@ -235,6 +244,11 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/enroll", userSetting.EnrollTwoFactor) m.Get("/enroll", userSetting.EnrollTwoFactor)
m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost) 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.Group("/openid", func() {
m.Post("", bindIgnErr(auth.AddOpenIDForm{}), userSetting.OpenIDPost) m.Post("", bindIgnErr(auth.AddOpenIDForm{}), userSetting.OpenIDPost)
m.Post("/delete", userSetting.DeleteOpenID) m.Post("/delete", userSetting.DeleteOpenID)

@ -21,6 +21,7 @@ import (
"github.com/go-macaron/captcha" "github.com/go-macaron/captcha"
"github.com/markbates/goth" "github.com/markbates/goth"
"github.com/tstranex/u2f"
) )
const ( const (
@ -35,6 +36,7 @@ const (
tplTwofa base.TplName = "user/auth/twofa" tplTwofa base.TplName = "user/auth/twofa"
tplTwofaScratch base.TplName = "user/auth/twofa_scratch" tplTwofaScratch base.TplName = "user/auth/twofa_scratch"
tplLinkAccount base.TplName = "user/auth/link_account" tplLinkAccount base.TplName = "user/auth/link_account"
tplU2F base.TplName = "user/auth/u2f"
) )
// AutoSignIn reads cookie and try to auto-login. // AutoSignIn reads cookie and try to auto-login.
@ -159,7 +161,6 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) {
} }
return return
} }
// If this user is enrolled in 2FA, we can't sign the user in just yet. // If this user is enrolled in 2FA, we can't sign the user in just yet.
// Instead, redirect them to the 2FA authentication page. // Instead, redirect them to the 2FA authentication page.
_, err = models.GetTwoFactorByUID(u.ID) _, 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. // User needs to use 2FA, save data and redirect to 2FA page.
ctx.Session.Set("twofaUid", u.ID) ctx.Session.Set("twofaUid", u.ID)
ctx.Session.Set("twofaRemember", form.Remember) 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") 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{}) 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. // This handles the final part of the sign-in process of the user.
func handleSignIn(ctx *context.Context, u *models.User, remember bool) { func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
handleSignInFull(ctx, u, remember, true) 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 { if remember {
days := 86400 * setting.LogInRememberDays days := 86400 * setting.LogInRememberDays
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL) 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("openid_determined_username")
ctx.Session.Delete("twofaUid") ctx.Session.Delete("twofaUid")
ctx.Session.Delete("twofaRemember") ctx.Session.Delete("twofaRemember")
ctx.Session.Delete("u2fChallenge")
ctx.Session.Delete("linkAccount")
ctx.Session.Set("uid", u.ID) ctx.Session.Set("uid", u.ID)
ctx.Session.Set("uname", u.Name) 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() u.Language = ctx.Locale.Language()
if err := models.UpdateUserCols(u, "language"); err != nil { 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)) 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() u.SetLastLogin()
if err := models.UpdateUserCols(u, "last_login_unix"); err != nil { if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
ctx.ServerError("UpdateUserCols", err) ctx.ServerError("UpdateUserCols", err)
return return setting.AppSubURL + "/"
} }
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 { 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 { if obeyRedirect {
ctx.RedirectToFirst(redirectTo) ctx.RedirectToFirst(redirectTo)
} }
return return redirectTo
} }
if obeyRedirect { if obeyRedirect {
ctx.Redirect(setting.AppSubURL + "/") ctx.Redirect(setting.AppSubURL + "/")
} }
return setting.AppSubURL + "/"
} }
// SignInOAuth handles the OAuth2 login buttons // 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. // User needs to use 2FA, save data and redirect to 2FA page.
ctx.Session.Set("twofaUid", u.ID) ctx.Session.Set("twofaUid", u.ID)
ctx.Session.Set("twofaRemember", false) 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") 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("twofaRemember", signInForm.Remember)
ctx.Session.Set("linkAccount", true) 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") ctx.Redirect(setting.AppSubURL + "/user/two_factor")
} }

@ -33,6 +33,14 @@ func Security(ctx *context.Context) {
} }
} }
ctx.Data["TwofaEnrolled"] = enrolled 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) tokens, err := models.ListAccessTokens(ctx.User.ID)
if err != nil { if err != nil {

@ -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
}

@ -64,6 +64,9 @@
{{if .RequireDropzone}} {{if .RequireDropzone}}
<script src="{{AppSubUrl}}/vendor/plugins/dropzone/dropzone.js"></script> <script src="{{AppSubUrl}}/vendor/plugins/dropzone/dropzone.js"></script>
{{end}} {{end}}
{{if .RequireU2F}}
<script src="{{AppSubUrl}}/vendor/plugins/u2f/index.js"></script>
{{end}}
{{if .RequireTribute}} {{if .RequireTribute}}
<script src="{{AppSubUrl}}/vendor/plugins/tribute/tribute.min.js"></script> <script src="{{AppSubUrl}}/vendor/plugins/tribute/tribute.min.js"></script>

@ -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" .}}

@ -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>

@ -4,6 +4,7 @@
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
{{template "user/settings/security_twofa" .}} {{template "user/settings/security_twofa" .}}
{{template "user/settings/security_u2f" .}}
{{template "user/settings/security_accountlinks" .}} {{template "user/settings/security_accountlinks" .}}
{{if .EnableOpenIDSignIn}} {{if .EnableOpenIDSignIn}}
{{template "user/settings/security_openid" .}} {{template "user/settings/security_openid" .}}

@ -43,7 +43,7 @@
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<div class="required field {{if .Err_OpenID}}error{{end}}"> <div class="required field {{if .Err_OpenID}}error{{end}}">
<label for="openid">{{.i18n.Tr "settings.add_new_openid"}}</label> <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> </div>
<button class="ui green button"> <button class="ui green button">
{{.i18n.Tr "settings.add_openid"}} {{.i18n.Tr "settings.add_openid"}}

@ -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>

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

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

@ -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
}

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

@ -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"`
}

@ -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
}

@ -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
}

@ -1368,6 +1368,12 @@
"revision": "917f41c560270110ceb73c5b38be2a9127387071", "revision": "917f41c560270110ceb73c5b38be2a9127387071",
"revisionTime": "2016-03-11T05:04:36Z" "revisionTime": "2016-03-11T05:04:36Z"
}, },
{
"checksumSHA1": "NE1kNfAZ0AAXCUbwx196os/DSUE=",
"path": "github.com/tstranex/u2f",
"revision": "d21a03e0b1d9fc1df59ff54e7a513655c1748b0c",
"revisionTime": "2018-05-05T18:51:14Z"
},
{ {
"checksumSHA1": "MfWqWj0xRPdk1DpXCN0EXyBCa4Q=", "checksumSHA1": "MfWqWj0xRPdk1DpXCN0EXyBCa4Q=",
"path": "github.com/tinylib/msgp/msgp", "path": "github.com/tinylib/msgp/msgp",

Loading…
Cancel
Save