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
 | 
			
		||||
; Disallow registration, only allow admins to create accounts.
 | 
			
		||||
DISABLE_REGISTRATION = false
 | 
			
		||||
; Allow registration only using third part services, it works only when DISABLE_REGISTRATION is false 
 | 
			
		||||
; Allow registration only using third part services, it works only when DISABLE_REGISTRATION is false
 | 
			
		||||
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
 | 
			
		||||
; User must sign in to view anything.
 | 
			
		||||
REQUIRE_SIGNIN_VIEW = false
 | 
			
		||||
| 
						 | 
				
			
			@ -570,6 +570,14 @@ MAX_RESPONSE_ITEMS = 50
 | 
			
		|||
LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR
 | 
			
		||||
NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어
 | 
			
		||||
 | 
			
		||||
[U2F]
 | 
			
		||||
; Two Factor authentication with security keys
 | 
			
		||||
; https://developers.yubico.com/U2F/App_ID.html
 | 
			
		||||
APP_ID = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s
 | 
			
		||||
; Comma seperated list of truisted facets
 | 
			
		||||
TRUSTED_FACETS = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
; Used for datetimepicker
 | 
			
		||||
[i18n.datelang]
 | 
			
		||||
en-US = en
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -272,6 +272,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
 | 
			
		|||
- `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view.
 | 
			
		||||
- `GC_ARGS`: **\<empty\>**: Arguments for command `git gc`, e.g. `--aggressive --auto`.
 | 
			
		||||
 | 
			
		||||
## U2F (`U2F`)
 | 
			
		||||
- `APP_ID`: **`ROOT_URL`**: Declares the facet of the application. Requires HTTPS.
 | 
			
		||||
- `TRUSTED_FACETS`: List of additional facets which are trusted. This is not support by all browsers.
 | 
			
		||||
 | 
			
		||||
## Markup (`markup`)
 | 
			
		||||
 | 
			
		||||
Gitea can support Markup using external tools. The example below will add a markup named `asciidoc`.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -535,6 +535,15 @@ _Symbols used in table:_
 | 
			
		|||
      <td>✓</td>
 | 
			
		||||
      <td>✓</td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td>FIDO U2F (2FA)</td>
 | 
			
		||||
      <td>✓</td>
 | 
			
		||||
      <td>✘</td>
 | 
			
		||||
      <td>✓</td>
 | 
			
		||||
      <td>✓</td>
 | 
			
		||||
      <td>✓</td>
 | 
			
		||||
      <td>✓</td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <tr>
 | 
			
		||||
      <td>Webhook support</td>
 | 
			
		||||
      <td>✓</td>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1237,3 +1237,25 @@ func IsErrExternalLoginUserNotExist(err error) bool {
 | 
			
		|||
func (err ErrExternalLoginUserNotExist) Error() string {
 | 
			
		||||
	return fmt.Sprintf("external login user link does not exists [userID: %d, loginSourceID: %d]", err.UserID, err.LoginSourceID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ____ ________________________________              .__          __                 __  .__
 | 
			
		||||
// |    |   \_____  \_   _____/\______   \ ____   ____ |__| _______/  |_____________ _/  |_|__| ____   ____
 | 
			
		||||
// |    |   //  ____/|    __)   |       _// __ \ / ___\|  |/  ___/\   __\_  __ \__  \\   __\  |/  _ \ /    \
 | 
			
		||||
// |    |  //       \|     \    |    |   \  ___// /_/  >  |\___ \  |  |  |  | \// __ \|  | |  (  <_> )   |  \
 | 
			
		||||
// |______/ \_______ \___  /    |____|_  /\___  >___  /|__/____  > |__|  |__|  (____  /__| |__|\____/|___|  /
 | 
			
		||||
// \/   \/            \/     \/_____/         \/                   \/                    \/
 | 
			
		||||
 | 
			
		||||
// ErrU2FRegistrationNotExist represents a "ErrU2FRegistrationNotExist" kind of error.
 | 
			
		||||
type ErrU2FRegistrationNotExist struct {
 | 
			
		||||
	ID int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (err ErrU2FRegistrationNotExist) Error() string {
 | 
			
		||||
	return fmt.Sprintf("U2F registration does not exist [id: %d]", err.ID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsErrU2FRegistrationNotExist checks if an error is a ErrU2FRegistrationNotExist.
 | 
			
		||||
func IsErrU2FRegistrationNotExist(err error) bool {
 | 
			
		||||
	_, ok := err.(ErrU2FRegistrationNotExist)
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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),
 | 
			
		||||
	// v64 -> v65
 | 
			
		||||
	NewMigration("add multiple assignees", addMultipleAssignees),
 | 
			
		||||
	// v65 -> v66
 | 
			
		||||
	NewMigration("add u2f", addU2FReg),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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(Reaction),
 | 
			
		||||
		new(IssueAssignees),
 | 
			
		||||
		new(U2FRegistration),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	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 {
 | 
			
		||||
	return validate(errs, ctx.Data, f, ctx.Locale)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// U2FRegistrationForm for reserving an U2F name
 | 
			
		||||
type U2FRegistrationForm struct {
 | 
			
		||||
	Name string `binding:"Required"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate valideates the fields
 | 
			
		||||
