Commit dd42cb5f authored by Douwe Maan's avatar Douwe Maan

Merge branch '36829-gpg-commit-not-verified-if-signed-with-a-subkey' into 'master'

Add support for GPG subkeys in signature verification

Closes #36829

See merge request gitlab-org/gitlab-ce!14517
parents 05054803 555f50b3
...@@ -108,6 +108,15 @@ ...@@ -108,6 +108,15 @@
} }
} }
.subkeys-list {
@include basic-list;
li {
padding: 3px 0;
border: none;
}
}
.key-list-item { .key-list-item {
.key-list-item-info { .key-list-item-info {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
......
...@@ -2,7 +2,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController ...@@ -2,7 +2,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
before_action :set_gpg_key, only: [:destroy, :revoke] before_action :set_gpg_key, only: [:destroy, :revoke]
def index def index
@gpg_keys = current_user.gpg_keys @gpg_keys = current_user.gpg_keys.with_subkeys
@gpg_key = GpgKey.new @gpg_key = GpgKey.new
end end
......
...@@ -9,6 +9,9 @@ class GpgKey < ActiveRecord::Base ...@@ -9,6 +9,9 @@ class GpgKey < ActiveRecord::Base
belongs_to :user belongs_to :user
has_many :gpg_signatures has_many :gpg_signatures
has_many :subkeys, class_name: 'GpgKeySubkey'
scope :with_subkeys, -> { includes(:subkeys) }
validates :user, presence: true validates :user, presence: true
...@@ -36,10 +39,12 @@ class GpgKey < ActiveRecord::Base ...@@ -36,10 +39,12 @@ class GpgKey < ActiveRecord::Base
before_validation :extract_fingerprint, :extract_primary_keyid before_validation :extract_fingerprint, :extract_primary_keyid
after_commit :update_invalid_gpg_signatures, on: :create after_commit :update_invalid_gpg_signatures, on: :create
after_create :generate_subkeys
def primary_keyid def primary_keyid
super&.upcase super&.upcase
end end
alias_method :keyid, :primary_keyid
def fingerprint def fingerprint
super&.upcase super&.upcase
...@@ -49,6 +54,10 @@ class GpgKey < ActiveRecord::Base ...@@ -49,6 +54,10 @@ class GpgKey < ActiveRecord::Base
super(value&.strip) super(value&.strip)
end end
def keyids
[keyid].concat(subkeys.map(&:keyid))
end
def user_infos def user_infos
@user_infos ||= Gitlab::Gpg.user_infos_from_key(key) @user_infos ||= Gitlab::Gpg.user_infos_from_key(key)
end end
...@@ -82,10 +91,11 @@ class GpgKey < ActiveRecord::Base ...@@ -82,10 +91,11 @@ class GpgKey < ActiveRecord::Base
def revoke def revoke
GpgSignature GpgSignature
.where(gpg_key: self) .with_key_and_subkeys(self)
.where.not(verification_status: GpgSignature.verification_statuses[:unknown_key]) .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
.update_all( .update_all(
gpg_key_id: nil, gpg_key_id: nil,
gpg_key_subkey_id: nil,
verification_status: GpgSignature.verification_statuses[:unknown_key], verification_status: GpgSignature.verification_statuses[:unknown_key],
updated_at: Time.zone.now updated_at: Time.zone.now
) )
...@@ -106,4 +116,12 @@ class GpgKey < ActiveRecord::Base ...@@ -106,4 +116,12 @@ class GpgKey < ActiveRecord::Base
# only allows one key # only allows one key
self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
end end
def generate_subkeys
gpg_subkeys = Gitlab::Gpg.subkeys_from_key(key)
gpg_subkeys[primary_keyid]&.each do |subkey_data|
subkeys.create!(keyid: subkey_data[:keyid], fingerprint: subkey_data[:fingerprint])
end
end
end end
class GpgKeySubkey < ActiveRecord::Base
include ShaAttribute
sha_attribute :keyid
sha_attribute :fingerprint
belongs_to :gpg_key
validates :gpg_key_id, presence: true
validates :fingerprint, :keyid, presence: true, uniqueness: true
delegate :key, :user, :user_infos, :verified?, :verified_user_infos,
:verified_and_belongs_to_email?, to: :gpg_key
def keyid
super&.upcase
end
def fingerprint
super&.upcase
end
end
...@@ -15,11 +15,42 @@ class GpgSignature < ActiveRecord::Base ...@@ -15,11 +15,42 @@ class GpgSignature < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :gpg_key belongs_to :gpg_key
belongs_to :gpg_key_subkey
validates :commit_sha, presence: true validates :commit_sha, presence: true
validates :project_id, presence: true validates :project_id, presence: true
validates :gpg_key_primary_keyid, presence: true validates :gpg_key_primary_keyid, presence: true
def self.with_key_and_subkeys(gpg_key)
subkey_ids = gpg_key.subkeys.pluck(:id)
where(
arel_table[:gpg_key_id].eq(gpg_key.id).or(
arel_table[:gpg_key_subkey_id].in(subkey_ids)
)
)
end
def gpg_key=(model)
case model
when GpgKey
super
when GpgKeySubkey
self.gpg_key_subkey = model
when NilClass
super
self.gpg_key_subkey = nil
end
end
def gpg_key
if gpg_key_id
super
elsif gpg_key_subkey_id
gpg_key_subkey
end
end
def gpg_key_primary_keyid def gpg_key_primary_keyid
super&.upcase super&.upcase
end end
......
...@@ -7,6 +7,13 @@ ...@@ -7,6 +7,13 @@
.description .description
%code= key.fingerprint %code= key.fingerprint
- if key.subkeys.present?
.subkeys
%span.bold Subkeys:
%ul.subkeys-list
- key.subkeys.each do |subkey|
%li
%code= subkey.fingerprint
.pull-right .pull-right
%span.key-created-at %span.key-created-at
created #{time_ago_with_tooltip(key.created_at)} created #{time_ago_with_tooltip(key.created_at)}
......
---
title: Add support for GPG subkeys in signature verification
merge_request: 14517
author:
type: added
class CreateGpgKeySubkeys < ActiveRecord::Migration
DOWNTIME = false
def up
create_table :gpg_key_subkeys do |t|
t.references :gpg_key, null: false, index: true, foreign_key: { on_delete: :cascade }
t.binary :keyid
t.binary :fingerprint
t.index :keyid, unique: true, length: Gitlab::Database.mysql? ? 20 : nil
t.index :fingerprint, unique: true, length: Gitlab::Database.mysql? ? 20 : nil
end
add_reference :gpg_signatures, :gpg_key_subkey, index: true, foreign_key: { on_delete: :nullify }
end
def down
remove_reference(:gpg_signatures, :gpg_key_subkey, index: true, foreign_key: true)
drop_table :gpg_key_subkeys
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ScheduleCreateGpgKeySubkeysFromGpgKeys < ActiveRecord::Migration
disable_ddl_transaction!
DOWNTIME = false
MIGRATION = 'CreateGpgKeySubkeysFromGpgKeys'
class GpgKey < ActiveRecord::Base
self.table_name = 'gpg_keys'
include EachBatch
end
def up
GpgKey.select(:id).each_batch do |gpg_keys|
jobs = gpg_keys.pluck(:id).map do |id|
[MIGRATION, [id]]
end
BackgroundMigrationWorker.perform_bulk(jobs)
end
end
def down
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171004121444) do ActiveRecord::Schema.define(version: 20171005130944) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -580,6 +580,16 @@ ActiveRecord::Schema.define(version: 20171004121444) do ...@@ -580,6 +580,16 @@ ActiveRecord::Schema.define(version: 20171004121444) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
create_table "gpg_key_subkeys", force: :cascade do |t|
t.integer "gpg_key_id", null: false
t.binary "keyid"
t.binary "fingerprint"
end
add_index "gpg_key_subkeys", ["fingerprint"], name: "index_gpg_key_subkeys_on_fingerprint", unique: true, using: :btree
add_index "gpg_key_subkeys", ["gpg_key_id"], name: "index_gpg_key_subkeys_on_gpg_key_id", using: :btree
add_index "gpg_key_subkeys", ["keyid"], name: "index_gpg_key_subkeys_on_keyid", unique: true, using: :btree
create_table "gpg_keys", force: :cascade do |t| create_table "gpg_keys", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
...@@ -603,11 +613,13 @@ ActiveRecord::Schema.define(version: 20171004121444) do ...@@ -603,11 +613,13 @@ ActiveRecord::Schema.define(version: 20171004121444) do
t.text "gpg_key_user_name" t.text "gpg_key_user_name"
t.text "gpg_key_user_email" t.text "gpg_key_user_email"
t.integer "verification_status", limit: 2, default: 0, null: false t.integer "verification_status", limit: 2, default: 0, null: false
t.integer "gpg_key_subkey_id"
end end
add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree
add_index "gpg_signatures", ["gpg_key_id"], name: "index_gpg_signatures_on_gpg_key_id", using: :btree add_index "gpg_signatures", ["gpg_key_id"], name: "index_gpg_signatures_on_gpg_key_id", using: :btree
add_index "gpg_signatures", ["gpg_key_primary_keyid"], name: "index_gpg_signatures_on_gpg_key_primary_keyid", using: :btree add_index "gpg_signatures", ["gpg_key_primary_keyid"], name: "index_gpg_signatures_on_gpg_key_primary_keyid", using: :btree
add_index "gpg_signatures", ["gpg_key_subkey_id"], name: "index_gpg_signatures_on_gpg_key_subkey_id", using: :btree
add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
create_table "identities", force: :cascade do |t| create_table "identities", force: :cascade do |t|
...@@ -1727,7 +1739,9 @@ ActiveRecord::Schema.define(version: 20171004121444) do ...@@ -1727,7 +1739,9 @@ ActiveRecord::Schema.define(version: 20171004121444) do
add_foreign_key "events", "projects", on_delete: :cascade add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
add_foreign_key "gpg_key_subkeys", "gpg_keys", on_delete: :cascade
add_foreign_key "gpg_keys", "users", on_delete: :cascade add_foreign_key "gpg_keys", "users", on_delete: :cascade
add_foreign_key "gpg_signatures", "gpg_key_subkeys", on_delete: :nullify
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
......
class Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys
class GpgKey < ActiveRecord::Base
self.table_name = 'gpg_keys'
include EachBatch
include ShaAttribute
sha_attribute :primary_keyid
sha_attribute :fingerprint
has_many :subkeys, class_name: 'GpgKeySubkey'
end
class GpgKeySubkey < ActiveRecord::Base
self.table_name = 'gpg_key_subkeys'
include ShaAttribute
sha_attribute :keyid
sha_attribute :fingerprint
end
def perform(gpg_key_id)
gpg_key = GpgKey.find_by(id: gpg_key_id)
return if gpg_key.nil?
return if gpg_key.subkeys.any?
create_subkeys(gpg_key)
update_signatures(gpg_key)
end
private
def create_subkeys(gpg_key)
gpg_subkeys = Gitlab::Gpg.subkeys_from_key(gpg_key.key)
gpg_subkeys[gpg_key.primary_keyid.upcase]&.each do |subkey_data|
gpg_key.subkeys.build(keyid: subkey_data[:keyid], fingerprint: subkey_data[:fingerprint])
end
# Improve latency by doing all INSERTs in a single call
GpgKey.transaction do
gpg_key.save!
end
end
def update_signatures(gpg_key)
return unless gpg_key.subkeys.exists?
InvalidGpgSignatureUpdateWorker.perform_async(gpg_key.id)
end
end
...@@ -34,6 +34,21 @@ module Gitlab ...@@ -34,6 +34,21 @@ module Gitlab
end end
end end
def subkeys_from_key(key)
using_tmp_keychain do
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
raw_keys = GPGME::Key.find(:public, fingerprints)
raw_keys.each_with_object({}) do |raw_key, grouped_subkeys|
primary_subkey_id = raw_key.primary_subkey.keyid
grouped_subkeys[primary_subkey_id] = raw_key.subkeys[1..-1].map do |s|
{ keyid: s.keyid, fingerprint: s.fingerprint }
end
end
end
end
def user_infos_from_key(key) def user_infos_from_key(key)
using_tmp_keychain do using_tmp_keychain do
fingerprints = CurrentKeyChain.fingerprints_from_key(key) fingerprints = CurrentKeyChain.fingerprints_from_key(key)
......
...@@ -43,7 +43,9 @@ module Gitlab ...@@ -43,7 +43,9 @@ module Gitlab
# key belonging to the keyid. # key belonging to the keyid.
# This way we can add the key to the temporary keychain and extract # This way we can add the key to the temporary keychain and extract
# the proper signature. # the proper signature.
gpg_key = GpgKey.find_by(primary_keyid: verified_signature.fingerprint) # NOTE: the invoked method is #fingerprint but it's only returning
# 16 characters (the format used by keyid) instead of 40.
gpg_key = find_gpg_key(verified_signature.fingerprint)
if gpg_key if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
...@@ -74,7 +76,7 @@ module Gitlab ...@@ -74,7 +76,7 @@ module Gitlab
commit_sha: @commit.sha, commit_sha: @commit.sha,
project: @commit.project, project: @commit.project,
gpg_key: gpg_key, gpg_key: gpg_key,
gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint, gpg_key_primary_keyid: gpg_key&.keyid || verified_signature.fingerprint,
gpg_key_user_name: user_infos[:name], gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email], gpg_key_user_email: user_infos[:email],
verification_status: verification_status verification_status: verification_status
...@@ -98,6 +100,10 @@ module Gitlab ...@@ -98,6 +100,10 @@ module Gitlab
def user_infos(gpg_key) def user_infos(gpg_key)
gpg_key&.verified_user_infos&.first || gpg_key&.user_infos&.first || {} gpg_key&.verified_user_infos&.first || gpg_key&.user_infos&.first || {}
end end
def find_gpg_key(keyid)
GpgKey.find_by(primary_keyid: keyid) || GpgKeySubkey.find_by(keyid: keyid)
end
end end
end end
end end
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
GpgSignature GpgSignature
.select(:id, :commit_sha, :project_id) .select(:id, :commit_sha, :project_id)
.where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified]) .where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified])
.where(gpg_key_primary_keyid: @gpg_key.primary_keyid) .where(gpg_key_primary_keyid: @gpg_key.keyids)
.find_each { |sig| sig.gpg_commit.update_signature!(sig) } .find_each { |sig| sig.gpg_commit.update_signature!(sig) }
end end
end end
......
require_relative '../support/gpg_helpers'
FactoryGirl.define do
factory :gpg_key_subkey do
gpg_key
sequence(:keyid) { |n| "keyid-#{n}" }
sequence(:fingerprint) { |n| "fingerprint-#{n}" }
end
end
...@@ -4,5 +4,9 @@ FactoryGirl.define do ...@@ -4,5 +4,9 @@ FactoryGirl.define do
factory :gpg_key do factory :gpg_key do
key GpgHelpers::User1.public_key key GpgHelpers::User1.public_key
user user
factory :gpg_key_with_subkeys do
key GpgHelpers::User1.public_key_with_extra_signing_key
end
end end
end end
...@@ -5,7 +5,7 @@ FactoryGirl.define do ...@@ -5,7 +5,7 @@ FactoryGirl.define do
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
project project
gpg_key gpg_key
gpg_key_primary_keyid { gpg_key.primary_keyid } gpg_key_primary_keyid { gpg_key.keyid }
verification_status :verified verification_status :verified
end end
end end
...@@ -20,6 +20,18 @@ feature 'Profile > GPG Keys' do ...@@ -20,6 +20,18 @@ feature 'Profile > GPG Keys' do
expect(page).to have_content('bette.cartwright@example.net Unverified') expect(page).to have_content('bette.cartwright@example.net Unverified')
expect(page).to have_content(GpgHelpers::User2.fingerprint) expect(page).to have_content(GpgHelpers::User2.fingerprint)
end end
scenario 'with multiple subkeys' do
fill_in('Key', with: GpgHelpers::User3.public_key)
click_button('Add key')
expect(page).to have_content('john.doe@example.com Unverified')
expect(page).to have_content(GpgHelpers::User3.fingerprint)
GpgHelpers::User3.subkey_fingerprints.each do |fingerprint|
expect(page).to have_content(fingerprint)
end
end
end end
scenario 'User sees their key' do scenario 'User sees their key' do
......
require 'spec_helper'
describe Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys, :migration, schema: 20171005130944 do
context 'when GpgKey exists' do
let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) }
before do
GpgKeySubkey.destroy_all
end
it 'generate the subkeys' do
expect do
described_class.new.perform(gpg_key.id)
end.to change { gpg_key.subkeys.count }.from(0).to(2)
end
it 'schedules the signature update worker' do
expect(InvalidGpgSignatureUpdateWorker).to receive(:perform_async).with(gpg_key.id)
described_class.new.perform(gpg_key.id)
end
end
context 'when GpgKey does not exist' do
it 'does not do anything' do
expect(Gitlab::Gpg).not_to receive(:subkeys_from_key)
expect(InvalidGpgSignatureUpdateWorker).not_to receive(:perform_async)
described_class.new.perform(123)
end
end
end
...@@ -63,6 +63,45 @@ describe Gitlab::Gpg::Commit do ...@@ -63,6 +63,45 @@ describe Gitlab::Gpg::Commit do
it_behaves_like 'returns the cached signature on second call' it_behaves_like 'returns the cached signature on second call'
end end
context 'commit signed with a subkey' do
let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User3.emails.first }
let!(:user) { create(:user, email: GpgHelpers::User3.emails.first) }
let!(:gpg_key) do
create :gpg_key, key: GpgHelpers::User3.public_key, user: user
end
let(:gpg_key_subkey) do
gpg_key.subkeys.find_by(fingerprint: '0522DD29B98F167CD8421752E38FFCAF75ABD92A')
end
before do
allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha)
.and_return(
[
GpgHelpers::User3.signed_commit_signature,
GpgHelpers::User3.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_subkey,
gpg_key_primary_keyid: gpg_key_subkey.keyid,
gpg_key_user_name: GpgHelpers::User3.names.first,
gpg_key_user_email: GpgHelpers::User3.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 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!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first }
......
...@@ -2,17 +2,16 @@ require 'rails_helper' ...@@ -2,17 +2,16 @@ require 'rails_helper'
RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
describe '#run' do describe '#run' do
let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } let(:signature) { [GpgHelpers::User1.signed_commit_signature, GpgHelpers::User1.signed_commit_base_data] }
let!(:project) { create :project, :repository, path: 'sample-project' } let(:committer_email) { GpgHelpers::User1.emails.first }
let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
let!(:project) { create :project, :repository, path: 'sample-project' }
let!(:raw_commit) do let!(:raw_commit) do
raw_commit = double( raw_commit = double(
:raw_commit, :raw_commit,
signature: [ signature: signature,
GpgHelpers::User1.signed_commit_signature,
GpgHelpers::User1.signed_commit_base_data
],
sha: commit_sha, sha: commit_sha,
committer_email: GpgHelpers::User1.emails.first committer_email: committer_email
) )
allow(raw_commit).to receive :save! allow(raw_commit).to receive :save!
...@@ -29,12 +28,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do ...@@ -29,12 +28,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
allow(Rugged::Commit).to receive(:extract_signature) allow(Rugged::Commit).to receive(:extract_signature)
.with(Rugged::Repository, commit_sha) .with(Rugged::Repository, commit_sha)
.and_return( .and_return(signature)
[
GpgHelpers::User1.signed_commit_signature,
GpgHelpers::User1.signed_commit_base_data
]
)
end end
context 'gpg signature did have an associated gpg key which was removed later' do context 'gpg signature did have an associated gpg key which was removed later' do
...@@ -183,5 +177,34 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do ...@@ -183,5 +177,34 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
) )
end end
end end
context 'gpg signature did not have an associated gpg subkey' do
let(:signature) { [GpgHelpers::User3.signed_commit_signature, GpgHelpers::User3.signed_commit_base_data] }
let(:committer_email) { GpgHelpers::User3.emails.first }
let!(:user) { create :user, email: GpgHelpers::User3.emails.first }
let!(:invalid_gpg_signature) do
create :gpg_signature,
project: project,
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User3.subkey_fingerprints.last[24..-1],
verification_status: 'unknown_key'
end
it 'updates the signature to being valid when the missing gpg key is added' do
# InvalidGpgSignatureUpdater is called by the after_create hook
gpg_key = create(:gpg_key, key: GpgHelpers::User3.public_key, user: user)
subkey = gpg_key.subkeys.last
expect(invalid_gpg_signature.reload).to have_attributes(
project: project,
commit_sha: commit_sha,
gpg_key_subkey_id: subkey.id,
gpg_key_primary_keyid: subkey.keyid,
verification_status: 'verified'
)
end
end
end end
end end
...@@ -28,6 +28,23 @@ describe Gitlab::Gpg do ...@@ -28,6 +28,23 @@ describe Gitlab::Gpg do
end end
end end
describe '.subkeys_from_key' do
it 'returns the subkeys by primary key' do
all_subkeys = described_class.subkeys_from_key(GpgHelpers::User1.public_key)
subkeys = all_subkeys[GpgHelpers::User1.primary_keyid]
expect(subkeys).to be_present
expect(subkeys.first[:keyid]).to be_present
expect(subkeys.first[:fingerprint]).to be_present
end
it 'returns an empty array when there are not subkeys' do
all_subkeys = described_class.subkeys_from_key(GpgHelpers::User4.public_key)
expect(all_subkeys[GpgHelpers::User4.primary_keyid]).to be_empty
end
end
describe '.user_infos_from_key' do describe '.user_infos_from_key' do
it 'returns the names and emails' do it 'returns the names and emails' do
user_infos = described_class.user_infos_from_key(GpgHelpers::User1.public_key) user_infos = described_class.user_infos_from_key(GpgHelpers::User1.public_key)
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20171005130944_schedule_create_gpg_key_subkeys_from_gpg_keys')
describe ScheduleCreateGpgKeySubkeysFromGpgKeys, :migration, :sidekiq do
matcher :be_scheduled_migration do |*expected|
match do |migration|
BackgroundMigrationWorker.jobs.any? do |job|
job['args'] == [migration, expected]
end
end
failure_message do |migration|
"Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
end
end
before do
create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key)
create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key)
# Delete all subkeys so they can be recreated
GpgKeySubkey.destroy_all
end
it 'correctly schedules background migrations' do
Sidekiq::Testing.fake! do
migrate!
expect(described_class::MIGRATION).to be_scheduled_migration(1)
expect(described_class::MIGRATION).to be_scheduled_migration(2)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
it 'schedules background migrations' do
Sidekiq::Testing.inline! do
expect(GpgKeySubkey.count).to eq(0)
migrate!
expect(GpgKeySubkey.count).to eq(3)
end
end
end
...@@ -3,6 +3,7 @@ require 'rails_helper' ...@@ -3,6 +3,7 @@ require 'rails_helper'
describe GpgKey do describe GpgKey do
describe "associations" do describe "associations" do
it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:subkeys) }
end end
describe "validation" do describe "validation" do
...@@ -38,6 +39,14 @@ describe GpgKey do ...@@ -38,6 +39,14 @@ describe GpgKey do
expect(gpg_key.primary_keyid).to eq GpgHelpers::User1.primary_keyid expect(gpg_key.primary_keyid).to eq GpgHelpers::User1.primary_keyid
end end
end end
describe 'generate_subkeys' do
it 'extracts the subkeys from the gpg key' do
gpg_key = create(:gpg_key, key: GpgHelpers::User1.public_key_with_extra_signing_key)
expect(gpg_key.subkeys.count).to eq(2)
end
end
end end
describe '#key=' do describe '#key=' do
...@@ -182,5 +191,29 @@ describe GpgKey do ...@@ -182,5 +191,29 @@ describe GpgKey do
expect(unrelated_gpg_key.destroyed?).to be false expect(unrelated_gpg_key.destroyed?).to be false
end end
it 'deletes all the associated subkeys' do
gpg_key = create :gpg_key, key: GpgHelpers::User3.public_key
expect(gpg_key.subkeys).to be_present
gpg_key.revoke
expect(gpg_key.subkeys(true)).to be_blank
end
it 'invalidates all signatures associated to the subkeys' do
gpg_key = create :gpg_key, key: GpgHelpers::User3.public_key
gpg_key_subkey = gpg_key.subkeys.last
gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: gpg_key_subkey
gpg_key.revoke
expect(gpg_signature.reload).to have_attributes(
verification_status: 'unknown_key',
gpg_key: nil,
gpg_key_subkey: nil
)
end
end end
end end
require 'rails_helper'
describe GpgKeySubkey do
subject { build(:gpg_key_subkey) }
describe 'associations' do
it { is_expected.to belong_to(:gpg_key) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:gpg_key_id) }
it { is_expected.to validate_presence_of(:fingerprint) }
it { is_expected.to validate_presence_of(:keyid) }
end
end
require 'rails_helper' require 'rails_helper'
RSpec.describe GpgSignature do RSpec.describe GpgSignature do
let(:gpg_key) { create(:gpg_key) }
let(:gpg_key_subkey) { create(:gpg_key_subkey) }
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:gpg_key) } it { is_expected.to belong_to(:gpg_key) }
it { is_expected.to belong_to(:gpg_key_subkey) }
end end
describe 'validation' do describe 'validation' do
...@@ -25,4 +29,26 @@ RSpec.describe GpgSignature do ...@@ -25,4 +29,26 @@ RSpec.describe GpgSignature do
gpg_signature.commit gpg_signature.commit
end end
end end
describe '#gpg_key=' do
it 'supports the assignment of a GpgKey' do
gpg_signature = create(:gpg_signature, gpg_key: gpg_key)
expect(gpg_signature.gpg_key).to be_an_instance_of(GpgKey)
end
it 'supports the assignment of a GpgKeySubkey' do
gpg_signature = create(:gpg_signature, gpg_key: gpg_key_subkey)
expect(gpg_signature.gpg_key).to be_an_instance_of(GpgKeySubkey)
end
it 'clears gpg_key and gpg_key_subkey_id when passing nil' do
gpg_signature = create(:gpg_signature, gpg_key: gpg_key_subkey)
gpg_signature.update_attribute(:gpg_key, nil)
expect(gpg_signature.gpg_key_id).to be_nil
expect(gpg_signature.gpg_key_subkey_id).to be_nil
end
end
end end
...@@ -18,4 +18,14 @@ describe GpgKeys::CreateService do ...@@ -18,4 +18,14 @@ describe GpgKeys::CreateService do
it 'creates a gpg key' do it 'creates a gpg key' do
expect { subject.execute }.to change { user.gpg_keys.where(params).count }.by(1) expect { subject.execute }.to change { user.gpg_keys.where(params).count }.by(1)
end end
context 'when the public key contains subkeys' do
let(:params) { attributes_for(:gpg_key_with_subkeys) }
it 'generates the gpg subkeys' do
gpg_key = subject.execute
expect(gpg_key.subkeys.count).to eq(2)
end
end
end end
...@@ -92,6 +92,46 @@ module GpgHelpers ...@@ -92,6 +92,46 @@ module GpgHelpers
KEY KEY
end end
def public_key_with_extra_signing_key
<<~KEY.strip
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mI0EWK7VJwEEANSFayuVYenl7sBKUjmIxwDRc3jd+K+FWUZgknLgiLcevaLh/mxV
98dLxDKGDHHNKc/B7Y4qdlZYv1wfNQVuIbd8dqUQFOOkH7ukbgcGBTxH+2IM67y+
QBH618luS5Gz1d4bd0YoFf/xZGEh9G5xicz7TiXYzLKjnMjHu2EmbFePABEBAAG0
LU5hbm5pZSBCZXJuaGFyZCA8bmFubmllLmJlcm5oYXJkQGV4YW1wbGUuY29tPoi4
BBMBAgAiBQJYrtUnAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDM++Gf
AKyLHaeSA/99oUWpb02PlfkALcx5RncboMHkgczYEU9wOFIgvXIReswThCMOvPZa
piui+ItyJfV3ijJfO8IvbbFcvU7jjGA073Bb7tbzAEOQLA16mWgBLQlGaRWbHDW4
uwFxvkJKA0GzEsadEXeniESaZPc4rOXKPO3+/MSQWS2bmvvGsBTEuriNBFiu1ScB
BADIXkITf+kKCkD+n8tMsdTLInefu8KrJ8p7YRYCCabEXnWRsDb5zxUAG2VXCVUh
Yl6QXQybkNiBaduS+uxilz7gtYZUMFJvQ09+fV7D2N9B7u/1bGdIYz+cDFJnEJit
LY4w/nju2Sno5CL5Ead8sZuslKetSXPYHR/kbW462EOw5wARAQABiJ8EGAECAAkF
Aliu1ScCGwwACgkQzPvhnwCsix2WRQQAtOXpBS60myrBUXhlcqabDQgSTw+Spbgb
61hEMckyzpk7SfMNLz0EbYMvj9SU6znBG8RGeUljPTVMxPGr9yIpoFMSPKAUi/0K
AgRmH3tVpxlMipwXjST1Jukk2eHckt/3jGw3E1ElMSFtULe6u5p4gu578hHukEwT
IKzj0ZyC7DK5AQ0EWcx23AEIANwpAq85bT10JCBuNhOMyF2jKVt5wHbI9wBtjWYG
fgJFBkRvm6IsbmR0Y5DSBvF/of0UX1iGMfx6mvCDJkb1okquhCUef6MONWRpzXYE
CIZDm1TXu6yv0D35tkLfPo+/sY9UHHp1zGRcPAU46e8ztRwoD+zEJwy7lobLHGOL
9OdWtCGjsutLOTqKRK4jsifr8n3rePU09rejhDkRONNs7ufn9GRcWMN7RWiFDtpU
gNe84AJ38qaXPU8GHNTrDtDtRRPmn68ezMmE1qTNsxQxD4Isexe5Wsfc4+ElaP9s
zaHgij7npX1HS9RpmhnOa2h1ESroM9cqDh3IJVhf+eP6/uMAEQEAAYkBxAQYAQIA
DwUCWcx23AIbAgUJAeEzgAEpCRDM++GfAKyLHcBdIAQZAQIABgUCWcx23AAKCRDk
garE0uOuES7DCAC2Kgl6zO+NqIBIS6McgcEN0sGyvOvZ8Ps4hBiMwCyDAnsIRAUi
v4KZMtQMAyl9njJ3YjPWBsdieuTz45O06DDnrzJpZO5rUGJjAcEue4zvRRWIyu3H
qHC8MsvkslsNCygJHoWlknm+HucroskTNtxHQ+FdKZ6Tey+twl1u+PhV8PQVyFkl
4G1chO90EP4dvYrye26CC+ik2JkvC7Vy5M+U0PJikme8pFMjcdNks25BnAKcdqKU
AU8RTkSjoYvb8qSmZyldJjYjQRkTPRX1ZdaOID1EdiWl+s5cn0Oypo3z7BChcEMx
IWB/gmXQJQXVr5qNQnJObyMO/RczXYi9qNnyGMED/2EJJERLR0nefjHQalwDKQVP
s5lX1OKAcf2CrV6ZarckqaQgtqjZbuV2C2mrOHUs5uojlXaopj5gA4yJSGDcYhj1
Rg9jdHWBtkHBj3eL32ZqrHDs3ap8ErZMmPE8A+mn9TTnQS+FY2QF7vBjJKM3qPT7
DMVGWrg4m1NF8N6yMPMP
=RB1y
-----END PGP PUBLIC KEY BLOCK-----
KEY
end
def primary_keyid def primary_keyid
fingerprint[-16..-1] fingerprint[-16..-1]
end end
...@@ -201,4 +241,277 @@ module GpgHelpers ...@@ -201,4 +241,277 @@ module GpgHelpers
['bette.cartwright@example.com', 'bette.cartwright@example.net'] ['bette.cartwright@example.com', 'bette.cartwright@example.net']
end end
end end
# GPG Key with extra signing key
module User3
extend self
def signed_commit_signature
<<~SIGNATURE
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEBSLdKbmPFnzYQhdS44/8r3Wr2SoFAlnNlT8ACgkQ44/8r3Wr
2SqP1wf9FC4J2S8LIHs/fpxgkYzsyCp5lCbS7JuoD4pqmI2KWyBx+vi9/3mZPCsm
Fj9f0vFEtNOb39GNGZbaA8DdGw30/WAS6kI6yco0WSK53KHrLw9Kqd+3e/NAVSsl
991Gq4n8X1U5izSH+gZOMtEEUBGqIlZKgRrEh7lhNcz0G7JTF2VCE4NNtZdq7GDA
N6jOQxDGUwi9wQBYORQzIBc3NihfhGloII1hXf0XzrgUY3zNYHTT7QipCxKAmH54
skwW+wi8RpBedar4saf7fs5xZbP/0yyVz98MJMdHBL68++Xt1AIHoqrb7eWszqnd
PCo/fnz1iHKCig602KLj0/zhADcNkg==
=LsTi
-----END PGP SIGNATURE-----
SIGNATURE
end
def signed_commit_base_data
<<~SIGNEDDATA
tree 86ec18bfe87ad42a782fdabd8310f9b7ac750f51
parent 2d1096e3a0ecf1d2baf6dee036cc80775d4940ba
author John Doe <john.doe@example.com> 1506645311 -0500
committer John Doe <john.doe@example.com> 1506645311 -0500
Commit signed with subkey by John Doe
SIGNEDDATA
end
def public_key
<<~KEY.strip
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
/jQYeOgFDKq20x86WulkvsUtBoaZJg==
=Q5Z7
-----END PGP PUBLIC KEY BLOCK-----
KEY
end
def secret_key
<<~SECRET
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
Jg==
=TKlF
-----END PGP PRIVATE KEY BLOCK-----
SECRET
end
def fingerprint
'EA3F8B88972A8FD474872F2F115B3AF1AAD3EDBA'
end
def subkey_fingerprints
%w(159AD5DDF199591D67D2B87AA3CEC5C0A7C270EC 0522DD29B98F167CD8421752E38FFCAF75ABD92A)
end
def names
['John Doe']
end
def emails
['john.doe@example.com']
end
end
# GPG Key containing just the main key
module User4
extend self
def public_key
<<~KEY.strip
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFnWcesBCAC6Y8FXl9ZJ9HPa6dIYcgQrvjIQcwoQCUEsaXNRpc+206RPCIXK
aIYr0nTD8GeovMuUONXTj+DdueQU2GAAqHHOqvDDVXqRrW3xfWnSwix7sTuhG1Ew
PLHYmjLENqaTsdyliEo3N8VWy2k0QRbC3R6xvop4Ooa87D5vcATIl0gYFtSiHIL+
TervYvTG9Eq1qSLZHbe2x4IzeqX2luikPKokL7j8FTZaCmC5MezIUur1ulfyYY/j
SkST/1aUFc5QXJJSZA0MYJWZX6x7Y3l7yl0dkHqmK8OTuo8RPWd3ybEiuvRsOL8K
GAv/PmVJRGDAf7GGbwXXsE9MiZ5GzVPxHnexABEBAAG0G0pvaG4gRG9lIDxqb2hu
QGV4YW1wbGUuY29tPokBTgQTAQgAOBYhBAh0izYM0lwuzJnVlAcBbPnhOj+bBQJZ
1nHrAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEAcBbPnhOj+bkywH/i4w
OwpDxoTjUQlPlqGAGuzvWaPzSJndawgmMTr68oRsD+wlQmQQTR5eqxCpUIyV4aYb
D697RYzoqbT4mlU49ymzfKSAxFe88r1XQWdm81DcofHVPmw2GBrIqaX3Du4Z7xkI
Q9/S43orwknh5FoVwU8Nau7qBuv9vbw2apSkuA1oBj3spQ8hqwLavACyQ+fQloAT
hSDNqPiCZj6L0dwM1HYiqVoN3Q7qjgzzeBzlXzljJoWblhxllvMK20bVoa7H+uR2
lczFHfsX8VTIMjyTGP7R3oHN91DEahlQybVVNLmNSDKZM2P/0d28BRUmWxQJ4Ws3
J4hOWDKnLMed3VOIWzM=
=xVuW
-----END PGP PUBLIC KEY BLOCK-----
KEY
end
def secret_key
<<~KEY.strip
-----BEGIN PGP PRIVATE KEY BLOCK-----
lQPGBFnWcesBCAC6Y8FXl9ZJ9HPa6dIYcgQrvjIQcwoQCUEsaXNRpc+206RPCIXK
aIYr0nTD8GeovMuUONXTj+DdueQU2GAAqHHOqvDDVXqRrW3xfWnSwix7sTuhG1Ew
PLHYmjLENqaTsdyliEo3N8VWy2k0QRbC3R6xvop4Ooa87D5vcATIl0gYFtSiHIL+
TervYvTG9Eq1qSLZHbe2x4IzeqX2luikPKokL7j8FTZaCmC5MezIUur1ulfyYY/j
SkST/1aUFc5QXJJSZA0MYJWZX6x7Y3l7yl0dkHqmK8OTuo8RPWd3ybEiuvRsOL8K
GAv/PmVJRGDAf7GGbwXXsE9MiZ5GzVPxHnexABEBAAH+BwMC4UwgHgH5Cp7meY39
G5Q3GV2xtwADoaAvlOvPOLPK2fQqxQfb4WN4eZECp2wQuMRBMj52c4i9yphab1mQ
vOzoPIRGvkcJoxG++OxQ0kRk0C0gX6wM6SGVdb1nQnfZnoJCCU3IwCaSGktkLDs1
jwdI+VmXJbSugUbd25bakHQcE2BaNHuRBlQWQfFbhGBy0+uMfNDBZ6FRipBu47hO
f/wm/xXuV8N8BSgvNR/qtAqSQI34CdsnWAhMYm9rqmTNyt0nq4dveX+E0YzVn4lH
lOEa7cpYeuBwIL8L3EvSPNCICiJlF3gVqiYzyqRElnCkv1OGc0x3W5onY/agHgGZ
KYyi/ubOdqqDgBR+eMt0JKSGH2EPxUAGFPY5F37u4erdxH86GzIinAExLSmADiVR
KtxluZP6S2KLbETN5uVbrfa+HVcMbbUZaBHHtL+YbY8PqaFUIvIUR1HM2SK7IrFw
KuQ8ibRgooyP7VgMNiPzlFpY4NXUv+FXIrNJ6ELuIaENi0izJ7aIbVBM8SijDz6u
5EEmodnDvmU2hmQNZJ17TxggE7oeT0rKdDGHM5zBvqZ3deqE9sgKx/aTKcj61ID3
M80ZkHPDFazUCohLpYgFN20bYYSmxU4LeNFy8YEiuic8QQKaAFxSf9Lf87UFQwyF
dduI1RWEbjMsbEJXwlmGM02ssQHsgoVKwZxijq5A5R1Ul6LowazQ8obPiwRS4NZ4
Z+QKDon79MMXiFEeh1jeG/MKKWPxFg3pdtCWhC7WdH4hfkBsCVKf+T58yB2Gzziy
fOHvAl7v3PtdZgf1xikF8spGYGCWo4B2lxC79xIflKAb2U6myb5I4dpUYxzxoMxT
zxHwxEie3NxzZGUyXSt3LqYe2r4CxWnOCXWjIxxRlLue1BE5Za1ycnDRjgUO24+Z
uDQne6KLkhAotBtKb2huIERvZSA8am9obkBleGFtcGxlLmNvbT6JAU4EEwEIADgW
IQQIdIs2DNJcLsyZ1ZQHAWz54To/mwUCWdZx6wIbAwULCQgHAgYVCAkKCwIEFgID
AQIeAQIXgAAKCRAHAWz54To/m5MsB/4uMDsKQ8aE41EJT5ahgBrs71mj80iZ3WsI
JjE6+vKEbA/sJUJkEE0eXqsQqVCMleGmGw+ve0WM6Km0+JpVOPcps3ykgMRXvPK9
V0FnZvNQ3KHx1T5sNhgayKml9w7uGe8ZCEPf0uN6K8JJ4eRaFcFPDWru6gbr/b28
NmqUpLgNaAY97KUPIasC2rwAskPn0JaAE4Ugzaj4gmY+i9HcDNR2IqlaDd0O6o4M
83gc5V85YyaFm5YcZZbzCttG1aGux/rkdpXMxR37F/FUyDI8kxj+0d6BzfdQxGoZ
UMm1VTS5jUgymTNj/9HdvAUVJlsUCeFrNyeITlgypyzHnd1TiFsz
=/37z
-----END PGP PRIVATE KEY BLOCK-----
KEY
end
def primary_keyid
fingerprint[-16..-1]
end
def fingerprint
'08748B360CD25C2ECC99D59407016CF9E13A3F9B'
end
end
end 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