Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for FIDO U2F #3971

Merged
merged 19 commits into from May 19, 2018
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion custom/conf/app.ini.sample
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a U2F section to the "Config Cheatsheet" page in the docs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

; Two Factor authentication with security keys
; https://developers.yubico.com/U2F/App_ID.html
APP_ID = https://example.com
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app.ini.sample should contain values that are same as default

; Comma seperated list of truisted facets
TRUSTED_FACETS = https://localhost:3000,https://192.168.178.18:3000


; Used for datetimepicker
[i18n.datelang]
en-US = en
Expand Down
4 changes: 4 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Expand Up @@ -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`.
Expand Down
22 changes: 22 additions & 0 deletions models/error.go
Expand Up @@ -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 changes: 7 additions & 0 deletions models/fixtures/u2f_registration.yml
@@ -0,0 +1,7 @@
-
id: 1
name: "U2F Key"
user_id: 1
counter: 0
created_unix: 946684800
updated_unix: 946684800
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions models/migrations/v65.go
@@ -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{})
}
1 change: 1 addition & 0 deletions models/models.go
Expand Up @@ -120,6 +120,7 @@ func init() {
new(LFSLock),
new(Reaction),
new(IssueAssignees),
new(U2FRegistration),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
120 changes: 120 additions & 0 deletions models/u2f.go
@@ -0,0 +1,120 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package models

import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"

"github.com/tstranex/u2f"
)

// U2FRegistration represents the registration data and counter of a security key
type U2FRegistration struct {
ID int64 `xorm:"pk autoincr"`
Name string
UserID int64 `xorm:"INDEX"`
Raw []byte
Counter uint32
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
}

// TableName returns a better table name for U2FRegistration
func (reg U2FRegistration) TableName() string {
return "u2f_registration"
}

// Parse will convert the db entry U2FRegistration to an u2f.Registration struct
func (reg *U2FRegistration) Parse() (*u2f.Registration, error) {
r := new(u2f.Registration)
return r, r.UnmarshalBinary(reg.Raw)
}

func (reg *U2FRegistration) updateCounter(e Engine) error {
_, err := e.ID(reg.ID).Cols("counter").Update(reg)
return err
}

// UpdateCounter will update the database value of counter
func (reg *U2FRegistration) UpdateCounter() error {
return reg.updateCounter(x)
}

// U2FRegistrationList is a list of *U2FRegistration
type U2FRegistrationList []*U2FRegistration

// ToRegistrations will convert all U2FRegistrations to u2f.Registrations
func (list U2FRegistrationList) ToRegistrations() []u2f.Registration {
regs := make([]u2f.Registration, len(list))
for _, reg := range list {
r, err := reg.Parse()
if err != nil {
log.Fatal(4, "parsing u2f registration: %v", err)
continue
}
regs = append(regs, *r)
}

return regs
}

func getU2FRegistrationsByUID(e Engine, uid int64) (U2FRegistrationList, error) {
regs := make(U2FRegistrationList, 0)
return regs, e.Where("user_id = ?", uid).Find(&regs)
}

// GetU2FRegistrationByID returns U2F registration by id
func GetU2FRegistrationByID(id int64) (*U2FRegistration, error) {
return getU2FRegistrationByID(x, id)
}

func getU2FRegistrationByID(e Engine, id int64) (*U2FRegistration, error) {
reg := new(U2FRegistration)
if found, err := e.ID(id).Get(reg); err != nil {
return nil, err
} else if !found {
return nil, ErrU2FRegistrationNotExist{ID: id}
}
return reg, nil
}

// GetU2FRegistrationsByUID returns all U2F registrations of the given user
func GetU2FRegistrationsByUID(uid int64) (U2FRegistrationList, error) {
return getU2FRegistrationsByUID(x, uid)
}

func createRegistration(e Engine, user *User, name string, reg *u2f.Registration) (*U2FRegistration, error) {
raw, err := reg.MarshalBinary()
if err != nil {
return nil, err
}
r := &U2FRegistration{
UserID: user.ID,
Name: name,
Counter: 0,
Raw: raw,
}
_, err = e.InsertOne(r)
if err != nil {
return nil, err
}
return r, nil
}

// CreateRegistration will create a new U2FRegistration from the given Registration
func CreateRegistration(user *User, name string, reg *u2f.Registration) (*U2FRegistration, error) {
return createRegistration(x, user, name, reg)
}

// DeleteRegistration will delete U2FRegistration
func DeleteRegistration(reg *U2FRegistration) error {
return deleteRegistration(x, reg)
}

func deleteRegistration(e Engine, reg *U2FRegistration) error {
_, err := e.Delete(reg)
return err
}
61 changes: 61 additions & 0 deletions models/u2f_test.go
@@ -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})
}
20 changes: 20 additions & 0 deletions modules/auth/user_form.go
Expand Up @@ -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)
}
8 changes: 8 additions & 0 deletions modules/setting/setting.go
Expand Up @@ -521,6 +521,11 @@ var (
MaxResponseItems: 50,
}

U2F = struct {
AppID string
TrustedFacets []string
}{}

// I18n settings
Langs []string
Names []string
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions options/locale/locale_en-US.ini
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down