SSH signatures incorrectly marked as "Different user's signature" when signed with unconfirmed secondary email
Summary
SSH signatures are incorrectly marked with "Different user's signature" (:other_user status) when a commit is signed by a user with their own SSH key but using an unconfirmed secondary email address. The expected behavior is to show "Signature key mismatch" (:same_user_different_email status) with a message prompting the user to verify their email address.
This issue was introduced by the security fix in commit 8dc17acc (July 2022), which changed User.by_any_email to always filter out unconfirmed secondary emails.
Steps to reproduce
- Create a user with a confirmed primary email
- Add an SSH key to the user's account with usage type "Authentication & Signing"
- Add a secondary email to the user's account but do not confirm it
- Sign a commit using the SSH key with the unconfirmed secondary email as the committer email
- Push the commit to GitLab
- View the commit signature status
Expected result: The commit should show "Signature key mismatch" with the message: "This commit was signed with a verified signature, but the committer email is not verified."
Actual result: The commit shows "Different user's signature" as if the SSH key belongs to a different user.
Example
As reproduced in MR !217570: !217570 (comment 3003011752)
# User has SSH key and unconfirmed secondary email test_email@test_email.com
User.find_by_any_email("test_email@test_email.com")
# => nil (should find the user but doesn't due to security fix)
# When the email is verified:
User.find_by_any_email("test_email@test_email.com")
# => #<User id:1 @root>
Root Cause Analysis
The Security Fix (Commit 8dc17acc)
In July 2022, commit 8dc17acc fixed a security vulnerability where unconfirmed secondary emails could be linked to users. The fix modified User.by_any_email in app/models/user.rb:
# Before (insecure):
def by_any_email(emails, confirmed: false)
from_users = by_user_email(emails)
from_users = from_users.confirmed if confirmed
from_emails = by_emails(emails)
from_emails = from_emails.confirmed.merge(Email.confirmed) if confirmed # Only filtered when confirmed=true
# ...
end
# After (security fix):
def by_any_email(emails, confirmed: false)
from_users = by_user_email(emails)
from_users = from_users.confirmed if confirmed
from_emails = by_emails(emails).merge(Email.confirmed) # ALWAYS filters unconfirmed secondary emails
from_emails = from_emails.confirmed if confirmed
# ...
end
The change: The line from_emails = by_emails(emails).merge(Email.confirmed) now always filters to only confirmed secondary emails, regardless of the confirmed parameter.
How This Broke SSH Signature Verification
The SSH signature verification logic in lib/gitlab/ssh/signature.rb relies on User.find_by_any_email to determine if the committer email belongs to the key owner:
def committer?
# Lookup by email because users can push verified commits that were made
# by someone else. For example: Doing a rebase.
committer = User.find_by_any_email(committer_email) # Defaults to confirmed: false
committer && signed_by_key.user == committer
end
def calculate_verification_status
return :unknown_key unless signed_by_key
return :other_user unless committer? # ← Returns :other_user when committer? is false
return :same_user_different_email unless signed_by_user_email_verified? # ← Never reached!
:verified
end
The broken flow:
- User signs commit with their SSH key using an unconfirmed secondary email
-
committer?callsUser.find_by_any_email(committer_email)with defaultconfirmed: false - Due to the security fix,
find_by_any_emailreturnsnilfor unconfirmed secondary emails -
committer?returnsfalsebecausecommitterisnil -
calculate_verification_statusreturns:other_userat line 86 - The code never reaches line 87 to check
signed_by_user_email_verified?and return:same_user_different_email
Proposed Solution
The SSH signature verification logic needs to be updated to handle unconfirmed secondary emails. There are several approaches:
Option 1: Check if email belongs to key owner (Recommended)
Modify lib/gitlab/ssh/signature.rb to check if the committer email belongs to the SSH key owner, even if unconfirmed:
def committer?
# For SSH signatures, we need to check if the email belongs to the key owner
# even if it's unconfirmed, to properly distinguish between :other_user and :same_user_different_email
committer = User.find_by_any_email(committer_email)
return false unless committer
signed_by_key.user == committer
end
def signed_by_user_email?
# Check if the committer email belongs to the key owner (confirmed or unconfirmed)
signed_by_key.user.all_emails.include?(committer_email)
end
def calculate_verification_status
return :unknown_key unless signed_by_key
return :other_user unless signed_by_user_email? # Check if email belongs to key owner
return :same_user_different_email unless signed_by_user_email_verified?
:verified
end
Option 2: Add a method to check unconfirmed emails
Add a new method User.email_belongs_to_user?(email, user) that checks both confirmed and unconfirmed emails for a specific user, without exposing the general find_by_any_email to unconfirmed emails.
Related Issues and MRs
- MR !217570: Add :same_user_different_email status for SSH signatures
- Security fix commit: 8dc17acc (July 2022)
- Original security MR: gitlab-org/security/gitlab!2491
Additional Context
The security fix was necessary and correct — we should not link unconfirmed secondary emails to users in general contexts. However, the SSH signature verification logic needs special handling because:
- The SSH key already proves the user's identity
- We need to distinguish between "wrong user" and "right user, wrong/unverified email"
- The user needs specific guidance about verifying their email vs. fixing their SSH key configuration