mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-25 09:20:36 +00:00
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>
This commit is contained in:
parent
f933bcdfee
commit
951309f76a
34 changed files with 1599 additions and 9 deletions
|
@ -288,7 +288,7 @@ RESET_PASSWD_CODE_LIVE_MINUTES = 180
|
||||||
REGISTER_EMAIL_CONFIRM = false
|
REGISTER_EMAIL_CONFIRM = false
|
||||||
; Disallow registration, only allow admins to create accounts.
|
; Disallow registration, only allow admins to create accounts.
|
||||||
DISABLE_REGISTRATION = false
|
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
|
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||||
; User must sign in to view anything.
|
; User must sign in to view anything.
|
||||||
REQUIRE_SIGNIN_VIEW = false
|
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
|
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
|
||||||
|
}
|
||||||
|
|
7
models/fixtures/u2f_registration.yml
Normal file
7
models/fixtures/u2f_registration.yml
Normal file
|
@ -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
|
||||||
|
|
19
models/migrations/v65.go
Normal file
19
models/migrations/v65.go
Normal file
|
@ -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"}
|
||||||
|
|
120
models/u2f.go
Normal file
120
models/u2f.go
Normal file
|
@ -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
|
||||||
|
}
|
61
models/u2f_test.go
Normal file
61
models/u2f_test.go
Normal file
|
@ -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) {
|
||||||
|
@ -2201,7 +2327,7 @@ function initTopicbar() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var topicArray = topics.split(",");
|
var topicArray = topics.split(",");
|
||||||
|
|
||||||
var last = viewDiv.children("a").last();
|
var last = viewDiv.children("a").last();
|
||||||
for (var i=0;i < topicArray.length; i++) {
|
for (var i=0;i < topicArray.length; i++) {
|
||||||
$('<div class="ui green basic label topic" style="cursor:pointer;">'+topicArray[i]+'</div>').insertBefore(last)
|
$('<div class="ui green basic label topic" style="cursor:pointer;">'+topicArray[i]+'</div>').insertBefore(last)
|
||||||
|
|
5
public/vendor/librejs.html
vendored
5
public/vendor/librejs.html
vendored
|
@ -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>
|
||||||
|
|
1
public/vendor/plugins/u2f/index.js
vendored
Normal file
1
public/vendor/plugins/u2f/index.js
vendored
Normal file
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 {
|
||||||
|
|
99
routers/user/setting/security_u2f.go
Normal file
99
routers/user/setting/security_u2f.go
Normal file
|
@ -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>
|
||||||
|
|
||||||
|
|
22
templates/user/auth/u2f.tmpl
Normal file
22
templates/user/auth/u2f.tmpl
Normal file
|
@ -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" .}}
|
32
templates/user/auth/u2f_error.tmpl
Normal file
32
templates/user/auth/u2f_error.tmpl
Normal file
|
@ -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"}}
|
||||||
|
|
56
templates/user/settings/security_u2f.tmpl
Normal file
56
templates/user/settings/security_u2f.tmpl
Normal file
|
@ -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>
|
||||||
|
|
21
vendor/github.com/tstranex/u2f/LICENSE
generated
vendored
Normal file
21
vendor/github.com/tstranex/u2f/LICENSE
generated
vendored
Normal file
|
@ -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.
|
97
vendor/github.com/tstranex/u2f/README.md
generated
vendored
Normal file
97
vendor/github.com/tstranex/u2f/README.md
generated
vendored
Normal file
|
@ -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.
|
136
vendor/github.com/tstranex/u2f/auth.go
generated
vendored
Normal file
136
vendor/github.com/tstranex/u2f/auth.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
89
vendor/github.com/tstranex/u2f/certs.go
generated
vendored
Normal file
89
vendor/github.com/tstranex/u2f/certs.go
generated
vendored
Normal file
|
@ -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))
|
87
vendor/github.com/tstranex/u2f/messages.go
generated
vendored
Normal file
87
vendor/github.com/tstranex/u2f/messages.go
generated
vendored
Normal file
|
@ -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"`
|
||||||
|
}
|
230
vendor/github.com/tstranex/u2f/register.go
generated
vendored
Normal file
230
vendor/github.com/tstranex/u2f/register.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
125
vendor/github.com/tstranex/u2f/util.go
generated
vendored
Normal file
125
vendor/github.com/tstranex/u2f/util.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
|
@ -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…
Reference in a new issue