Commit 320da597 authored by Douwe Maan's avatar Douwe Maan Committed by Jose Ivan Vargas

Merge branch 'feature/gpg-verification-status' into 'master'

GPG signature must match the committer in order to be verified

See merge request !13771
parent 74d579b9
......@@ -248,7 +248,10 @@ $(function () {
// Initialize popovers
$body.popover({
selector: '[data-toggle="popover"]',
trigger: 'focus'
trigger: 'focus',
// set the viewport to the main content, excluding the navigation bar, so
// the navigation can't overlap the popover
viewport: '.page-with-sidebar'
});
$('.trigger-submit').on('change', function () {
return $(this).parents('form').submit();
......
......@@ -393,6 +393,6 @@ class Commit
end
def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self)
@gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end
end
......@@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base
def verified_user_infos
user_infos.select do |user_info|
user_info[:email] == user.email
user.verified_email?(user_info[:email])
end
end
......@@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base
user_infos.map do |user_info|
[
user_info[:email],
user_info[:email] == user.email
user.verified_email?(user_info[:email])
]
end.to_h
end
def verified?
emails_with_verified_status.any? { |_email, verified| verified }
emails_with_verified_status.values.any?
end
def verified_and_belongs_to_email?(email)
emails_with_verified_status.fetch(email, false)
end
def update_invalid_gpg_signatures
......@@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base
end
def revoke
GpgSignature.where(gpg_key: self, valid_signature: true).update_all(
gpg_key_id: nil,
valid_signature: false,
updated_at: Time.zone.now
)
GpgSignature
.where(gpg_key: self)
.where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
.update_all(
gpg_key_id: nil,
verification_status: GpgSignature.verification_statuses[:unknown_key],
updated_at: Time.zone.now
)
destroy
end
......
class GpgSignature < ActiveRecord::Base
include ShaAttribute
include IgnorableColumn
ignore_column :valid_signature
sha_attribute :commit_sha
sha_attribute :gpg_key_primary_keyid
enum verification_status: {
unverified: 0,
verified: 1,
same_user_different_email: 2,
other_user: 3,
unverified_key: 4,
unknown_key: 5
}
belongs_to :project
belongs_to :gpg_key
......@@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base
end
def gpg_commit
Gitlab::Gpg::Commit.new(project, commit_sha)
Gitlab::Gpg::Commit.new(commit)
end
end
......@@ -1045,6 +1045,10 @@ class User < ActiveRecord::Base
ensure_rss_token!
end
def verified_email?(email)
self.email == email
end
protected
# override, from Devise::Validatable
......
- title = capture do
.gpg-popover-icon.invalid
= render 'shared/icons/icon_status_notfound_borderless.svg'
%div
This commit was signed with an <strong>unverified</strong> signature.
This commit was signed with a different user's verified signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] }
- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
This commit was signed with a verified signature, but the committer email
is <strong>not verified</strong> to belong to the same user.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
- if signature
- if signature.valid_signature?
= render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
- else
= render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
= render partial: "projects/commit/#{signature.verification_status}_signature_badge", locals: { signature: signature }
- css_classes = commit_signature_badge_classes(css_classes)
- signature = local_assigns.fetch(:signature)
- title = local_assigns.fetch(:title)
- label = local_assigns.fetch(:label)
- css_class = local_assigns.fetch(:css_class)
- icon = local_assigns.fetch(:icon)
- show_user = local_assigns.fetch(:show_user, false)
- css_classes = commit_signature_badge_classes(css_class)
- title = capture do
.gpg-popover-status
= title
.gpg-popover-icon{ class: css_class }
= render "shared/icons/#{icon}.svg"
%div
= title
- content = capture do
.clearfix
= content
- if show_user
.clearfix
= render partial: 'projects/commit/signature_badge_user', locals: { signature: signature }
GPG Key ID:
%span.monospace= signature.gpg_key_primary_keyid
= link_to('Learn more about signing commits', help_page_path('user/project/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
= link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
= label
- gpg_key = signature.gpg_key
- user = gpg_key&.user
- user_name = signature.gpg_key_user_name
- user_email = signature.gpg_key_user_email
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
%div
= user_avatar_without_link(user: user, size: 32)
%div
%strong= user.name
%div= user.to_reference
- else
= mail_to user_email do
%div
= user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
%div
%strong= user_name
%div= user_email
= render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature }
= render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature }
- title = capture do
This commit was signed with an <strong>unverified</strong> signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' }
= render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
.gpg-popover-icon.valid
= render 'shared/icons/icon_status_success_borderless.svg'
%div
This commit was signed with a <strong>verified</strong> signature.
- content = capture do
- gpg_key = signature.gpg_key
- user = gpg_key&.user
- user_name = signature.gpg_key_user_name
- user_email = signature.gpg_key_user_email
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
%div
= user_avatar_without_link(user: user, size: 32)
%div
%strong= gpg_key.user.name
%div @#{gpg_key.user.username}
- else
= mail_to user_email do
%div
= user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
%div
%strong= user_name
%div= user_email
- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] }
= render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
This commit was signed with a <strong>verified</strong> signature and the
committer email is verified to belong to the same user.
- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
......@@ -6,7 +6,11 @@ class CreateGpgSignatureWorker
project = Project.find_by(id: project_id)
return unless project
commit = project.commit(commit_sha)
return unless commit
# This calculates and caches the signature in the database
Gitlab::Gpg::Commit.new(project, commit_sha).signature
Gitlab::Gpg::Commit.new(commit).signature
end
end
---
title: 'Update the GPG verification semantics: A GPG signature must additionally match
the committer in order to be verified'
merge_request: 13771
author: Alexis Reigel
type: changed
class AddVerificationStatusToGpgSignatures < ActiveRecord::Migration
DOWNTIME = false
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
# First we remove all signatures because we need to re-verify them all
# again anyway (because of the updated verification logic).
#
# This makes adding the column with default values faster
truncate(:gpg_signatures)
add_column_with_default(:gpg_signatures, :verification_status, :smallint, default: 0)
end
def down
remove_column(:gpg_signatures, :verification_status)
end
end
class DestroyGpgSignatures < ActiveRecord::Migration
DOWNTIME = false
def up
truncate(:gpg_signatures)
end
def down
end
end
class RemoveValidSignatureFromGpgSignatures < ActiveRecord::Migration
DOWNTIME = false
def up
remove_column :gpg_signatures, :valid_signature
end
def down
add_column :gpg_signatures, :valid_signature, :boolean
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170824162758) do
ActiveRecord::Schema.define(version: 20170831195038) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -596,11 +596,11 @@ ActiveRecord::Schema.define(version: 20170824162758) do
t.datetime "updated_at", null: false
t.integer "project_id"
t.integer "gpg_key_id"
t.boolean "valid_signature"
t.binary "commit_sha"
t.binary "gpg_key_primary_keyid"
t.text "gpg_key_user_name"
t.text "gpg_key_user_email"
t.integer "verification_status", limit: 2, default: 0, null: false
end
add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree
......
# Signing commits with GPG
> [Introduced][ce-9546] in GitLab 9.5.
GitLab can show whether a commit is verified or not when signed with a GPG key.
All you need to do is upload the public GPG key in your profile settings.
GPG verified tags are not supported yet.
## Getting started with GPG
Here are a few guides to get you started with GPG:
- [Git Tools - Signing Your Work](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work)
- [Managing OpenPGP Keys](https://riseup.net/en/security/message-security/openpgp/gpg-keys)
- [OpenPGP Best Practices](https://riseup.net/en/security/message-security/openpgp/best-practices)
- [Creating a new GPG key with subkeys](https://www.void.gr/kargig/blog/2013/12/02/creating-a-new-gpg-key-with-subkeys/) (advanced)
## How GitLab handles GPG
GitLab uses its own keyring to verify the GPG signature. It does not access any
public key server.
In order to have a commit verified on GitLab the corresponding public key needs
to be uploaded to GitLab. For a signature to be verified three conditions need
to be met:
1. The public key needs to be added your GitLab account
1. One of the emails in the GPG key matches your **primary** email
1. The committer's email matches the verified email from the gpg key
## Generating a GPG key
If you don't already have a GPG key, the following steps will help you get
started:
1. [Install GPG](https://www.gnupg.org/download/index.html) for your operating system
1. Generate the private/public key pair with the following command:
```sh
gpg --full-gen-key
```
This will spawn a series of questions.
1. The first question is which algorithm can be used. Select the kind you want
or press <kbd>Enter</kbd> to choose the default (RSA and RSA):
```
Please select what kind of key you want:
(1) RSA and RSA (default)
(2) DSA and Elgamal
(3) DSA (sign only)
(4) RSA (sign only)
Your selection? 1
```
1. The next question is key length. We recommend to choose the highest value
which is `4096`:
```
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 4096
Requested keysize is 4096 bits
```
1. Next, you need to specify the validity period of your key. This is something
subjective, and you can use the default value which is to never expire:
```
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
```
1. Confirm that the answers you gave were correct by typing `y`:
```
Is this correct? (y/N) y
```
1. Enter you real name, the email address to be associated with this key (should
match the primary email address you use in GitLab) and an optional comment
(press <kbd>Enter</kbd> to skip):
```
GnuPG needs to construct a user ID to identify your key.
Real name: Mr. Robot
Email address: mr@robot.sh
Comment:
You selected this USER-ID:
"Mr. Robot <mr@robot.sh>"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
```
1. Pick a strong password when asked and type it twice to confirm.
1. Use the following command to list the private GPG key you just created:
```
gpg --list-secret-keys mr@robot.sh
```
Replace `mr@robot.sh` with the email address you entered above.
1. Copy the GPG key ID that starts with `sec`. In the following example, that's
`0x30F2B65B9246B6CA`:
```
sec rsa4096/0x30F2B65B9246B6CA 2017-08-18 [SC]
D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA
uid [ultimate] Mr. Robot <mr@robot.sh>
ssb rsa4096/0xB7ABC0813E4028C0 2017-08-18 [E]
```
1. Export the public key of that ID (replace your key ID from the previous step):
```
gpg --armor --export 0x30F2B65B9246B6CA
```
1. Finally, copy the public key and [add it in your profile settings](#adding-a-gpg-key-to-your-account)
## Adding a GPG key to your account
>**Note:**
Once you add a key, you cannot edit it, only remove it. In case the paste
didn't work, you'll have to remove the offending key and re-add it.
You can add a GPG key in your profile's settings:
1. On the upper right corner, click on your avatar and go to your **Settings**.
![Settings dropdown](../../../profile/img/profile_settings_dropdown.png)
1. Navigate to the **GPG keys** tab and paste your _public_ key in the 'Key'
box.
![Paste GPG public key](img/profile_settings_gpg_keys_paste_pub.png)
1. Finally, click on **Add key** to add it to GitLab. You will be able to see
its fingerprint, the corresponding email address and creation date.
![GPG key single page](img/profile_settings_gpg_keys_single_key.png)
## Associating your GPG key with Git
After you have [created your GPG key](#generating-a-gpg-key) and [added it to
your account](#adding-a-gpg-key-to-your-account), it's time to tell Git which
key to use.
1. Use the following command to list the private GPG key you just created:
```
gpg --list-secret-keys mr@robot.sh
```
Replace `mr@robot.sh` with the email address you entered above.
1. Copy the GPG key ID that starts with `sec`. In the following example, that's
`0x30F2B65B9246B6CA`:
```
sec rsa4096/0x30F2B65B9246B6CA 2017-08-18 [SC]
D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA
uid [ultimate] Mr. Robot <mr@robot.sh>
ssb rsa4096/0xB7ABC0813E4028C0 2017-08-18 [E]
```
1. Tell Git to use that key to sign the commits:
```
git config --global user.signingkey 0x30F2B65B9246B6CA
```
Replace `0x30F2B65B9246B6CA` with your GPG key ID.
## Signing commits
After you have [created your GPG key](#generating-a-gpg-key) and [added it to
your account](#adding-a-gpg-key-to-your-account), you can start signing your
commits:
1. Commit like you used to, the only difference is the addition of the `-S` flag:
```
git commit -S -m "My commit msg"
```
1. Enter the passphrase of your GPG key when asked.
1. Push to GitLab and check that your commits [are verified](#verifying-commits).
If you don't want to type the `-S` flag every time you commit, you can tell Git
to sign your commits automatically:
```
git config --global commit.gpgsign true
```
## Verifying commits
1. Within a project or [merge request](../../merge_requests/index.md), navigate to
the **Commits** tab. Signed commits will show a badge containing either
"Verified" or "Unverified", depending on the verification status of the GPG
signature.
![Signed and unsigned commits](img/project_signed_and_unsigned_commits.png)
1. By clicking on the GPG badge, details of the signature are displayed.
![Signed commit with verified signature](img/project_signed_commit_verified_signature.png)
![Signed commit with verified signature](img/project_signed_commit_unverified_signature.png)
## Revoking a GPG key
Revoking a key **unverifies** already signed commits. Commits that were
verified by using this key will change to an unverified state. Future commits
will also stay unverified once you revoke this key. This action should be used
in case your key has been compromised.
To revoke a GPG key:
1. On the upper right corner, click on your avatar and go to your **Settings**.
1. Navigate to the **GPG keys** tab.
1. Click on **Revoke** besides the GPG key you want to delete.
## Removing a GPG key
Removing a key **does not unverify** already signed commits. Commits that were
verified by using this key will stay verified. Only unpushed commits will stay
unverified once you remove this key. To unverify already signed commits, you need
to [revoke the associated GPG key](#revoking-a-gpg-key) from your account.
To remove a GPG key from your account:
1. On the upper right corner, click on your avatar and go to your **Settings**.
1. Navigate to the **GPG keys** tab.
1. Click on the trash icon besides the GPG key you want to delete.
[ce-9546]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9546
......@@ -39,7 +39,7 @@ module Gitlab
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
GPGME::Key.find(:public, fingerprints).flat_map do |raw_key|
raw_key.uids.map { |uid| { name: uid.name, email: uid.email } }
raw_key.uids.map { |uid| { name: uid.name, email: uid.email.downcase } }
end
end
end
......
module Gitlab
module Gpg
class Commit
def self.for_commit(commit)
new(commit.project, commit.sha)
end
def initialize(project, sha)
@project = project
@sha = sha
def initialize(commit)
@commit = commit
@signature_text, @signed_text =
begin
Rugged::Commit.extract_signature(project.repository.rugged, sha)
Rugged::Commit.extract_signature(@commit.project.repository.rugged, @commit.sha)
rescue Rugged::OdbError
nil
end
......@@ -26,7 +21,7 @@ module Gitlab
return @signature if @signature
cached_signature = GpgSignature.find_by(commit_sha: @sha)
cached_signature = GpgSignature.find_by(commit_sha: @commit.sha)
return @signature = cached_signature if cached_signature.present?
@signature = create_cached_signature!
......@@ -73,20 +68,31 @@ module Gitlab
def attributes(gpg_key)
user_infos = user_infos(gpg_key)
verification_status = verification_status(gpg_key)
{
commit_sha: @sha,
project: @project,
commit_sha: @commit.sha,
project: @commit.project,
gpg_key: gpg_key,
gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint,
gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email],
valid_signature: gpg_signature_valid_signature_value(gpg_key)
verification_status: verification_status
}
end
def gpg_signature_valid_signature_value(gpg_key)
!!(gpg_key && gpg_key.verified? && verified_signature.valid?)
def verification_status(gpg_key)
return :unknown_key unless gpg_key
return :unverified_key unless gpg_key.verified?
return :unverified unless verified_signature.valid?
if gpg_key.verified_and_belongs_to_email?(@commit.committer_email)
:verified
elsif gpg_key.user.all_emails.include?(@commit.committer_email)
:same_user_different_email
else
:other_user
end
end
def user_infos(gpg_key)
......
......@@ -8,7 +8,7 @@ module Gitlab
def run
GpgSignature
.select(:id, :commit_sha, :project_id)
.where('gpg_key_id IS NULL OR valid_signature = ?', false)
.where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified])
.where(gpg_key_primary_keyid: @gpg_key.primary_keyid)
.find_each { |sig| sig.gpg_commit.update_signature!(sig) }
end
......
......@@ -6,6 +6,6 @@ FactoryGirl.define do
project
gpg_key
gpg_key_primary_keyid { gpg_key.primary_keyid }
valid_signature true
verification_status :verified
end
end
......@@ -203,105 +203,4 @@ describe 'Commits' do
end
end
end
describe 'GPG signed commits', :js do
it 'changes from unverified to verified when the user changes his email to match the gpg key' do
user = create :user, email: 'unrelated.user@example.org'
project.team << [user, :master]
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
sign_in(user)
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).not_to have_content 'Verified'
end
# user changes his email which makes the gpg key verified
Sidekiq::Testing.inline! do
user.skip_reconfirmation!
user.update_attributes!(email: GpgHelpers::User1.emails.first)
end
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).to have_content 'Verified'
end
end
it 'changes from unverified to verified when the user adds the missing gpg key' do
user = create :user, email: GpgHelpers::User1.emails.first
project.team << [user, :master]
sign_in(user)
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).not_to have_content 'Verified'
end
# user adds the gpg key which makes the signature valid
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).to have_content 'Verified'
end
end
it 'shows popover badges' do
gpg_user = create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard'
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: gpg_user
end
user = create :user
project.team << [user, :master]
sign_in(user)
visit project_commits_path(project, :'signed-commits')
# unverified signature
click_on 'Unverified', match: :first
within '.popover' do
expect(page).to have_content 'This commit was signed with an unverified signature.'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
# verified and the gpg user has a gitlab profile
click_on 'Verified', match: :first
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature.'
expect(page).to have_content 'Nannie Bernhard'
expect(page).to have_content '@nannie.bernhard'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
# verified and the gpg user's profile doesn't exist anymore
gpg_user.destroy!
visit project_commits_path(project, :'signed-commits')
click_on 'Verified', match: :first
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature.'
expect(page).to have_content 'Nannie Bernhard'
expect(page).to have_content 'nannie.bernhard@example.com'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
end
end
end
......@@ -42,7 +42,7 @@ feature 'Profile > GPG Keys' do
scenario 'User revokes a key via the key index' do
gpg_key = create :gpg_key, user: user, key: GpgHelpers::User2.public_key
gpg_signature = create :gpg_signature, gpg_key: gpg_key, valid_signature: true
gpg_signature = create :gpg_signature, gpg_key: gpg_key, verification_status: :verified
visit profile_gpg_keys_path
......@@ -51,7 +51,7 @@ feature 'Profile > GPG Keys' do
expect(page).to have_content('Your GPG keys (0)')
expect(gpg_signature.reload).to have_attributes(
valid_signature: false,
verification_status: 'unknown_key',
gpg_key: nil
)
end
......
require 'spec_helper'
describe 'GPG signed commits', :js do
let(:project) { create(:project, :repository) }
it 'changes from unverified to verified when the user changes his email to match the gpg key' do
user = create :user, email: 'unrelated.user@example.org'
project.team << [user, :master]
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
sign_in(user)
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).not_to have_content 'Verified'
end
# user changes his email which makes the gpg key verified
Sidekiq::Testing.inline! do
user.skip_reconfirmation!
user.update_attributes!(email: GpgHelpers::User1.emails.first)
end
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).to have_content 'Verified'
end
end
it 'changes from unverified to verified when the user adds the missing gpg key' do
user = create :user, email: GpgHelpers::User1.emails.first
project.team << [user, :master]
sign_in(user)
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).not_to have_content 'Verified'
end
# user adds the gpg key which makes the signature valid
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
visit project_commits_path(project, :'signed-commits')
within '#commits-list' do
expect(page).to have_content 'Unverified'
expect(page).to have_content 'Verified'
end
end
context 'shows popover badges' do
let(:user_1) do
create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard'
end
let(:user_1_key) do
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user_1
end
end
let(:user_2) do
create(:user, email: GpgHelpers::User2.emails.first, username: 'bette.cartwright', name: 'Bette Cartwright').tap do |user|
# secondary, unverified email
create :email, user: user, email: GpgHelpers::User2.emails.last
end
end
let(:user_2_key) do
Sidekiq::Testing.inline! do
create :gpg_key, key: GpgHelpers::User2.public_key, user: user_2
end
end
before do
user = create :user
project.team << [user, :master]
sign_in(user)
end
it 'unverified signature' do
visit project_commits_path(project, :'signed-commits')
within(find('.commit', text: 'signed commit by bette cartwright')) do
click_on 'Unverified'
within '.popover' do
expect(page).to have_content 'This commit was signed with an unverified signature.'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
end
end
it 'unverified signature: user email does not match the committer email, but is the same user' do
user_2_key
visit project_commits_path(project, :'signed-commits')
within(find('.commit', text: 'signed and authored commit by bette cartwright, different email')) do
click_on 'Unverified'
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.'
expect(page).to have_content 'Bette Cartwright'
expect(page).to have_content '@bette.cartwright'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
end
end
it 'unverified signature: user email does not match the committer email' do
user_2_key
visit project_commits_path(project, :'signed-commits')
within(find('.commit', text: 'signed commit by bette cartwright')) do
click_on 'Unverified'
within '.popover' do
expect(page).to have_content "This commit was signed with a different user's verified signature."
expect(page).to have_content 'Bette Cartwright'
expect(page).to have_content '@bette.cartwright'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
end
end
it 'verified and the gpg user has a gitlab profile' do
user_1_key
visit project_commits_path(project, :'signed-commits')
within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
click_on 'Verified'
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
expect(page).to have_content 'Nannie Bernhard'
expect(page).to have_content '@nannie.bernhard'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
end
end
it "verified and the gpg user's profile doesn't exist anymore" do
user_1_key
visit project_commits_path(project, :'signed-commits')
# wait for the signature to get generated
within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
expect(page).to have_content 'Verified'
end
user_1.destroy!
refresh
within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
click_on 'Verified'
within '.popover' do
expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
expect(page).to have_content 'Nannie Bernhard'
expect(page).to have_content 'nannie.bernhard@example.com'
expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
end
end
end
end
......@@ -2,45 +2,9 @@ require 'rails_helper'
describe Gitlab::Gpg::Commit do
describe '#signature' do
let!(:project) { create :project, :repository, path: 'sample-project' }
let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
context 'unsigned commit' do
it 'returns nil' do
expect(described_class.new(project, commit_sha).signature).to be_nil
end
end
context 'known and verified public key' do
let!(:gpg_key) do
create :gpg_key, key: GpgHelpers::User1.public_key, user: create(:user, email: GpgHelpers::User1.emails.first)
end
before do
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
.and_return(
[
GpgHelpers::User1.signed_commit_signature,
GpgHelpers::User1.signed_commit_base_data
]
)
end
it 'returns a valid signature' do
expect(described_class.new(project, commit_sha).signature).to have_attributes(
commit_sha: commit_sha,
project: project,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
gpg_key_user_name: GpgHelpers::User1.names.first,
gpg_key_user_email: GpgHelpers::User1.emails.first,
valid_signature: true
)
end
shared_examples 'returns the cached signature on second call' do
it 'returns the cached signature on second call' do
gpg_commit = described_class.new(project, commit_sha)
gpg_commit = described_class.new(commit)
expect(gpg_commit).to receive(:using_keychain).and_call_original
gpg_commit.signature
......@@ -51,11 +15,140 @@ describe Gitlab::Gpg::Commit do
end
end
context 'known but unverified public key' do
let!(:gpg_key) { create :gpg_key, key: GpgHelpers::User1.public_key }
let!(:project) { create :project, :repository, path: 'sample-project' }
let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
before do
allow(Rugged::Commit).to receive(:extract_signature)
context 'unsigned commit' do
let!(:commit) { create :commit, project: project, sha: commit_sha }
it 'returns nil' do
expect(described_class.new(commit).signature).to be_nil
end
end
context 'known key' do
context 'user matches the key uid' do
context 'user email matches the email committer' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
let!(:gpg_key) do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
before do
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
.and_return(
[
GpgHelpers::User1.signed_commit_signature,
GpgHelpers::User1.signed_commit_base_data
]
)
end
it 'returns a valid signature' do
expect(described_class.new(commit).signature).to have_attributes(
commit_sha: commit_sha,
project: project,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
gpg_key_user_name: GpgHelpers::User1.names.first,
gpg_key_user_email: GpgHelpers::User1.emails.first,
verification_status: 'verified'
)
end
it_behaves_like 'returns the cached signature on second call'
end
context 'user email does not match the committer email, but is the same user' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first }
let(:user) do
create(:user, email: GpgHelpers::User1.emails.first).tap do |user|
create :email, user: user, email: GpgHelpers::User2.emails.first
end
end
let!(:gpg_key) do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
before do
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
.and_return(
[
GpgHelpers::User1.signed_commit_signature,
GpgHelpers::User1.signed_commit_base_data
]
)
end
it 'returns an invalid signature' do
expect(described_class.new(commit).signature).to have_attributes(
commit_sha: commit_sha,
project: project,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
gpg_key_user_name: GpgHelpers::User1.names.first,
gpg_key_user_email: GpgHelpers::User1.emails.first,
verification_status: 'same_user_different_email'
)
end
it_behaves_like 'returns the cached signature on second call'
end
context 'user email does not match the committer email' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first }
let(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
let!(:gpg_key) do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
before do
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
.and_return(
[
GpgHelpers::User1.signed_commit_signature,
GpgHelpers::User1.signed_commit_base_data
]
)
end
it 'returns an invalid signature' do
expect(described_class.new(commit).signature).to have_attributes(
commit_sha: commit_sha,
project: project,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
gpg_key_user_name: GpgHelpers::User1.names.first,
gpg_key_user_email: GpgHelpers::User1.emails.first,
verification_status: 'other_user'
)
end
it_behaves_like 'returns the cached signature on second call'
end
end
context 'user does not match the key uid' do
let!(:commit) { create :commit, project: project, sha: commit_sha }
let(:user) { create(:user, email: GpgHelpers::User2.emails.first) }
let!(:gpg_key) do
create :gpg_key, key: GpgHelpers::User1.public_key, user: user
end
before do
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
.and_return(
[
......@@ -63,33 +156,27 @@ describe Gitlab::Gpg::Commit do
GpgHelpers::User1.signed_commit_base_data
]
)
end
it 'returns an invalid signature' do
expect(described_class.new(project, commit_sha).signature).to have_attributes(
commit_sha: commit_sha,
project: project,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
gpg_key_user_name: GpgHelpers::User1.names.first,
gpg_key_user_email: GpgHelpers::User1.emails.first,
valid_signature: false
)
end
it 'returns the cached signature on second call' do
gpg_commit = described_class.new(project, commit_sha)
expect(gpg_commit).to receive(:using_keychain).and_call_original
gpg_commit.signature
end
it 'returns an invalid signature' do
expect(described_class.new(commit).signature).to have_attributes(
commit_sha: commit_sha,
project: project,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
gpg_key_user_name: GpgHelpers::User1.names.first,
gpg_key_user_email: GpgHelpers::User1.emails.first,
verification_status: 'unverified_key'
)
end
# consecutive call
expect(gpg_commit).not_to receive(:using_keychain).and_call_original
gpg_commit.signature
it_behaves_like 'returns the cached signature on second call'
end
end
context 'unknown public key' do
context 'unknown key' do
let!(:commit) { create :commit, project: project, sha: commit_sha }
before do
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
......@@ -102,27 +189,18 @@ describe Gitlab::Gpg::Commit do
end
it 'returns an invalid signature' do
expect(described_class.new(project, commit_sha).signature).to have_attributes(
expect(described_class.new(commit).signature).to have_attributes(
commit_sha: commit_sha,
project: project,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
gpg_key_user_name: nil,
gpg_key_user_email: nil,
valid_signature: false
verification_status: 'unknown_key'
)
end
it 'returns the cached signature on second call' do
gpg_commit = described_class.new(project, commit_sha)
expect(gpg_commit).to receive(:using_keychain).and_call_original
gpg_commit.signature
# consecutive call
expect(gpg_commit).not_to receive(:using_keychain).and_call_original
gpg_commit.signature
end
it_behaves_like 'returns the cached signature on second call'
end
end
end
......@@ -4,8 +4,29 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
describe '#run' do
let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
let!(:project) { create :project, :repository, path: 'sample-project' }
let!(:raw_commit) do
raw_commit = double(
:raw_commit,
signature: [
GpgHelpers::User1.signed_commit_signature,
GpgHelpers::User1.signed_commit_base_data
],
sha: commit_sha,
committer_email: GpgHelpers::User1.emails.first
)
allow(raw_commit).to receive :save!
raw_commit
end
let!(:commit) do
create :commit, git_commit: raw_commit, project: project
end
before do
allow_any_instance_of(Project).to receive(:commit).and_return(commit)
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
.and_return(
......@@ -25,7 +46,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: true
verification_status: 'verified'
end
it 'assigns the gpg key to the signature when the missing gpg key is added' do
......@@ -39,7 +60,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: true
verification_status: 'verified'
)
end
......@@ -54,7 +75,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: true
verification_status: 'verified'
)
end
end
......@@ -68,7 +89,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: false
verification_status: 'unknown_key'
end
it 'updates the signature to being valid when the missing gpg key is added' do
......@@ -82,7 +103,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: true
verification_status: 'verified'
)
end
......@@ -97,7 +118,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: false
verification_status: 'unknown_key'
)
end
end
......@@ -115,7 +136,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: false
verification_status: 'unknown_key'
end
it 'updates the signature to being valid when the user updates the email address' do
......@@ -123,7 +144,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
key: GpgHelpers::User1.public_key,
user: user
expect(invalid_gpg_signature.reload.valid_signature).to be_falsey
expect(invalid_gpg_signature.reload.verification_status).to eq 'unverified_key'
# InvalidGpgSignatureUpdater is called by the after_update hook
user.update_attributes!(email: GpgHelpers::User1.emails.first)
......@@ -133,7 +154,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: true
verification_status: 'verified'
)
end
......@@ -147,7 +168,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: false
verification_status: 'unverified_key'
)
# InvalidGpgSignatureUpdater is called by the after_update hook
......@@ -158,7 +179,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
valid_signature: false
verification_status: 'unverified_key'
)
end
end
......
......@@ -42,6 +42,21 @@ describe Gitlab::Gpg do
described_class.user_infos_from_key('bogus')
).to eq []
end
it 'downcases the email' do
public_key = double(:key)
fingerprints = double(:fingerprints)
uid = double(:uid, name: 'Nannie Bernhard', email: 'NANNIE.BERNHARD@EXAMPLE.COM')
raw_key = double(:raw_key, uids: [uid])
allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints)
allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key])
user_infos = described_class.user_infos_from_key(public_key)
expect(user_infos).to eq([{
name: 'Nannie Bernhard',
email: 'nannie.bernhard@example.com'
}])
end
end
describe '.current_home_dir' do
......
......@@ -99,14 +99,14 @@ describe GpgKey do
end
describe '#verified?' do
it 'returns true one of the email addresses in the key belongs to the user' do
it 'returns true if one of the email addresses in the key belongs to the user' do
user = create :user, email: 'bette.cartwright@example.com'
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
expect(gpg_key.verified?).to be_truthy
end
it 'returns false if one of the email addresses in the key does not belong to the user' do
it 'returns false if none of the email addresses in the key does not belong to the user' do
user = create :user, email: 'someone.else@example.com'
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
......@@ -114,6 +114,32 @@ describe GpgKey do
end
end
describe 'verified_and_belongs_to_email?' do
it 'returns false if none of the email addresses in the key does not belong to the user' do
user = create :user, email: 'someone.else@example.com'
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
expect(gpg_key.verified?).to be_falsey
expect(gpg_key.verified_and_belongs_to_email?('someone.else@example.com')).to be_falsey
end
it 'returns false if one of the email addresses in the key belongs to the user and does not match the provided email' do
user = create :user, email: 'bette.cartwright@example.com'
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
expect(gpg_key.verified?).to be_truthy
expect(gpg_key.verified_and_belongs_to_email?('bette.cartwright@example.net')).to be_falsey
end
it 'returns true if one of the email addresses in the key belongs to the user and matches the provided email' do
user = create :user, email: 'bette.cartwright@example.com'
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
expect(gpg_key.verified?).to be_truthy
expect(gpg_key.verified_and_belongs_to_email?('bette.cartwright@example.com')).to be_truthy
end
end
describe 'notification', :mailer do
let(:user) { create(:user) }
......@@ -129,15 +155,15 @@ describe GpgKey do
describe '#revoke' do
it 'invalidates all associated gpg signatures and destroys the key' do
gpg_key = create :gpg_key
gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: gpg_key
gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: gpg_key
unrelated_gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key
unrelated_gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: unrelated_gpg_key
unrelated_gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: unrelated_gpg_key
gpg_key.revoke
expect(gpg_signature.reload).to have_attributes(
valid_signature: false,
verification_status: 'unknown_key',
gpg_key: nil
)
......@@ -145,7 +171,7 @@ describe GpgKey do
# unrelated signature is left untouched
expect(unrelated_gpg_signature.reload).to have_attributes(
valid_signature: true,
verification_status: 'verified',
gpg_key: unrelated_gpg_key
)
......
......@@ -2074,4 +2074,18 @@ describe User do
end
end
end
describe '#verified_email?' do
it 'returns true when the email is the primary email' do
user = build :user, email: 'email@example.com'
expect(user.verified_email?('email@example.com')).to be true
end
it 'returns false when the email is not the primary email' do
user = build :user, email: 'email@example.com'
expect(user.verified_email?('other_email@example.com')).to be false
end
end
end
......@@ -5,7 +5,7 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
'signed-commits' => '5d4a1cb',
'signed-commits' => '2d1096e',
'not-merged-branch' => 'b83d6e3',
'branch-merged' => '498214d',
'empty-branch' => '7efb185',
......
......@@ -7,9 +7,14 @@ describe CreateGpgSignatureWorker do
let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
it 'calls Gitlab::Gpg::Commit#signature' do
expect(Gitlab::Gpg::Commit).to receive(:new).with(project, commit_sha).and_call_original
commit = instance_double(Commit)
gpg_commit = instance_double(Gitlab::Gpg::Commit)
expect_any_instance_of(Gitlab::Gpg::Commit).to receive(:signature)
allow(Project).to receive(:find_by).with(id: project.id).and_return(project)
allow(project).to receive(:commit).with(commit_sha).and_return(commit)
expect(Gitlab::Gpg::Commit).to receive(:new).with(commit).and_return(gpg_commit)
expect(gpg_commit).to receive(:signature)
described_class.new.perform(commit_sha, project.id)
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment