Skip to content

Commit e819da0

Browse files
authored
WebAuthn CredentialID field needs to be increased in size (#20530)
WebAuthn have updated their specification to set the maximum size of the CredentialID to 1023 bytes. This is somewhat larger than our current size and therefore we need to migrate. The PR changes the struct to add CredentialIDBytes and migrates the CredentialID string to the bytes field before another migration drops the old CredentialID field. Another migration renames this field back. Fix #20457 Signed-off-by: Andrew Thornton <[email protected]>
1 parent 692707f commit e819da0

File tree

10 files changed

+363
-16
lines changed

10 files changed

+363
-16
lines changed

models/auth/webauthn.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package auth
66

77
import (
88
"context"
9-
"encoding/base32"
109
"fmt"
1110
"strings"
1211

@@ -20,14 +19,14 @@ import (
2019
// ErrWebAuthnCredentialNotExist represents a "ErrWebAuthnCRedentialNotExist" kind of error.
2120
type ErrWebAuthnCredentialNotExist struct {
2221
ID int64
23-
CredentialID string
22+
CredentialID []byte
2423
}
2524

2625
func (err ErrWebAuthnCredentialNotExist) Error() string {
27-
if err.CredentialID == "" {
26+
if len(err.CredentialID) == 0 {
2827
return fmt.Sprintf("WebAuthn credential does not exist [id: %d]", err.ID)
2928
}
30-
return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %s]", err.CredentialID)
29+
return fmt.Sprintf("WebAuthn credential does not exist [credential_id: %x]", err.CredentialID)
3130
}
3231

3332
// IsErrWebAuthnCredentialNotExist checks if an error is a ErrWebAuthnCredentialNotExist.
@@ -43,7 +42,7 @@ type WebAuthnCredential struct {
4342
Name string
4443
LowerName string `xorm:"unique(s)"`
4544
UserID int64 `xorm:"INDEX unique(s)"`
46-
CredentialID string `xorm:"INDEX VARCHAR(410)"`
45+
CredentialID []byte `xorm:"INDEX VARBINARY(1024)"`
4746
PublicKey []byte
4847
AttestationType string
4948
AAGUID []byte
@@ -94,9 +93,8 @@ type WebAuthnCredentialList []*WebAuthnCredential
9493
func (list WebAuthnCredentialList) ToCredentials() []webauthn.Credential {
9594
creds := make([]webauthn.Credential, 0, len(list))
9695
for _, cred := range list {
97-
credID, _ := base32.HexEncoding.DecodeString(cred.CredentialID)
9896
creds = append(creds, webauthn.Credential{
99-
ID: credID,
97+
ID: cred.CredentialID,
10098
PublicKey: cred.PublicKey,
10199
AttestationType: cred.AttestationType,
102100
Authenticator: webauthn.Authenticator{
@@ -164,11 +162,11 @@ func HasWebAuthnRegistrationsByUID(uid int64) (bool, error) {
164162
}
165163

166164
// GetWebAuthnCredentialByCredID returns WebAuthn credential by credential ID
167-
func GetWebAuthnCredentialByCredID(userID int64, credID string) (*WebAuthnCredential, error) {
165+
func GetWebAuthnCredentialByCredID(userID int64, credID []byte) (*WebAuthnCredential, error) {
168166
return getWebAuthnCredentialByCredID(db.DefaultContext, userID, credID)
169167
}
170168

171-
func getWebAuthnCredentialByCredID(ctx context.Context, userID int64, credID string) (*WebAuthnCredential, error) {
169+
func getWebAuthnCredentialByCredID(ctx context.Context, userID int64, credID []byte) (*WebAuthnCredential, error) {
172170
cred := new(WebAuthnCredential)
173171
if found, err := db.GetEngine(ctx).Where("user_id = ? AND credential_id = ?", userID, credID).Get(cred); err != nil {
174172
return nil, err
@@ -187,7 +185,7 @@ func createCredential(ctx context.Context, userID int64, name string, cred *weba
187185
c := &WebAuthnCredential{
188186
UserID: userID,
189187
Name: name,
190-
CredentialID: base32.HexEncoding.EncodeToString(cred.ID),
188+
CredentialID: cred.ID,
191189
PublicKey: cred.PublicKey,
192190
AttestationType: cred.AttestationType,
193191
AAGUID: cred.Authenticator.AAGUID,

models/auth/webauthn_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package auth
66

77
import (
8-
"encoding/base32"
98
"testing"
109

1110
"code.gitea.io/gitea/models/unittest"
@@ -61,9 +60,7 @@ func TestCreateCredential(t *testing.T) {
6160
res, err := CreateCredential(1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")})
6261
assert.NoError(t, err)
6362
assert.Equal(t, "WebAuthn Created Credential", res.Name)
64-
bs, err := base32.HexEncoding.DecodeString(res.CredentialID)
65-
assert.NoError(t, err)
66-
assert.Equal(t, []byte("Test"), bs)
63+
assert.Equal(t, []byte("Test"), res.CredentialID)
6764

6865
unittest.AssertExistsIf(t, true, &WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
6966
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-
2+
id: 1
3+
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
4+
-
5+
id: 2
6+
credential_id: "051CLMMKB62S6M9M2A4H54K7MMCQALFJ36G4TGB2S9A47APLTILU6C6744CEBG4EKCGV357N21BSLH8JD33GQMFAR6DQ70S76P34J6FR="
7+
-
8+
id: 4
9+
credential_id: "APU4B1NDTEVTEM60V4T0FRL7SRJMO9KIE2AKFQ8JDGTQ7VHFI41FDEFTDLBVQEAE4ER49QV2GTGVFDNBO31BPOA3OQN6879OT6MTU3G="
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-
2+
id: 1
3+
lower_name: "u2fkey-correctly-migrated"
4+
name: "u2fkey-correctly-migrated"
5+
user_id: 1
6+
credential_id: "TVHE44TOH7DF7V48SEAIT3EMMJ7TGBOQ289E5AQB34S98LFCUFJ7U2NAVI8RJG6K2F4TC8AQ8KBNO7AGEOQOL9NE43GR63HTEHJSLOG="
7+
public_key: 0x040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf2
8+
attestation_type: 'fido-u2f'
9+
sign_count: 1
10+
clone_warning: false
11+
-
12+
id: 2
13+
lower_name: "non-u2f-key"
14+
name: "non-u2f-key"
15+
user_id: 1
16+
credential_id: "051CLMMKB62S6M9M2A4H54K7MMCQALFJ36G4TGB2S9A47APLTILU6C6744CEBG4EKCGV357N21BSLH8JD33GQMFAR6DQ70S76P34J6FR"
17+
public_key: 0x040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf2
18+
attestation_type: 'none'
19+
sign_count: 1
20+
clone_warning: false
21+
-
22+
id: 4
23+
lower_name: "packed-key"
24+
name: "packed-key"
25+
user_id: 1
26+
credential_id: "APU4B1NDTEVTEM60V4T0FRL7SRJMO9KIE2AKFQ8JDGTQ7VHFI41FDEFTDLBVQEAE4ER49QV2GTGVFDNBO31BPOA3OQN6879OT6MTU3G="
27+
public_key: 0x040d0967a2cad045011631187576492a0beb5b377954b4f694c5afc8bdf25270f87f09a9ab6ce9c282f447ba71b2f2bae2105b32b847e0704f310f48644e3eddf2
28+
attestation_type: 'fido-u2f'
29+
sign_count: 1
30+
clone_warning: false
31+

models/migrations/migrations.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,12 @@ var migrations = []Migration{
400400
NewMigration("Add sync_on_commit column to push_mirror table", addSyncOnCommitColForPushMirror),
401401
// v220 -> v221
402402
NewMigration("Add container repository property", addContainerRepositoryProperty),
403+
// v221 -> v222
404+
NewMigration("Store WebAuthentication CredentialID as bytes and increase size to at least 1024", storeWebauthnCredentialIDAsBytes),
405+
// v222 -> v223
406+
NewMigration("Drop old CredentialID column", dropOldCredentialIDColumn),
407+
// v223 -> v224
408+
NewMigration("Rename CredentialIDBytes column to CredentialID", renameCredentialIDBytes),
403409
}
404410

405411
// GetCurrentDBVersion returns the current db version

models/migrations/v221.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"encoding/base32"
9+
"fmt"
10+
11+
"code.gitea.io/gitea/modules/timeutil"
12+
13+
"xorm.io/xorm"
14+
)
15+
16+
func storeWebauthnCredentialIDAsBytes(x *xorm.Engine) error {
17+
// Create webauthnCredential table
18+
type webauthnCredential struct {
19+
ID int64 `xorm:"pk autoincr"`
20+
Name string
21+
LowerName string `xorm:"unique(s)"`
22+
UserID int64 `xorm:"INDEX unique(s)"`
23+
CredentialID string `xorm:"INDEX VARCHAR(410)"`
24+
// Note the lack of INDEX here - these will be created once the column is renamed in v223.go
25+
CredentialIDBytes []byte `xorm:"VARBINARY(1024)"` // CredentialID is at most 1023 bytes as per spec released 20 July 2022
26+
PublicKey []byte
27+
AttestationType string
28+
AAGUID []byte
29+
SignCount uint32 `xorm:"BIGINT"`
30+
CloneWarning bool
31+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
32+
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
33+
}
34+
if err := x.Sync2(&webauthnCredential{}); err != nil {
35+
return err
36+
}
37+
38+
var start int
39+
creds := make([]*webauthnCredential, 0, 50)
40+
for {
41+
err := x.Select("id, credential_id").OrderBy("id").Limit(50, start).Find(&creds)
42+
if err != nil {
43+
return err
44+
}
45+
46+
err = func() error {
47+
sess := x.NewSession()
48+
defer sess.Close()
49+
if err := sess.Begin(); err != nil {
50+
return fmt.Errorf("unable to allow start session. Error: %w", err)
51+
}
52+
for _, cred := range creds {
53+
cred.CredentialIDBytes, err = base32.HexEncoding.DecodeString(cred.CredentialID)
54+
if err != nil {
55+
return fmt.Errorf("unable to parse credential id %s for credential[%d]: %w", cred.CredentialID, cred.ID, err)
56+
}
57+
count, err := sess.ID(cred.ID).Cols("credential_id_bytes").Update(cred)
58+
if count != 1 || err != nil {
59+
return fmt.Errorf("unable to update credential id bytes for credential[%d]: %d,%w", cred.ID, count, err)
60+
}
61+
}
62+
return sess.Commit()
63+
}()
64+
if err != nil {
65+
return err
66+
}
67+
68+
if len(creds) < 50 {
69+
break
70+
}
71+
start += 50
72+
creds = creds[:0]
73+
}
74+
return nil
75+
}

models/migrations/v221_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"encoding/base32"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func Test_storeWebauthnCredentialIDAsBytes(t *testing.T) {
15+
// Create webauthnCredential table
16+
type WebauthnCredential struct {
17+
ID int64 `xorm:"pk autoincr"`
18+
Name string
19+
LowerName string `xorm:"unique(s)"`
20+
UserID int64 `xorm:"INDEX unique(s)"`
21+
CredentialID string `xorm:"INDEX VARCHAR(410)"`
22+
PublicKey []byte
23+
AttestationType string
24+
AAGUID []byte
25+
SignCount uint32 `xorm:"BIGINT"`
26+
CloneWarning bool
27+
}
28+
29+
type ExpectedWebauthnCredential struct {
30+
ID int64 `xorm:"pk autoincr"`
31+
CredentialID string // CredentialID is at most 1023 bytes as per spec released 20 July 2022
32+
}
33+
34+
type ConvertedWebauthnCredential struct {
35+
ID int64 `xorm:"pk autoincr"`
36+
CredentialIDBytes []byte `xorm:"VARBINARY(1024)"` // CredentialID is at most 1023 bytes as per spec released 20 July 2022
37+
}
38+
39+
// Prepare and load the testing database
40+
x, deferable := prepareTestEnv(t, 0, new(WebauthnCredential), new(ExpectedWebauthnCredential))
41+
defer deferable()
42+
if x == nil || t.Failed() {
43+
return
44+
}
45+
46+
if err := storeWebauthnCredentialIDAsBytes(x); err != nil {
47+
assert.NoError(t, err)
48+
return
49+
}
50+
51+
expected := []ExpectedWebauthnCredential{}
52+
if err := x.Table("expected_webauthn_credential").Asc("id").Find(&expected); !assert.NoError(t, err) {
53+
return
54+
}
55+
56+
got := []ConvertedWebauthnCredential{}
57+
if err := x.Table("webauthn_credential").Select("id, credential_id_bytes").Asc("id").Find(&got); !assert.NoError(t, err) {
58+
return
59+
}
60+
61+
for i, e := range expected {
62+
credIDBytes, _ := base32.HexEncoding.DecodeString(e.CredentialID)
63+
assert.Equal(t, credIDBytes, got[i].CredentialIDBytes)
64+
}
65+
}

models/migrations/v222.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"context"
9+
"fmt"
10+
11+
"code.gitea.io/gitea/modules/timeutil"
12+
13+
"xorm.io/xorm"
14+
)
15+
16+
func dropOldCredentialIDColumn(x *xorm.Engine) error {
17+
// This migration maybe rerun so that we should check if it has been run
18+
credentialIDExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webauthn_credential", "credential_id")
19+
if err != nil {
20+
return err
21+
}
22+
if !credentialIDExist {
23+
// Column is already non-extant
24+
return nil
25+
}
26+
credentialIDBytesExists, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webauthn_credential", "credential_id_bytes")
27+
if err != nil {
28+
return err
29+
}
30+
if !credentialIDBytesExists {
31+
// looks like 221 hasn't properly run
32+
return fmt.Errorf("webauthn_credential does not have a credential_id_bytes column... it is not safe to run this migration")
33+
}
34+
35+
// Create webauthnCredential table
36+
type webauthnCredential struct {
37+
ID int64 `xorm:"pk autoincr"`
38+
Name string
39+
LowerName string `xorm:"unique(s)"`
40+
UserID int64 `xorm:"INDEX unique(s)"`
41+
CredentialID string `xorm:"INDEX VARCHAR(410)"`
42+
// Note the lack of the INDEX on CredentialIDBytes - we will add this in v223.go
43+
CredentialIDBytes []byte `xorm:"VARBINARY(1024)"` // CredentialID is at most 1023 bytes as per spec released 20 July 2022
44+
PublicKey []byte
45+
AttestationType string
46+
AAGUID []byte
47+
SignCount uint32 `xorm:"BIGINT"`
48+
CloneWarning bool
49+
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
50+
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
51+
}
52+
if err := x.Sync2(&webauthnCredential{}); err != nil {
53+
return err
54+
}
55+
56+
// Drop the old credential ID
57+
sess := x.NewSession()
58+
defer sess.Close()
59+
60+
if err := dropTableColumns(sess, "webauthn_credential", "credential_id"); err != nil {
61+
return fmt.Errorf("unable to drop old credentialID column: %w", err)
62+
}
63+
return sess.Commit()
64+
}

0 commit comments

Comments
 (0)