func (f *U2FRegistrationForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
 | 
			
		||||
	return validate(errs, ctx.Data, f, ctx.Locale)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// U2FDeleteForm for deleting U2F keys
 | 
			
		||||
type U2FDeleteForm struct {
 | 
			
		||||
	ID int64 `binding:"Required"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate valideates the fields
 | 
			
		||||
func (f *U2FDeleteForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
 | 
			
		||||
	return validate(errs, ctx.Data, f, ctx.Locale)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -521,6 +521,11 @@ var (
 | 
			
		|||
		MaxResponseItems:      50,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	U2F = struct {
 | 
			
		||||
		AppID         string
 | 
			
		||||
		TrustedFacets []string
 | 
			
		||||
	}{}
 | 
			
		||||
 | 
			
		||||
	// I18n settings
 | 
			
		||||
	Langs     []string
 | 
			
		||||
	Names     []string
 | 
			
		||||
| 
						 | 
				
			
			@ -1135,6 +1140,9 @@ func NewContext() {
 | 
			
		|||
			IsInputFile:    sec.Key("IS_INPUT_FILE").MustBool(false),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	sec = Cfg.Section("U2F")
 | 
			
		||||
	U2F.TrustedFacets, _ = shellquote.Split(sec.Key("TRUSTED_FACETS").MustString(strings.TrimRight(AppURL, "/")))
 | 
			
		||||
	U2F.AppID = sec.Key("APP_ID").MustString(strings.TrimRight(AppURL, "/"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Service settings
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,19 @@ twofa = Two-Factor Authentication
 | 
			
		|||
twofa_scratch = Two-Factor Scratch Code
 | 
			
		||||
passcode = Passcode
 | 
			
		||||
 | 
			
		||||
u2f_insert_key = Insert your security key
 | 
			
		||||
u2f_sign_in = Press the button on your security key. If you can't find a button, re-insert it.
 | 
			
		||||
u2f_press_button = Please press the button on your security key…
 | 
			
		||||
u2f_use_twofa = Use a two-factor code from your phone
 | 
			
		||||
u2f_error = We can't read your security key!
 | 
			
		||||
u2f_unsupported_browser = Your browser don't support U2F keys. Please try another browser.
 | 
			
		||||
u2f_error_1 = An unknown error occured. Please retry.
 | 
			
		||||
u2f_error_2 = Please make sure that you're using an encrypted connection (https://) and visiting the correct URL.
 | 
			
		||||
u2f_error_3 = The server could not proceed your request.
 | 
			
		||||
u2f_error_4 = The presented key is not eligible for this request. If you try to register it, make sure that the key isn't already registered.
 | 
			
		||||
u2f_error_5 = Timeout reached before your key could be read. Please reload to retry.
 | 
			
		||||
u2f_reload = Reload
 | 
			
		||||
 | 
			
		||||
repository = Repository
 | 
			
		||||
organization = Organization
 | 
			
		||||
mirror = Mirror
 | 
			
		||||
| 
						 | 
				
			
			@ -320,6 +333,7 @@ twofa = Two-Factor Authentication
 | 
			
		|||
account_link = Linked Accounts
 | 
			
		||||
organization = Organizations
 | 
			
		||||
uid = Uid
 | 
			
		||||
u2f = Security Keys
 | 
			
		||||
 | 
			
		||||
public_profile = Public Profile
 | 
			
		||||
profile_desc = Your email address will be used for notifications and other operations.
 | 
			
		||||
| 
						 | 
				
			
			@ -449,6 +463,14 @@ then_enter_passcode = And enter the passcode shown in the application:
 | 
			
		|||
passcode_invalid = The passcode is incorrect. Try again.
 | 
			
		||||
twofa_enrolled = Your account has been enrolled into two-factor authentication. Store your scratch token (%s) in a safe place as it is only shown once!
 | 
			
		||||
 | 
			
		||||
u2f_desc = Security keys are hardware devices containing cryptograhic keys. They could be used for two factor authentication. The security key must support the <a href="https://fidoalliance.org/">FIDO U2F</a> standard.
 | 
			
		||||
u2f_require_twofa = Two-Factor-Authentication must be enrolled in order to use security keys.
 | 
			
		||||
u2f_register_key = Add Security Key
 | 
			
		||||
u2f_nickname = Nickname
 | 
			
		||||
u2f_press_button = Press the button on your security key to register it.
 | 
			
		||||
u2f_delete_key = Remove Security Key
 | 
			
		||||
u2f_delete_key_desc= If you remove a security key you cannot login with it anymore. Are you sure?
 | 
			
		||||
 | 
			
		||||
manage_account_links = Manage Linked Accounts
 | 
			
		||||
manage_account_links_desc = These external accounts are linked to your Gitea account.
 | 
			
		||||
account_links_not_available = There are currently no external accounts linked to your Gitea account.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1432,6 +1432,130 @@ function initCodeView() {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initU2FAuth() {
 | 
			
		||||
    if($('#wait-for-key').length === 0) {
 | 
			
		||||
        return
 | 
			
		||||
    }
 | 
			
		||||
    u2fApi.ensureSupport()
 | 
			
		||||
        .then(function () {
 | 
			
		||||
            $.getJSON('/user/u2f/challenge').success(function(req) {
 | 
			
		||||
                u2fApi.sign(req.appId, req.challenge, req.registeredKeys, 30)
 | 
			
		||||
                    .then(u2fSigned)
 | 
			
		||||
                    .catch(function (err) {
 | 
			
		||||
                        if(err === undefined) {
 | 
			
		||||
                            u2fError(1);
 | 
			
		||||
                            return
 | 
			
		||||
                        }
 | 
			
		||||
                        u2fError(err.metaData.code);
 | 
			
		||||
                    });
 | 
			
		||||
            });
 | 
			
		||||
        }).catch(function () {
 | 
			
		||||
            // Fallback in case browser do not support U2F
 | 
			
		||||
            window.location.href = "/user/two_factor"
 | 
			
		||||
        })
 | 
			
		||||
}
 | 
			
		||||
function u2fSigned(resp) {
 | 
			
		||||
    $.ajax({
 | 
			
		||||
        url:'/user/u2f/sign',
 | 
			
		||||
        type:"POST",
 | 
			
		||||
        headers: {"X-Csrf-Token": csrf},
 | 
			
		||||
        data: JSON.stringify(resp),
 | 
			
		||||
        contentType:"application/json; charset=utf-8",
 | 
			
		||||
    }).done(function(res){
 | 
			
		||||
        window.location.replace(res);
 | 
			
		||||
    }).fail(function (xhr, textStatus) {
 | 
			
		||||
        u2fError(1);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function u2fRegistered(resp) {
 | 
			
		||||
    if (checkError(resp)) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    $.ajax({
 | 
			
		||||
        url:'/user/settings/security/u2f/register',
 | 
			
		||||
        type:"POST",
 | 
			
		||||
        headers: {"X-Csrf-Token": csrf},
 | 
			
		||||
        data: JSON.stringify(resp),
 | 
			
		||||
        contentType:"application/json; charset=utf-8",
 | 
			
		||||
        success: function(){
 | 
			
		||||
            window.location.reload();
 | 
			
		||||
        },
 | 
			
		||||
        fail: function (xhr, textStatus) {
 | 
			
		||||
            u2fError(1);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkError(resp) {
 | 
			
		||||
    if (!('errorCode' in resp)) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    if (resp.errorCode === 0) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
    u2fError(resp.errorCode);
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function u2fError(errorType) {
 | 
			
		||||
    var u2fErrors = {
 | 
			
		||||
        'browser': $('#unsupported-browser'),
 | 
			
		||||
        1: $('#u2f-error-1'),
 | 
			
		||||
        2: $('#u2f-error-2'),
 | 
			
		||||
        3: $('#u2f-error-3'),
 | 
			
		||||
        4: $('#u2f-error-4'),
 | 
			
		||||
        5: $('.u2f-error-5')
 | 
			
		||||
    };
 | 
			
		||||
    u2fErrors[errorType].removeClass('hide');
 | 
			
		||||
    for(var type in u2fErrors){
 | 
			
		||||
        if(type != errorType){
 | 
			
		||||
            u2fErrors[type].addClass('hide');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    $('#u2f-error').modal('show');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initU2FRegister() {
 | 
			
		||||
    $('#register-device').modal({allowMultiple: false});
 | 
			
		||||
    $('#u2f-error').modal({allowMultiple: false});
 | 
			
		||||
    $('#register-security-key').on('click', function(e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        u2fApi.ensureSupport()
 | 
			
		||||
            .then(u2fRegisterRequest)
 | 
			
		||||
            .catch(function() {
 | 
			
		||||
                u2fError('browser');
 | 
			
		||||
            })
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function u2fRegisterRequest() {
 | 
			
		||||
    $.post("/user/settings/security/u2f/request_register", {
 | 
			
		||||
        "_csrf": csrf,
 | 
			
		||||
        "name": $('#nickname').val()
 | 
			
		||||
    }).success(function(req) {
 | 
			
		||||
        $("#nickname").closest("div.field").removeClass("error");
 | 
			
		||||
        $('#register-device').modal('show');
 | 
			
		||||
        if(req.registeredKeys === null) {
 | 
			
		||||
            req.registeredKeys = []
 | 
			
		||||
        }
 | 
			
		||||
        u2fApi.register(req.appId, req.registerRequests, req.registeredKeys, 30)
 | 
			
		||||
            .then(u2fRegistered)
 | 
			
		||||
            .catch(function (reason) {
 | 
			
		||||
                if(reason === undefined) {
 | 
			
		||||
                    u2fError(1);
 | 
			
		||||
                    return
 | 
			
		||||
                }
 | 
			
		||||
                u2fError(reason.metaData.code);
 | 
			
		||||
            });
 | 
			
		||||
    }).fail(function(xhr, status, error) {
 | 
			
		||||
        if(xhr.status === 409) {
 | 
			
		||||
            $("#nickname").closest("div.field").addClass("error");
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$(document).ready(function () {
 | 
			
		||||
    csrf = $('meta[name=_csrf]').attr("content");
 | 
			
		||||
    suburl = $('meta[name=_suburl]').attr("content");
 | 
			
		||||
| 
						 | 
				
			
			@ -1643,6 +1767,8 @@ $(document).ready(function () {
 | 
			
		|||
    initCtrlEnterSubmit();
 | 
			
		||||
    initNavbarContentToggle();
 | 
			
		||||
    initTopicbar();
 | 
			
		||||
    initU2FAuth();
 | 
			
		||||
    initU2FRegister();
 | 
			
		||||
 | 
			
		||||
    // Repo clone url.
 | 
			
		||||
    if ($('#repo-clone-url').length > 0) {
 | 
			
		||||
| 
						 | 
				
			
			@ -2201,7 +2327,7 @@ function initTopicbar() {
 | 
			
		|||
                    return
 | 
			
		||||
                }
 | 
			
		||||
                var topicArray = topics.split(",");
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                var last = viewDiv.children("a").last();
 | 
			
		||||
                for (var i=0;i < topicArray.length; i++) {
 | 
			
		||||
                    $('<div class="ui green basic label topic" style="cursor:pointer;">'+topicArray[i]+'</div>').insertBefore(last)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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/archive/v1.4.20.tar.gz">pdf.js-v1.4.20.tar.gz</a></td>
 | 
			
		||||
        </tr>
 | 
			
		||||
		<tr>
 | 
			
		||||
		  <td><a href="/vendor/plugins/u2f/">u2f-api</a></td>
 | 
			
		||||
		  <td><a href="https://github.com/go-gitea/u2f-api/blob/master/LICENSE">Expat</a></td>
 | 
			
		||||
		  <td><a href="https://github.com/go-gitea/u2f-api/archive/v1.0.8.zip">u2f-api-1.0.8.zip</a></td>
 | 
			
		||||
		</tr>
 | 
			
		||||
        <tr>
 | 
			
		||||
          <td><a href="/vendor/assets/font-awesome/fonts/">font-awesome - fonts</a></td>
 | 
			
		||||
          <td><a href="http://fontawesome.io/license/">OFL</a></td>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/gob"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"time"
 | 
			
		||||
| 
						 | 
				
			
			@ -37,12 +39,13 @@ import (
 | 
			
		|||
	"github.com/go-macaron/i18n"
 | 
			
		||||
	"github.com/go-macaron/session"
 | 
			
		||||
	"github.com/go-macaron/toolbox"
 | 
			
		||||
	"github.com/tstranex/u2f"
 | 
			
		||||
	"gopkg.in/macaron.v1"
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// NewMacaron initializes Macaron instance.
 | 
			
		||||
func NewMacaron() *macaron.Macaron {
 | 
			
		||||
	gob.Register(&u2f.Challenge{})
 | 
			
		||||
	m := macaron.New()
 | 
			
		||||
	if !setting.DisableRouterLog {
 | 
			
		||||
		m.Use(macaron.Logger())
 | 
			
		||||
| 
						 | 
				
			
			@ -214,6 +217,12 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
			
		|||
			m.Get("/scratch", user.TwoFactorScratch)
 | 
			
		||||
			m.Post("/scratch", bindIgnErr(auth.TwoFactorScratchAuthForm{}), user.TwoFactorScratchPost)
 | 
			
		||||
		})
 | 
			
		||||
		m.Group("/u2f", func() {
 | 
			
		||||
			m.Get("", user.U2F)
 | 
			
		||||
			m.Get("/challenge", user.U2FChallenge)
 | 
			
		||||
			m.Post("/sign", bindIgnErr(u2f.SignResponse{}), user.U2FSign)
 | 
			
		||||
 | 
			
		||||
		})
 | 
			
		||||
	}, reqSignOut)
 | 
			
		||||
 | 
			
		||||
	m.Group("/user/settings", func() {
 | 
			
		||||
| 
						 | 
				
			
			@ -235,6 +244,11 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
			
		|||
				m.Get("/enroll", userSetting.EnrollTwoFactor)
 | 
			
		||||
				m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost)
 | 
			
		||||
			})
 | 
			
		||||
			m.Group("/u2f", func() {
 | 
			
		||||
				m.Post("/request_register", bindIgnErr(auth.U2FRegistrationForm{}), userSetting.U2FRegister)
 | 
			
		||||
				m.Post("/register", bindIgnErr(u2f.RegisterResponse{}), userSetting.U2FRegisterPost)
 | 
			
		||||
				m.Post("/delete", bindIgnErr(auth.U2FDeleteForm{}), userSetting.U2FDelete)
 | 
			
		||||
			})
 | 
			
		||||
			m.Group("/openid", func() {
 | 
			
		||||
				m.Post("", bindIgnErr(auth.AddOpenIDForm{}), userSetting.OpenIDPost)
 | 
			
		||||
				m.Post("/delete", userSetting.DeleteOpenID)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ import (
 | 
			
		|||
 | 
			
		||||
	"github.com/go-macaron/captcha"
 | 
			
		||||
	"github.com/markbates/goth"
 | 
			
		||||
	"github.com/tstranex/u2f"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +36,7 @@ const (
 | 
			
		|||
	tplTwofa          base.TplName = "user/auth/twofa"
 | 
			
		||||
	tplTwofaScratch   base.TplName = "user/auth/twofa_scratch"
 | 
			
		||||
	tplLinkAccount    base.TplName = "user/auth/link_account"
 | 
			
		||||
	tplU2F            base.TplName = "user/auth/u2f"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AutoSignIn reads cookie and try to auto-login.
 | 
			
		||||
| 
						 | 
				
			
			@ -159,7 +161,6 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) {
 | 
			
		|||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If this user is enrolled in 2FA, we can't sign the user in just yet.
 | 
			
		||||
	// Instead, redirect them to the 2FA authentication page.
 | 
			
		||||
	_, err = models.GetTwoFactorByUID(u.ID)
 | 
			
		||||
| 
						 | 
				
			
			@ -175,6 +176,13 @@ func SignInPost(ctx *context.Context, form auth.SignInForm) {
 | 
			
		|||
	// User needs to use 2FA, save data and redirect to 2FA page.
 | 
			
		||||
	ctx.Session.Set("twofaUid", u.ID)
 | 
			
		||||
	ctx.Session.Set("twofaRemember", form.Remember)
 | 
			
		||||
 | 
			
		||||
	regs, err := models.GetU2FRegistrationsByUID(u.ID)
 | 
			
		||||
	if err == nil && len(regs) > 0 {
 | 
			
		||||
		ctx.Redirect(setting.AppSubURL + "/user/u2f")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Redirect(setting.AppSubURL + "/user/two_factor")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -317,12 +325,115 @@ func TwoFactorScratchPost(ctx *context.Context, form auth.TwoFactorScratchAuthFo
 | 
			
		|||
	ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, auth.TwoFactorScratchAuthForm{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// U2F shows the U2F login page
 | 
			
		||||
func U2F(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["Title"] = ctx.Tr("twofa")
 | 
			
		||||
	ctx.Data["RequireU2F"] = true
 | 
			
		||||
	// Check auto-login.
 | 
			
		||||
	if checkAutoLogin(ctx) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Ensure user is in a 2FA session.
 | 
			
		||||
	if ctx.Session.Get("twofaUid") == nil {
 | 
			
		||||
		ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.HTML(200, tplU2F)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// U2FChallenge submits a sign challenge to the browser
 | 
			
		||||
func U2FChallenge(ctx *context.Context) {
 | 
			
		||||
	// Ensure user is in a U2F session.
 | 
			
		||||
	idSess := ctx.Session.Get("twofaUid")
 | 
			
		||||
	if idSess == nil {
 | 
			
		||||
		ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	id := idSess.(int64)
 | 
			
		||||
	regs, err := models.GetU2FRegistrationsByUID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("UserSignIn", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if len(regs) == 0 {
 | 
			
		||||
		ctx.ServerError("UserSignIn", errors.New("no device registered"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	challenge, err := u2f.NewChallenge(setting.U2F.AppID, setting.U2F.TrustedFacets)
 | 
			
		||||
	if err = ctx.Session.Set("u2fChallenge", challenge); err != nil {
 | 
			
		||||
		ctx.ServerError("UserSignIn", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.JSON(200, challenge.SignRequest(regs.ToRegistrations()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// U2FSign authenticates the user by signResp
 | 
			
		||||
func U2FSign(ctx *context.Context, signResp u2f.SignResponse) {
 | 
			
		||||
	challSess := ctx.Session.Get("u2fChallenge")
 | 
			
		||||
	idSess := ctx.Session.Get("twofaUid")
 | 
			
		||||
	if challSess == nil || idSess == nil {
 | 
			
		||||
		ctx.ServerError("UserSignIn", errors.New("not in U2F session"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	challenge := challSess.(*u2f.Challenge)
 | 
			
		||||
	id := idSess.(int64)
 | 
			
		||||
	regs, err := models.GetU2FRegistrationsByUID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("UserSignIn", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	for _, reg := range regs {
 | 
			
		||||
		r, err := reg.Parse()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(4, "parsing u2f registration: %v", err)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		newCounter, authErr := r.Authenticate(signResp, *challenge, reg.Counter)
 | 
			
		||||
		if authErr == nil {
 | 
			
		||||
			reg.Counter = newCounter
 | 
			
		||||
			user, err := models.GetUserByID(id)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.ServerError("UserSignIn", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			remember := ctx.Session.Get("twofaRemember").(bool)
 | 
			
		||||
			if err := reg.UpdateCounter(); err != nil {
 | 
			
		||||
				ctx.ServerError("UserSignIn", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if ctx.Session.Get("linkAccount") != nil {
 | 
			
		||||
				gothUser := ctx.Session.Get("linkAccountGothUser")
 | 
			
		||||
				if gothUser == nil {
 | 
			
		||||
					ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				err = models.LinkAccountToUser(user, gothUser.(goth.User))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					ctx.ServerError("UserSignIn", err)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			redirect := handleSignInFull(ctx, user, remember, false)
 | 
			
		||||
			if redirect == "" {
 | 
			
		||||
				redirect = setting.AppSubURL + "/"
 | 
			
		||||
			}
 | 
			
		||||
			ctx.PlainText(200, []byte(redirect))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Error(401)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This handles the final part of the sign-in process of the user.
 | 
			
		||||
func handleSignIn(ctx *context.Context, u *models.User, remember bool) {
 | 
			
		||||
	handleSignInFull(ctx, u, remember, true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) {
 | 
			
		||||
func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyRedirect bool) string {
 | 
			
		||||
	if remember {
 | 
			
		||||
		days := 86400 * setting.LogInRememberDays
 | 
			
		||||
		ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL)
 | 
			
		||||
| 
						 | 
				
			
			@ -336,6 +447,8 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
 | 
			
		|||
	ctx.Session.Delete("openid_determined_username")
 | 
			
		||||
	ctx.Session.Delete("twofaUid")
 | 
			
		||||
	ctx.Session.Delete("twofaRemember")
 | 
			
		||||
	ctx.Session.Delete("u2fChallenge")
 | 
			
		||||
	ctx.Session.Delete("linkAccount")
 | 
			
		||||
	ctx.Session.Set("uid", u.ID)
 | 
			
		||||
	ctx.Session.Set("uname", u.Name)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -345,7 +458,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
 | 
			
		|||
		u.Language = ctx.Locale.Language()
 | 
			
		||||
		if err := models.UpdateUserCols(u, "language"); err != nil {
 | 
			
		||||
			log.Error(4, fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language))
 | 
			
		||||
			return
 | 
			
		||||
			return setting.AppSubURL + "/"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -358,7 +471,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
 | 
			
		|||
	u.SetLastLogin()
 | 
			
		||||
	if err := models.UpdateUserCols(u, "last_login_unix"); err != nil {
 | 
			
		||||
		ctx.ServerError("UpdateUserCols", err)
 | 
			
		||||
		return
 | 
			
		||||
		return setting.AppSubURL + "/"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
 | 
			
		||||
| 
						 | 
				
			
			@ -366,12 +479,13 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR
 | 
			
		|||
		if obeyRedirect {
 | 
			
		||||
			ctx.RedirectToFirst(redirectTo)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
		return redirectTo
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if obeyRedirect {
 | 
			
		||||
		ctx.Redirect(setting.AppSubURL + "/")
 | 
			
		||||
	}
 | 
			
		||||
	return setting.AppSubURL + "/"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SignInOAuth handles the OAuth2 login buttons
 | 
			
		||||
| 
						 | 
				
			
			@ -467,6 +581,14 @@ func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context
 | 
			
		|||
	// User needs to use 2FA, save data and redirect to 2FA page.
 | 
			
		||||
	ctx.Session.Set("twofaUid", u.ID)
 | 
			
		||||
	ctx.Session.Set("twofaRemember", false)
 | 
			
		||||
 | 
			
		||||
	// If U2F is enrolled -> Redirect to U2F instead
 | 
			
		||||
	regs, err := models.GetU2FRegistrationsByUID(u.ID)
 | 
			
		||||
	if err == nil && len(regs) > 0 {
 | 
			
		||||
		ctx.Redirect(setting.AppSubURL + "/user/u2f")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Redirect(setting.AppSubURL + "/user/two_factor")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -593,6 +715,13 @@ func LinkAccountPostSignIn(ctx *context.Context, signInForm auth.SignInForm) {
 | 
			
		|||
	ctx.Session.Set("twofaRemember", signInForm.Remember)
 | 
			
		||||
	ctx.Session.Set("linkAccount", true)
 | 
			
		||||
 | 
			
		||||
	// If U2F is enrolled -> Redirect to U2F instead
 | 
			
		||||
	regs, err := models.GetU2FRegistrationsByUID(u.ID)
 | 
			
		||||
	if err == nil && len(regs) > 0 {
 | 
			
		||||
		ctx.Redirect(setting.AppSubURL + "/user/u2f")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Redirect(setting.AppSubURL + "/user/two_factor")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,14 @@ func Security(ctx *context.Context) {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Data["TwofaEnrolled"] = enrolled
 | 
			
		||||
	if enrolled {
 | 
			
		||||
		ctx.Data["U2FRegistrations"], err = models.GetU2FRegistrationsByUID(ctx.User.ID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.ServerError("GetU2FRegistrationsByUID", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Data["RequireU2F"] = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tokens, err := models.ListAccessTokens(ctx.User.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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}}
 | 
			
		||||
	<script src="{{AppSubUrl}}/vendor/plugins/dropzone/dropzone.js"></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{if .RequireU2F}}
 | 
			
		||||
	<script src="{{AppSubUrl}}/vendor/plugins/u2f/index.js"></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{if .RequireTribute}}
 | 
			
		||||
	<script src="{{AppSubUrl}}/vendor/plugins/tribute/tribute.min.js"></script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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">
 | 
			
		||||
		{{template "base/alert" .}}
 | 
			
		||||
		{{template "user/settings/security_twofa" .}}
 | 
			
		||||
		{{template "user/settings/security_u2f" .}}
 | 
			
		||||
		{{template "user/settings/security_accountlinks" .}}
 | 
			
		||||
		{{if .EnableOpenIDSignIn}}
 | 
			
		||||
		{{template "user/settings/security_openid" .}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@
 | 
			
		|||
		{{.CsrfTokenHtml}}
 | 
			
		||||
		<div class="required field {{if .Err_OpenID}}error{{end}}">
 | 
			
		||||
			<label for="openid">{{.i18n.Tr "settings.add_new_openid"}}</label>
 | 
			
		||||
			<input id="openid" name="openid" type="text" autofocus required>
 | 
			
		||||
			<input id="openid" name="openid" type="text" required>
 | 
			
		||||
		</div>
 | 
			
		||||
		<button class="ui green button">
 | 
			
		||||
			{{.i18n.Tr "settings.add_openid"}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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",
 | 
			
		||||
			"revisionTime": "2016-03-11T05:04:36Z"
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"checksumSHA1": "NE1kNfAZ0AAXCUbwx196os/DSUE=",
 | 
			
		||||
			"path": "github.com/tstranex/u2f",
 | 
			
		||||
			"revision": "d21a03e0b1d9fc1df59ff54e7a513655c1748b0c",
 | 
			
		||||
			"revisionTime": "2018-05-05T18:51:14Z"
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"checksumSHA1": "MfWqWj0xRPdk1DpXCN0EXyBCa4Q=",
 | 
			
		||||
			"path": "github.com/tinylib/msgp/msgp",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue