Skip to content

Fix user's sign email check #35045

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

Merged
merged 2 commits into from
Jul 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions models/asymkey/ssh_key_fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (
"xorm.io/builder"
)

// The database is used in checkKeyFingerprint however most of these functions probably belong in a module
// The database is used in checkKeyFingerprint. However, most of these functions probably belong in a module

// checkKeyFingerprint only checks if key fingerprint has been used as public key,
// checkKeyFingerprint only checks if key fingerprint has been used as a public key,
// it is OK to use same key as deploy key for multiple repositories/users.
func checkKeyFingerprint(ctx context.Context, fingerprint string) error {
has, err := db.Exist[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
Expand Down
1 change: 1 addition & 0 deletions models/fixtures/public_key.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
created_unix: 1559593109
updated_unix: 1565224552
login_source_id: 0
verified: false
33 changes: 6 additions & 27 deletions services/asymkey/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *asymkey_model
}

// ParseCommitWithSignatureCommitter parses a commit's GPG or SSH signature.
// The caller guarantees that the committer user is related to the commit by checking its activated email addresses or no-reply address.
// If the commit is singed by an instance key, then committer can be nil.
// If the signature exists, even if committer is nil, the returned CommittingUser will be a non-nil fake user.
// If the signature exists, even if committer is nil, the returned CommittingUser will be a non-nil fake user (e.g.: instance key)
func ParseCommitWithSignatureCommitter(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification {
// If no signature, just report the committer
if c.Signature == nil {
Expand Down Expand Up @@ -114,20 +115,11 @@ func parseCommitWithGPGSignature(ctx context.Context, c *git.Commit, committer *
}
}

committerEmailAddresses, _ := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committer.ID, user_model.GetEmailAddresses)
activated := false
for _, e := range committerEmailAddresses {
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
activated = true
break
}
}

for _, k := range keys {
// Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate
canValidate := false
email := ""
if k.Verified && activated {
if k.Verified {
canValidate = true
email = c.Committer.Email
}
Expand Down Expand Up @@ -217,8 +209,8 @@ func checkKeyEmails(ctx context.Context, email string, keys ...*asymkey_model.GP
return true, e.Email
}
}
if user.KeepEmailPrivate && strings.EqualFold(email, user.GetEmail()) {
return true, user.GetEmail()
if user != nil && strings.EqualFold(email, user.GetPlaceholderEmail()) {
return true, user.GetPlaceholderEmail()
}
}
}
Expand Down Expand Up @@ -388,21 +380,8 @@ func parseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUs
}
}

committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committerUser.ID, user_model.GetEmailAddresses)
if err != nil {
log.Error("GetEmailAddresses: %v", err)
}

activated := false
for _, e := range committerEmailAddresses {
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
activated = true
break
}
}

for _, k := range keys {
if k.Verified && activated {
if k.Verified {
commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committerUser, committerUser, c.Committer.Email)
if commitVerification != nil {
return commitVerification
Expand Down
47 changes: 46 additions & 1 deletion services/asymkey/commit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"strings"
"testing"

asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
Expand All @@ -17,7 +20,49 @@ import (
)

func TestParseCommitWithSSHSignature(t *testing.T) {
// Here we only test the TrustedSSHKeys. The complete signing test is in tests/integration/gpg_ssh_git_test.go
assert.NoError(t, unittest.PrepareTestDatabase())

// Here we only need to do some tests that "tests/integration/gpg_ssh_git_test.go" doesn't cover

// -----BEGIN OPENSSH PRIVATE KEY-----
// b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
// QyNTUxOQAAACC6T6zF0oPak8dOIzzT1kXB7LrcsVo04SKc3GjuvMllZwAAAJgy08upMtPL
// qQAAAAtzc2gtZWQyNTUxOQAAACC6T6zF0oPak8dOIzzT1kXB7LrcsVo04SKc3GjuvMllZw
// AAAEDWqPHTH51xb4hy1y1f1VeWL/2A9Q0b6atOyv5fx8x5prpPrMXSg9qTx04jPNPWRcHs
// utyxWjThIpzcaO68yWVnAAAAEXVzZXIyQGV4YW1wbGUuY29tAQIDBA==
// -----END OPENSSH PRIVATE KEY-----
sshPubKey, err := asymkey_model.AddPublicKey(t.Context(), 999, "user-ssh-key-any-name", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILpPrMXSg9qTx04jPNPWRcHsutyxWjThIpzcaO68yWVn", 0)
require.NoError(t, err)
_, err = db.GetEngine(t.Context()).ID(sshPubKey.ID).Cols("verified").Update(&asymkey_model.PublicKey{Verified: true})
require.NoError(t, err)

t.Run("UserSSHKey", func(t *testing.T) {
commit, err := git.CommitFromReader(nil, git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree a3b1fad553e0f9a2b4a58327bebde36c7da75aa2
author user2 <[email protected]> 1752194028 -0700
committer user2 <[email protected]> 1752194028 -0700
gpgsig -----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAguk+sxdKD2pPHTiM809ZFwey63L
FaNOEinNxo7rzJZWcAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
AAAAQBfX+6mcKZBnXckwHcBFqRuXMD3vTKi1yv5wgrqIxTyr2LWB97xxmO92cvjsr0POQ2
2YA7mQS510Cg2s1uU1XAk=
-----END SSH SIGNATURE-----

init project
`))
require.NoError(t, err)

// the committingUser is guaranteed by the caller, parseCommitWithSSHSignature doesn't do any more checks
committingUser := &user_model.User{ID: 999, Name: "user-x"}
ret := parseCommitWithSSHSignature(t.Context(), commit, committingUser)
require.NotNil(t, ret)
assert.True(t, ret.Verified)
assert.Equal(t, committingUser.Name+" / "+sshPubKey.Fingerprint, ret.Reason)
assert.False(t, ret.Warning)
assert.Equal(t, committingUser, ret.SigningUser)
assert.Equal(t, committingUser, ret.CommittingUser)
assert.Equal(t, sshPubKey.ID, ret.SigningSSHKey.ID)
})

t.Run("TrustedSSHKey", func(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")()
defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "[email protected]")()
Expand Down