[FEAT] Add support for webauthn credential level 3

- For WebAuthn Credential level 3, the `backup_eligible` and
`backup_state` flags are checked if they are consistent with the values
given on login. Forgejo never stored this data, so add a database
migration that makes all webauthn credentials 'legacy' and on the next
first use capture the values of `backup_eligible` and `backup_state`.
As suggested in https://github.com/go-webauthn/webauthn/discussions/219#discussioncomment-10429662
- Adds unit tests.
- Add E2E test.
This commit is contained in:
Gusted 2024-08-28 07:40:40 +02:00
parent 28c3f1e254
commit 63736e8301
No known key found for this signature in database
GPG key ID: FD821B732837125F
7 changed files with 131 additions and 12 deletions

View file

@ -40,7 +40,7 @@ func IsErrWebAuthnCredentialNotExist(err error) bool {
}
// WebAuthnCredential represents the WebAuthn credential data for a public-key
// credential conformant to WebAuthn Level 1
// credential conformant to WebAuthn Level 3
type WebAuthnCredential struct {
ID int64 `xorm:"pk autoincr"`
Name string
@ -52,8 +52,12 @@ type WebAuthnCredential struct {
AAGUID []byte
SignCount uint32 `xorm:"BIGINT"`
CloneWarning bool
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
BackupEligible bool `XORM:"NOT NULL DEFAULT false"`
BackupState bool `XORM:"NOT NULL DEFAULT false"`
// If legacy is set to true, backup_eligible and backup_state isn't set.
Legacy bool `XORM:"NOT NULL DEFAULT true"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
func init() {
@ -71,6 +75,12 @@ func (cred *WebAuthnCredential) UpdateSignCount(ctx context.Context) error {
return err
}
// UpdateFromLegacy update the values that aren't present on legacy credentials.
func (cred *WebAuthnCredential) UpdateFromLegacy(ctx context.Context) error {
_, err := db.GetEngine(ctx).ID(cred.ID).Cols("legacy", "backup_eligible", "backup_state").Update(cred)
return err
}
// BeforeInsert will be invoked by XORM before updating a record
func (cred *WebAuthnCredential) BeforeInsert() {
cred.LowerName = strings.ToLower(cred.Name)
@ -97,6 +107,10 @@ func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
ID: cred.CredentialID,
PublicKey: cred.PublicKey,
AttestationType: cred.AttestationType,
Flags: webauthn.CredentialFlags{
BackupEligible: cred.BackupEligible,
BackupState: cred.BackupState,
},
Authenticator: webauthn.Authenticator{
AAGUID: cred.AAGUID,
SignCount: cred.SignCount,
@ -167,6 +181,9 @@ func CreateCredential(ctx context.Context, userID int64, name string, cred *weba
AAGUID: cred.Authenticator.AAGUID,
SignCount: cred.Authenticator.SignCount,
CloneWarning: false,
BackupEligible: cred.Flags.BackupEligible,
BackupState: cred.Flags.BackupState,
Legacy: false,
}
if err := db.Insert(ctx, c); err != nil {

View file

@ -56,13 +56,23 @@ func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) {
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
}
func TestWebAuthenCredential_UpdateFromLegacy(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, Legacy: true})
cred.Legacy = false
cred.BackupEligible = true
cred.BackupState = true
require.NoError(t, cred.UpdateFromLegacy(db.DefaultContext))
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, BackupEligible: true, BackupState: true}, "legacy = false")
}
func TestCreateCredential(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
res, err := auth_model.CreateCredential(db.DefaultContext, 1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")})
res, err := auth_model.CreateCredential(db.DefaultContext, 1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test"), Flags: webauthn.CredentialFlags{BackupEligible: true, BackupState: true}})
require.NoError(t, err)
assert.Equal(t, "WebAuthn Created Credential", res.Name)
assert.Equal(t, []byte("Test"), res.CredentialID)
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1, BackupEligible: true, BackupState: true}, "legacy = false")
}

View file

@ -5,5 +5,6 @@
attestation_type: none
sign_count: 0
clone_warning: false
legacy: true
created_unix: 946684800
updated_unix: 946684800

View file

@ -80,6 +80,8 @@ var migrations = []*Migration{
NewMigration("Creating Quota-related tables", CreateQuotaTables),
// v21 -> v22
NewMigration("Add SSH keypair to `pull_mirror` table", AddSSHKeypairToPushMirror),
// v22 -> v23
NewMigration("Add `legacy` to `web_authn_credential` table", AddLegacyToWebAuthnCredential),
}
// GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -0,0 +1,17 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgejo_migrations //nolint:revive
import "xorm.io/xorm"
func AddLegacyToWebAuthnCredential(x *xorm.Engine) error {
type WebauthnCredential struct {
ID int64 `xorm:"pk autoincr"`
BackupEligible bool `xorm:"NOT NULL DEFAULT false"`
BackupState bool `xorm:"NOT NULL DEFAULT false"`
Legacy bool `xorm:"NOT NULL DEFAULT true"`
}
return x.Sync(&WebauthnCredential{})
}