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. | ||||||
|  | 
 | ||||||
|  | [](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