diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2022-01-14 23:03:31 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-14 16:03:31 +0100 |
commit | 35c3553870e35b2e7cfcc599645791acda6afcef (patch) | |
tree | 0ad600c2d1cd94ef12566482832768c9efcf8a69 /models | |
parent | 8808293247bebd20482c3c625c64937174503781 (diff) | |
download | gitea-35c3553870e35b2e7cfcc599645791acda6afcef.tar.gz gitea-35c3553870e35b2e7cfcc599645791acda6afcef.zip |
Support webauthn (#17957)
Migrate from U2F to Webauthn
Co-authored-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Diffstat (limited to 'models')
-rw-r--r-- | models/auth/main_test.go | 2 | ||||
-rw-r--r-- | models/auth/u2f.go | 154 | ||||
-rw-r--r-- | models/auth/u2f_test.go | 100 | ||||
-rw-r--r-- | models/auth/webauthn.go | 222 | ||||
-rw-r--r-- | models/auth/webauthn_test.go | 69 | ||||
-rw-r--r-- | models/fixtures/u2f_registration.yml | 7 | ||||
-rw-r--r-- | models/fixtures/webauthn_credential.yml | 8 | ||||
-rw-r--r-- | models/migrations/migrations.go | 2 | ||||
-rw-r--r-- | models/migrations/v207.go | 91 |
9 files changed, 393 insertions, 262 deletions
diff --git a/models/auth/main_test.go b/models/auth/main_test.go index 94a1f405d9..4255f488fe 100644 --- a/models/auth/main_test.go +++ b/models/auth/main_test.go @@ -17,6 +17,6 @@ func TestMain(m *testing.M) { "oauth2_application.yml", "oauth2_authorization_code.yml", "oauth2_grant.yml", - "u2f_registration.yml", + "webauthn_credential.yml", ) } diff --git a/models/auth/u2f.go b/models/auth/u2f.go deleted file mode 100644 index 71943b237c..0000000000 --- a/models/auth/u2f.go +++ /dev/null @@ -1,154 +0,0 @@ -// 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 auth - -import ( - "fmt" - - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/timeutil" - - "github.com/tstranex/u2f" -) - -// ____ ________________________________ .__ __ __ .__ -// | | \_____ \_ _____/\______ \ ____ ____ |__| _______/ |_____________ _/ |_|__| ____ ____ -// | | // ____/| __) | _// __ \ / ___\| |/ ___/\ __\_ __ \__ \\ __\ |/ _ \ / \ -// | | // \| \ | | \ ___// /_/ > |\___ \ | | | | \// __ \| | | ( <_> ) | \ -// |______/ \_______ \___ / |____|_ /\___ >___ /|__/____ > |__| |__| (____ /__| |__|\____/|___| / -// \/ \/ \/ \/_____/ \/ \/ \/ - -// 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 -} - -// 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 `xorm:"BIGINT"` - CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` - UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` -} - -func init() { - db.RegisterModel(new(U2FRegistration)) -} - -// 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 db.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(db.GetEngine(db.DefaultContext)) -} - -// 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, 0, len(list)) - for _, reg := range list { - r, err := reg.Parse() - if err != nil { - log.Error("parsing u2f registration: %v", err) - continue - } - regs = append(regs, *r) - } - - return regs -} - -func getU2FRegistrationsByUID(e db.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(db.GetEngine(db.DefaultContext), id) -} - -func getU2FRegistrationByID(e db.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(db.GetEngine(db.DefaultContext), uid) -} - -// HasU2FRegistrationsByUID returns whether a given user has U2F registrations -func HasU2FRegistrationsByUID(uid int64) (bool, error) { - return db.GetEngine(db.DefaultContext).Where("user_id = ?", uid).Exist(&U2FRegistration{}) -} - -func createRegistration(e db.Engine, userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) { - raw, err := reg.MarshalBinary() - if err != nil { - return nil, err - } - r := &U2FRegistration{ - UserID: userID, - 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(userID int64, name string, reg *u2f.Registration) (*U2FRegistration, error) { - return createRegistration(db.GetEngine(db.DefaultContext), userID, name, reg) -} - -// DeleteRegistration will delete U2FRegistration -func DeleteRegistration(reg *U2FRegistration) error { - return deleteRegistration(db.GetEngine(db.DefaultContext), reg) -} - -func deleteRegistration(e db.Engine, reg *U2FRegistration) error { - _, err := e.Delete(reg) - return err -} diff --git a/models/auth/u2f_test.go b/models/auth/u2f_test.go deleted file mode 100644 index 32ad17839c..0000000000 --- a/models/auth/u2f_test.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2020 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 auth - -import ( - "encoding/hex" - "testing" - - "code.gitea.io/gitea/models/unittest" - - "github.com/stretchr/testify/assert" - "github.com/tstranex/u2f" -) - -func TestGetU2FRegistrationByID(t *testing.T) { - assert.NoError(t, unittest.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, unittest.PrepareTestDatabase()) - - res, err := GetU2FRegistrationsByUID(32) - - 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, unittest.PrepareTestDatabase()) - reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration) - reg.Counter = 1 - assert.NoError(t, reg.UpdateCounter()) - unittest.AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 1}) -} - -func TestU2FRegistration_UpdateLargeCounter(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration) - reg.Counter = 0xffffffff - assert.NoError(t, reg.UpdateCounter()) - unittest.AssertExistsIf(t, true, &U2FRegistration{ID: 1, Counter: 0xffffffff}) -} - -func TestCreateRegistration(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - res, err := CreateRegistration(1, "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) - - unittest.AssertExistsIf(t, true, &U2FRegistration{Name: "U2F Created Key", UserID: 1}) -} - -func TestDeleteRegistration(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - reg := unittest.AssertExistsAndLoadBean(t, &U2FRegistration{ID: 1}).(*U2FRegistration) - - assert.NoError(t, DeleteRegistration(reg)) - unittest.AssertNotExistsBean(t, &U2FRegistration{ID: 1}) -} - -const validU2FRegistrationResponseHex = "0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871" - -func TestToRegistrations_SkipInvalidItemsWithoutCrashing(t *testing.T) { - regKeyRaw, _ := hex.DecodeString(validU2FRegistrationResponseHex) - regs := U2FRegistrationList{ - &U2FRegistration{ID: 1}, - &U2FRegistration{ID: 2, Name: "U2F Key", UserID: 2, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800}, - } - - actual := regs.ToRegistrations() - assert.Len(t, actual, 1) -} - -func TestToRegistrations(t *testing.T) { - regKeyRaw, _ := hex.DecodeString(validU2FRegistrationResponseHex) - regs := U2FRegistrationList{ - &U2FRegistration{ID: 1, Name: "U2F Key", UserID: 1, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800}, - &U2FRegistration{ID: 2, Name: "U2F Key", UserID: 2, Counter: 0, Raw: regKeyRaw, CreatedUnix: 946684800, UpdatedUnix: 946684800}, - } - - actual := regs.ToRegistrations() - assert.Len(t, actual, 2) -} diff --git a/models/auth/webauthn.go b/models/auth/webauthn.go new file mode 100644 index 0000000000..75776f1e0e --- /dev/null +++ b/models/auth/webauthn.go @@ -0,0 +1,222 @@ +// Copyright 2020 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 auth + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/timeutil" + "xorm.io/xorm" + + "github.com/duo-labs/webauthn/webauthn" +) + +// ErrWebAuthnCredentialNotExist represents a "ErrWebAuthnCRedentialNotExist" kind of error. +type ErrWebAuthnCredentialNotExist struct { + ID int64 + CredentialID string +} + +func (err ErrWebAuthnCredentialNotExist) Error() string { + if err.CredentialID == "" { + return fmt.Sprintf("WebAuthn credential does not exist [id: %d]", err.ID) + } + return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %s]", err.CredentialID) +} + +//IsErrWebAuthnCredentialNotExist checks if an error is a ErrWebAuthnCredentialNotExist. +func IsErrWebAuthnCredentialNotExist(err error) bool { + _, ok := err.(ErrWebAuthnCredentialNotExist) + return ok +} + +//WebAuthnCredential represents the WebAuthn credential data for a public-key +//credential conformant to WebAuthn Level 1 +type WebAuthnCredential struct { + ID int64 `xorm:"pk autoincr"` + Name string + LowerName string `xorm:"unique(s)"` + UserID int64 `xorm:"INDEX unique(s)"` + CredentialID string `xorm:"INDEX"` + PublicKey []byte + AttestationType string + AAGUID []byte + SignCount uint32 `xorm:"BIGINT"` + CloneWarning bool + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` +} + +func init() { + db.RegisterModel(new(WebAuthnCredential)) +} + +// TableName returns a better table name for WebAuthnCredential +func (cred WebAuthnCredential) TableName() string { + return "webauthn_credential" +} + +// UpdateSignCount will update the database value of SignCount +func (cred *WebAuthnCredential) UpdateSignCount() error { + return cred.updateSignCount(db.DefaultContext) +} + +func (cred *WebAuthnCredential) updateSignCount(ctx context.Context) error { + _, err := db.GetEngine(ctx).ID(cred.ID).Cols("sign_count").Update(cred) + return err +} + +// BeforeInsert will be invoked by XORM before updating a record +func (cred *WebAuthnCredential) BeforeInsert() { + cred.LowerName = strings.ToLower(cred.Name) +} + +// BeforeUpdate will be invoked by XORM before updating a record +func (cred *WebAuthnCredential) BeforeUpdate() { + cred.LowerName = strings.ToLower(cred.Name) +} + +// AfterLoad is invoked from XORM after setting the values of all fields of this object. +func (cred *WebAuthnCredential) AfterLoad(session *xorm.Session) { + cred.LowerName = strings.ToLower(cred.Name) +} + +// WebAuthnCredentialList is a list of *WebAuthnCredential +type WebAuthnCredentialList []*WebAuthnCredential + +// ToCredentials will convert all WebAuthnCredentials to webauthn.Credentials +func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential { + creds := make([]webauthn.Credential, 0, len(list)) + for _, cred := range list { + credID, _ := base64.RawStdEncoding.DecodeString(cred.CredentialID) + creds = append(creds, webauthn.Credential{ + ID: credID, + PublicKey: cred.PublicKey, + AttestationType: cred.AttestationType, + Authenticator: webauthn.Authenticator{ + AAGUID: cred.AAGUID, + SignCount: cred.SignCount, + CloneWarning: cred.CloneWarning, + }, + }) + } + return creds +} + +//GetWebAuthnCredentialsByUID returns all WebAuthn credentials of the given user +func GetWebAuthnCredentialsByUID(uid int64) (WebAuthnCredentialList, error) { + return getWebAuthnCredentialsByUID(db.DefaultContext, uid) +} + +func getWebAuthnCredentialsByUID(ctx context.Context, uid int64) (WebAuthnCredentialList, error) { + creds := make(WebAuthnCredentialList, 0) + return creds, db.GetEngine(ctx).Where("user_id = ?", uid).Find(&creds) +} + +//ExistsWebAuthnCredentialsForUID returns if the given user has credentials +func ExistsWebAuthnCredentialsForUID(uid int64) (bool, error) { + return existsWebAuthnCredentialsByUID(db.DefaultContext, uid) +} + +func existsWebAuthnCredentialsByUID(ctx context.Context, uid int64) (bool, error) { + return db.GetEngine(ctx).Where("user_id = ?", uid).Exist(&WebAuthnCredential{}) +} + +// GetWebAuthnCredentialByName returns WebAuthn credential by id +func GetWebAuthnCredentialByName(uid int64, name string) (*WebAuthnCredential, error) { + return getWebAuthnCredentialByName(db.DefaultContext, uid, name) +} + +func getWebAuthnCredentialByName(ctx context.Context, uid int64, name string) (*WebAuthnCredential, error) { + cred := new(WebAuthnCredential) + if found, err := db.GetEngine(ctx).Where("user_id = ? AND lower_name = ?", uid, strings.ToLower(name)).Get(cred); err != nil { + return nil, err + } else if !found { + return nil, ErrWebAuthnCredentialNotExist{} + } + return cred, nil +} + +// GetWebAuthnCredentialByID returns WebAuthn credential by id +func GetWebAuthnCredentialByID(id int64) (*WebAuthnCredential, error) { + return getWebAuthnCredentialByID(db.DefaultContext, id) +} + +func getWebAuthnCredentialByID(ctx context.Context, id int64) (*WebAuthnCredential, error) { + cred := new(WebAuthnCredential) + if found, err := db.GetEngine(ctx).ID(id).Get(cred); err != nil { + return nil, err + } else if !found { + return nil, ErrWebAuthnCredentialNotExist{ID: id} + } + return cred, nil +} + +// HasWebAuthnRegistrationsByUID returns whether a given user has WebAuthn registrations +func HasWebAuthnRegistrationsByUID(uid int64) (bool, error) { + return db.GetEngine(db.DefaultContext).Where("user_id = ?", uid).Exist(&WebAuthnCredential{}) +} + +// GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID +func GetWebAuthnCredentialByCredID(credID string) (*WebAuthnCredential, error) { + return getWebAuthnCredentialByCredID(db.DefaultContext, credID) +} + +func getWebAuthnCredentialByCredID(ctx context.Context, credID string) (*WebAuthnCredential, error) { + cred := new(WebAuthnCredential) + if found, err := db.GetEngine(ctx).Where("credential_id = ?", credID).Get(cred); err != nil { + return nil, err + } else if !found { + return nil, ErrWebAuthnCredentialNotExist{CredentialID: credID} + } + return cred, nil +} + +// CreateCredential will create a new WebAuthnCredential from the given Credential +func CreateCredential(userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) { + return createCredential(db.DefaultContext, userID, name, cred) +} + +func createCredential(ctx context.Context, userID int64, name string, cred *webauthn.Credential) (*WebAuthnCredential, error) { + c := &WebAuthnCredential{ + UserID: userID, + Name: name, + CredentialID: base64.RawStdEncoding.EncodeToString(cred.ID), + PublicKey: cred.PublicKey, + AttestationType: cred.AttestationType, + AAGUID: cred.Authenticator.AAGUID, + SignCount: cred.Authenticator.SignCount, + CloneWarning: false, + } + + if err := db.Insert(ctx, c); err != nil { + return nil, err + } + return c, nil +} + +// DeleteCredential will delete WebAuthnCredential +func DeleteCredential(id, userID int64) (bool, error) { + return deleteCredential(db.DefaultContext, id, userID) +} + +func deleteCredential(ctx context.Context, id, userID int64) (bool, error) { + had, err := db.GetEngine(ctx).ID(id).Where("user_id = ?", userID).Delete(&WebAuthnCredential{}) + return had > 0, err +} + +//WebAuthnCredentials implementns the webauthn.User interface +func WebAuthnCredentials(userID int64) ([]webauthn.Credential, error) { + dbCreds, err := GetWebAuthnCredentialsByUID(userID) + if err != nil { + return nil, err + } + + return dbCreds.ToCredentials(), nil +} diff --git a/models/auth/webauthn_test.go b/models/auth/webauthn_test.go new file mode 100644 index 0000000000..572636dbbf --- /dev/null +++ b/models/auth/webauthn_test.go @@ -0,0 +1,69 @@ +// Copyright 2020 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 auth + +import ( + "encoding/base64" + "testing" + + "code.gitea.io/gitea/models/unittest" + + "github.com/duo-labs/webauthn/webauthn" + "github.com/stretchr/testify/assert" +) + +func TestGetWebAuthnCredentialByID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + res, err := GetWebAuthnCredentialByID(1) + assert.NoError(t, err) + assert.Equal(t, "WebAuthn credential", res.Name) + + _, err = GetWebAuthnCredentialByID(342432) + assert.Error(t, err) + assert.True(t, IsErrWebAuthnCredentialNotExist(err)) +} + +func TestGetWebAuthnCredentialsByUID(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + res, err := GetWebAuthnCredentialsByUID(32) + assert.NoError(t, err) + assert.Len(t, res, 1) + assert.Equal(t, "WebAuthn credential", res[0].Name) +} + +func TestWebAuthnCredential_TableName(t *testing.T) { + assert.Equal(t, "webauthn_credential", WebAuthnCredential{}.TableName()) +} + +func TestWebAuthnCredential_UpdateSignCount(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + cred := unittest.AssertExistsAndLoadBean(t, &WebAuthnCredential{ID: 1}).(*WebAuthnCredential) + cred.SignCount = 1 + assert.NoError(t, cred.UpdateSignCount()) + unittest.AssertExistsIf(t, true, &WebAuthnCredential{ID: 1, SignCount: 1}) +} + +func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + cred := unittest.AssertExistsAndLoadBean(t, &WebAuthnCredential{ID: 1}).(*WebAuthnCredential) + cred.SignCount = 0xffffffff + assert.NoError(t, cred.UpdateSignCount()) + unittest.AssertExistsIf(t, true, &WebAuthnCredential{ID: 1, SignCount: 0xffffffff}) +} + +func TestCreateCredential(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + res, err := CreateCredential(1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")}) + assert.NoError(t, err) + assert.Equal(t, "WebAuthn Created Credential", res.Name) + bs, err := base64.RawStdEncoding.DecodeString(res.CredentialID) + assert.NoError(t, err) + assert.Equal(t, []byte("Test"), bs) + + unittest.AssertExistsIf(t, true, &WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1}) +} diff --git a/models/fixtures/u2f_registration.yml b/models/fixtures/u2f_registration.yml deleted file mode 100644 index 60555c43f1..0000000000 --- a/models/fixtures/u2f_registration.yml +++ /dev/null @@ -1,7 +0,0 @@ -- - id: 1 - name: "U2F Key" - user_id: 32 - counter: 0 - created_unix: 946684800 - updated_unix: 946684800 diff --git a/models/fixtures/webauthn_credential.yml b/models/fixtures/webauthn_credential.yml new file mode 100644 index 0000000000..b4109a03f2 --- /dev/null +++ b/models/fixtures/webauthn_credential.yml @@ -0,0 +1,8 @@ +- id: 1 + name: "WebAuthn credential" + user_id: 32 + attestation_type: none + sign_count: 0 + clone_warning: false + created_unix: 946684800 + updated_unix: 946684800 diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 9423e5c5f6..4ee2bc839f 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -366,6 +366,8 @@ var migrations = []Migration{ NewMigration("Migrate to higher varchar on user struct", migrateUserPasswordSalt), // v206 -> v207 NewMigration("Add authorize column to team_unit table", addAuthorizeColForTeamUnit), + // v207 -> v208 + NewMigration("Add webauthn table and migrate u2f data to webauthn", addWebAuthnCred), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v207.go b/models/migrations/v207.go new file mode 100644 index 0000000000..82e2e3aa31 --- /dev/null +++ b/models/migrations/v207.go @@ -0,0 +1,91 @@ +// Copyright 2021 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 migrations + +import ( + "crypto/elliptic" + "encoding/base64" + "strings" + + "code.gitea.io/gitea/modules/timeutil" + + "github.com/tstranex/u2f" + "xorm.io/xorm" +) + +func addWebAuthnCred(x *xorm.Engine) error { + + // Create webauthnCredential table + type webauthnCredential struct { + ID int64 `xorm:"pk autoincr"` + Name string + LowerName string `xorm:"unique(s)"` + UserID int64 `xorm:"INDEX unique(s)"` + CredentialID string `xorm:"INDEX"` + PublicKey []byte + AttestationType string + AAGUID []byte + SignCount uint32 `xorm:"BIGINT"` + CloneWarning bool + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + if err := x.Sync2(&webauthnCredential{}); err != nil { + return err + } + + // Now migrate the old u2f registrations to the new format + type u2fRegistration struct { + ID int64 `xorm:"pk autoincr"` + Name string + UserID int64 `xorm:"INDEX"` + Raw []byte + Counter uint32 `xorm:"BIGINT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + + var start int + regs := make([]*u2fRegistration, 0, 50) + for { + err := x.OrderBy("id").Limit(50, start).Find(®s) + if err != nil { + return err + } + + for _, reg := range regs { + parsed := new(u2f.Registration) + err = parsed.UnmarshalBinary(reg.Raw) + if err != nil { + continue + } + + c := &webauthnCredential{ + ID: reg.ID, + Name: reg.Name, + LowerName: strings.ToLower(reg.Name), + UserID: reg.UserID, + CredentialID: base64.RawStdEncoding.EncodeToString(parsed.KeyHandle), + PublicKey: elliptic.Marshal(elliptic.P256(), parsed.PubKey.X, parsed.PubKey.Y), + AttestationType: "fido-u2f", + AAGUID: []byte{}, + SignCount: reg.Counter, + } + + _, err := x.Insert(c) + if err != nil { + return err + } + } + + if len(regs) < 50 { + break + } + start += 50 + regs = regs[:0] + } + + return nil +} |