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

  1. Create a user with a confirmed primary email
  2. Add an SSH key to the user's account with usage type "Authentication & Signing"
  3. Add a secondary email to the user's account but do not confirm it
  4. Sign a commit using the SSH key with the unconfirmed secondary email as the committer email
  5. Push the commit to GitLab
  6. 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:

  1. User signs commit with their SSH key using an unconfirmed secondary email
  2. committer? calls User.find_by_any_email(committer_email) with default confirmed: false
  3. Due to the security fix, find_by_any_email returns nil for unconfirmed secondary emails
  4. committer? returns false because committer is nil
  5. calculate_verification_status returns :other_user at line 86
  6. 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:

  1. The SSH key already proves the user's identity
  2. We need to distinguish between "wrong user" and "right user, wrong/unverified email"
  3. The user needs specific guidance about verifying their email vs. fixing their SSH key configuration
Edited Jan 22, 2026 by Vasilii Iakliushin
Assignee Loading
Time tracking Loading