* 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>tags/v1.5.0-dev
@@ -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 |
@@ -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`. |
@@ -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> |
@@ -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 | |||
} |
@@ -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), | |||
// v64 -> v65 | |||
NewMigration("add multiple assignees", addMultipleAssignees), | |||
// v65 -> v66 | |||
NewMigration("add u2f", addU2FReg), | |||
} | |||
// 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(Reaction), | |||
new(IssueAssignees), | |||
new(U2FRegistration), | |||
) | |||
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(®s) | |||
} | |||
// 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 { | |||
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, | |||
} | |||
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 |
@@ -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. |
@@ -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) |
@@ -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> |
@@ -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) |
@@ -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") | |||
} | |||
@@ -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 { |
@@ -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}} | |||
<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> | |||
@@ -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"> | |||
{{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" .}} |
@@ -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"}} |
@@ -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, ®.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", | |||
"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", |